initial commit
This commit is contained in:
Generated
+2665
File diff suppressed because it is too large
Load Diff
+54
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "grin_wallet"
|
||||
version = "1.1.0"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/mimblewimble/grin"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
readme = "README.md"
|
||||
exclude = ["**/*.grin", "**/*.grin2"]
|
||||
build = "src/build/build.rs"
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "grin-wallet"
|
||||
path = "src/bin/grin-wallet.rs"
|
||||
|
||||
[workspace]
|
||||
members = ["libwallet", "config"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "2.31", features = ["yaml"] }
|
||||
rpassword = "2.0.0"
|
||||
ctrlc = { version = "3.1", features = ["termination"] }
|
||||
blake2-rfc = "0.2"
|
||||
byteorder = "1"
|
||||
failure = "0.1"
|
||||
failure_derive = "0.1"
|
||||
futures = "0.1"
|
||||
hyper = "0.12"
|
||||
prettytable-rs = "0.7"
|
||||
rand = "0.5"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
term = "0.5"
|
||||
tokio = "= 0.1.11"
|
||||
tokio-core = "0.1"
|
||||
tokio-retry = "0.1"
|
||||
ring = "0.13"
|
||||
uuid = { version = "0.6", features = ["serde", "v4"] }
|
||||
url = "1.7.0"
|
||||
chrono = { version = "0.4.4", features = ["serde"] }
|
||||
|
||||
grin_api = { path = "../grin/api", version = "1.1.0" }
|
||||
grin_core = { path = "../grin/core", version = "1.1.0" }
|
||||
grin_keychain = { path = "../grin/keychain", version = "1.1.0" }
|
||||
grin_store = { path = "../grin/store", version = "1.1.0" }
|
||||
grin_util = { path = "../grin/util", version = "1.1.0" }
|
||||
grin_chain = { path = "../grin/chain", version = "1.1.0" }
|
||||
|
||||
[build-dependencies]
|
||||
built = "0.3"
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "grin_wallet_config"
|
||||
version = "1.1.0"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
description = "Configuration for grin wallet , a simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/mimblewimble/grin"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
workspace = ".."
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.5"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
toml = "0.4"
|
||||
dirs = "1.0.3"
|
||||
|
||||
grin_core = { path = "../../grin/core", version = "1.1.0" }
|
||||
grin_util = { path = "../../grin/util", version = "1.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.5.1"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,223 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Comments for configuration + injection into output .toml
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// maps entries to Comments that should precede them
|
||||
fn comments() -> HashMap<String, String> {
|
||||
let mut retval = HashMap::new();
|
||||
|
||||
retval.insert(
|
||||
"[wallet]".to_string(),
|
||||
"
|
||||
#########################################
|
||||
### WALLET CONFIGURATION ###
|
||||
#########################################
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"api_listen_interface".to_string(),
|
||||
"
|
||||
#host IP for wallet listener, change to \"0.0.0.0\" to receive grins
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"api_listen_port".to_string(),
|
||||
"
|
||||
#path of TLS certificate file, self-signed certificates are not supported
|
||||
#tls_certificate_file = \"\"
|
||||
#private key for the TLS certificate
|
||||
#tls_certificate_key = \"\"
|
||||
|
||||
#port for wallet listener
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"owner_api_listen_port".to_string(),
|
||||
"
|
||||
#port for wallet owner api
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"api_secret_path".to_string(),
|
||||
"
|
||||
#path of the secret token used by the API to authenticate the calls
|
||||
#comment it to disable basic auth
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"check_node_api_http_addr".to_string(),
|
||||
"
|
||||
#where the wallet should find a running node
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"node_api_secret_path".to_string(),
|
||||
"
|
||||
#location of the node api secret for basic auth on the Grin API
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"owner_api_include_foreign".to_string(),
|
||||
"
|
||||
#include the foreign API endpoints on the same port as the owner
|
||||
#API. Useful for networking environments like AWS ECS that make
|
||||
#it difficult to access multiple ports on a single service.
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"data_file_dir".to_string(),
|
||||
"
|
||||
#where to find wallet files (seed, data, etc)
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"no_commit_cache".to_string(),
|
||||
"
|
||||
#If true, don't store calculated commits in the database
|
||||
#better privacy, but at a performance cost of having to
|
||||
#re-calculate commits every time they're used
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"dark_background_color_scheme".to_string(),
|
||||
"
|
||||
#Whether to use the black background color scheme for command line
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
retval.insert(
|
||||
"keybase_notify_ttl".to_string(),
|
||||
"
|
||||
#The exploding lifetime for keybase notification on coins received.
|
||||
#Unit: Minute. Default value 1440 minutes for one day.
|
||||
#Refer to https://keybase.io/blog/keybase-exploding-messages for detail.
|
||||
#To disable this notification, set it as 0.
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"[logging]".to_string(),
|
||||
"
|
||||
#########################################
|
||||
### LOGGING CONFIGURATION ###
|
||||
#########################################
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"log_to_stdout".to_string(),
|
||||
"
|
||||
#whether to log to stdout
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"stdout_log_level".to_string(),
|
||||
"
|
||||
#log level for stdout: Error, Warning, Info, Debug, Trace
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"log_to_file".to_string(),
|
||||
"
|
||||
#whether to log to a file
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"file_log_level".to_string(),
|
||||
"
|
||||
#log level for file: Error, Warning, Info, Debug, Trace
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"log_file_path".to_string(),
|
||||
"
|
||||
#log file path
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"log_file_append".to_string(),
|
||||
"
|
||||
#whether to append to the log file (true), or replace it on every run (false)
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval.insert(
|
||||
"log_max_size".to_string(),
|
||||
"
|
||||
#maximum log file size in bytes before performing log rotation
|
||||
#comment it to disable log rotation
|
||||
"
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
retval
|
||||
}
|
||||
|
||||
fn get_key(line: &str) -> String {
|
||||
if line.contains("[") && line.contains("]") {
|
||||
return line.to_owned();
|
||||
} else if line.contains("=") {
|
||||
return line.split("=").collect::<Vec<&str>>()[0].trim().to_owned();
|
||||
} else {
|
||||
return "NOT_FOUND".to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_comments(orig: String) -> String {
|
||||
let comments = comments();
|
||||
let lines: Vec<&str> = orig.split("\n").collect();
|
||||
let mut out_lines = vec![];
|
||||
for l in lines {
|
||||
let key = get_key(l);
|
||||
if let Some(v) = comments.get(&key) {
|
||||
out_lines.push(v.to_owned());
|
||||
}
|
||||
out_lines.push(l.to_owned());
|
||||
out_lines.push("\n".to_owned());
|
||||
}
|
||||
let mut ret_val = String::from("");
|
||||
for l in out_lines {
|
||||
ret_val.push_str(&l);
|
||||
}
|
||||
ret_val.to_owned()
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Configuration file management
|
||||
|
||||
use dirs;
|
||||
use rand::distributions::{Alphanumeric, Distribution};
|
||||
use rand::thread_rng;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use crate::comments::insert_comments;
|
||||
use crate::core::global;
|
||||
use crate::p2p;
|
||||
use crate::servers::ServerConfig;
|
||||
use crate::types::{
|
||||
ConfigError, ConfigMembers, GlobalConfig, GlobalWalletConfig, GlobalWalletConfigMembers,
|
||||
};
|
||||
use crate::util::LoggingConfig;
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Wallet configuration file name
|
||||
pub const WALLET_CONFIG_FILE_NAME: &'static str = "grin-wallet.toml";
|
||||
const WALLET_LOG_FILE_NAME: &'static str = "grin-wallet.log";
|
||||
const GRIN_HOME: &'static str = ".grin";
|
||||
/// Wallet data directory
|
||||
pub const GRIN_WALLET_DIR: &'static str = "wallet_data";
|
||||
pub const API_SECRET_FILE_NAME: &'static str = ".api_secret";
|
||||
|
||||
fn get_grin_path(chain_type: &global::ChainTypes) -> Result<PathBuf, ConfigError> {
|
||||
// Check if grin dir exists
|
||||
let mut grin_path = match dirs::home_dir() {
|
||||
Some(p) => p,
|
||||
None => PathBuf::new(),
|
||||
};
|
||||
grin_path.push(GRIN_HOME);
|
||||
grin_path.push(chain_type.shortname());
|
||||
// Create if the default path doesn't exist
|
||||
if !grin_path.exists() {
|
||||
fs::create_dir_all(grin_path.clone())?;
|
||||
}
|
||||
Ok(grin_path)
|
||||
}
|
||||
|
||||
fn check_config_current_dir(path: &str) -> Option<PathBuf> {
|
||||
let p = env::current_dir();
|
||||
let mut c = match p {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
c.push(path);
|
||||
if c.exists() {
|
||||
return Some(c);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Create file with api secret
|
||||
pub fn init_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> {
|
||||
let mut api_secret_file = File::create(api_secret_path)?;
|
||||
let api_secret: String = Alphanumeric
|
||||
.sample_iter(&mut thread_rng())
|
||||
.take(20)
|
||||
.collect();
|
||||
api_secret_file.write_all(api_secret.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if file contains a secret and nothing else
|
||||
pub fn check_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> {
|
||||
let api_secret_file = File::open(api_secret_path)?;
|
||||
let buf_reader = BufReader::new(api_secret_file);
|
||||
let mut lines_iter = buf_reader.lines();
|
||||
let first_line = lines_iter.next();
|
||||
if first_line.is_none() || first_line.unwrap().is_err() {
|
||||
fs::remove_file(api_secret_path)?;
|
||||
init_api_secret(api_secret_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that the api secret file exists and is valid
|
||||
fn check_api_secret_file(chain_type: &global::ChainTypes) -> Result<(), ConfigError> {
|
||||
let grin_path = get_grin_path(chain_type)?;
|
||||
let mut api_secret_path = grin_path.clone();
|
||||
api_secret_path.push(API_SECRET_FILE_NAME);
|
||||
if !api_secret_path.exists() {
|
||||
init_api_secret(&api_secret_path)
|
||||
} else {
|
||||
check_api_secret(&api_secret_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles setup and detection of paths for wallet
|
||||
pub fn initial_setup_wallet(
|
||||
chain_type: &global::ChainTypes,
|
||||
) -> Result<GlobalWalletConfig, ConfigError> {
|
||||
check_api_secret_file(chain_type)?;
|
||||
// Use config file if current directory if it exists, .grin home otherwise
|
||||
if let Some(p) = check_config_current_dir(WALLET_CONFIG_FILE_NAME) {
|
||||
GlobalWalletConfig::new(p.to_str().unwrap())
|
||||
} else {
|
||||
// Check if grin dir exists
|
||||
let grin_path = get_grin_path(chain_type)?;
|
||||
|
||||
// Get path to default config file
|
||||
let mut config_path = grin_path.clone();
|
||||
config_path.push(WALLET_CONFIG_FILE_NAME);
|
||||
|
||||
// Spit it out if it doesn't exist
|
||||
if !config_path.exists() {
|
||||
let mut default_config = GlobalWalletConfig::for_chain(chain_type);
|
||||
// update paths relative to current dir
|
||||
default_config.update_paths(&grin_path);
|
||||
default_config.write_to_file(config_path.to_str().unwrap())?;
|
||||
}
|
||||
|
||||
GlobalWalletConfig::new(config_path.to_str().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GlobalWalletConfigMembers {
|
||||
fn default() -> GlobalWalletConfigMembers {
|
||||
GlobalWalletConfigMembers {
|
||||
logging: Some(LoggingConfig::default()),
|
||||
wallet: WalletConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GlobalWalletConfig {
|
||||
fn default() -> GlobalWalletConfig {
|
||||
GlobalWalletConfig {
|
||||
config_file_path: None,
|
||||
members: Some(GlobalWalletConfigMembers::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalWalletConfig {
|
||||
/// Same as GlobalConfig::default() but further tweaks parameters to
|
||||
/// apply defaults for each chain type
|
||||
pub fn for_chain(chain_type: &global::ChainTypes) -> GlobalWalletConfig {
|
||||
let mut defaults_conf = GlobalWalletConfig::default();
|
||||
let mut defaults = &mut defaults_conf.members.as_mut().unwrap().wallet;
|
||||
defaults.chain_type = Some(chain_type.clone());
|
||||
|
||||
match *chain_type {
|
||||
global::ChainTypes::Mainnet => {}
|
||||
global::ChainTypes::Floonet => {
|
||||
defaults.api_listen_port = 13415;
|
||||
defaults.check_node_api_http_addr = "http://127.0.0.1:13413".to_owned();
|
||||
}
|
||||
global::ChainTypes::UserTesting => {
|
||||
defaults.api_listen_port = 23415;
|
||||
defaults.check_node_api_http_addr = "http://127.0.0.1:23413".to_owned();
|
||||
}
|
||||
global::ChainTypes::AutomatedTesting => {
|
||||
panic!("Can't run automated testing directly");
|
||||
}
|
||||
}
|
||||
defaults_conf
|
||||
}
|
||||
/// Requires the path to a config file
|
||||
pub fn new(file_path: &str) -> Result<GlobalWalletConfig, ConfigError> {
|
||||
let mut return_value = GlobalWalletConfig::default();
|
||||
return_value.config_file_path = Some(PathBuf::from(&file_path));
|
||||
|
||||
// Config file path is given but not valid
|
||||
let config_file = return_value.config_file_path.clone().unwrap();
|
||||
if !config_file.exists() {
|
||||
return Err(ConfigError::FileNotFoundError(String::from(
|
||||
config_file.to_str().unwrap(),
|
||||
)));
|
||||
}
|
||||
|
||||
// Try to parse the config file if it exists, explode if it does exist but
|
||||
// something's wrong with it
|
||||
return_value.read_config()
|
||||
}
|
||||
|
||||
/// Read config
|
||||
fn read_config(mut self) -> Result<GlobalWalletConfig, ConfigError> {
|
||||
let mut file = File::open(self.config_file_path.as_mut().unwrap())?;
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
let decoded: Result<GlobalWalletConfigMembers, toml::de::Error> = toml::from_str(&contents);
|
||||
match decoded {
|
||||
Ok(gc) => {
|
||||
self.members = Some(gc);
|
||||
return Ok(self);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ConfigError::ParseError(
|
||||
String::from(
|
||||
self.config_file_path
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
),
|
||||
String::from(format!("{}", e)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update paths
|
||||
pub fn update_paths(&mut self, wallet_home: &PathBuf) {
|
||||
let mut wallet_path = wallet_home.clone();
|
||||
wallet_path.push(GRIN_WALLET_DIR);
|
||||
self.members.as_mut().unwrap().wallet.data_file_dir =
|
||||
wallet_path.to_str().unwrap().to_owned();
|
||||
let mut secret_path = wallet_home.clone();
|
||||
secret_path.push(API_SECRET_FILE_NAME);
|
||||
self.members.as_mut().unwrap().wallet.api_secret_path =
|
||||
Some(secret_path.to_str().unwrap().to_owned());
|
||||
let mut node_secret_path = wallet_home.clone();
|
||||
node_secret_path.push(API_SECRET_FILE_NAME);
|
||||
self.members.as_mut().unwrap().wallet.node_api_secret_path =
|
||||
Some(node_secret_path.to_str().unwrap().to_owned());
|
||||
let mut log_path = wallet_home.clone();
|
||||
log_path.push(WALLET_LOG_FILE_NAME);
|
||||
self.members
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.logging
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.log_file_path = log_path.to_str().unwrap().to_owned();
|
||||
}
|
||||
|
||||
/// Serialize config
|
||||
pub fn ser_config(&mut self) -> Result<String, ConfigError> {
|
||||
let encoded: Result<String, toml::ser::Error> =
|
||||
toml::to_string(self.members.as_mut().unwrap());
|
||||
match encoded {
|
||||
Ok(enc) => return Ok(enc),
|
||||
Err(e) => {
|
||||
return Err(ConfigError::SerializationError(String::from(format!(
|
||||
"{}",
|
||||
e
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write configuration to a file
|
||||
pub fn write_to_file(&mut self, name: &str) -> Result<(), ConfigError> {
|
||||
let conf_out = self.ser_config()?;
|
||||
let conf_out = insert_comments(conf_out);
|
||||
let mut file = File::create(name)?;
|
||||
file.write_all(conf_out.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Crate wrapping up the Grin binary and configuration file
|
||||
|
||||
#![deny(non_upper_case_globals)]
|
||||
#![deny(non_camel_case_types)]
|
||||
#![deny(non_snake_case)]
|
||||
#![deny(unused_mut)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
use grin_core as core;
|
||||
use grin_util as util;
|
||||
|
||||
mod comments;
|
||||
pub mod config;
|
||||
pub mod types;
|
||||
|
||||
pub use crate::config::{initial_setup_wallet, GRIN_WALLET_DIR};
|
||||
pub use crate::types::{ConfigError, GlobalWalletConfig};
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Public types for config modules
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::util::LoggingConfig;
|
||||
use crate::wallet::WalletConfig;
|
||||
|
||||
/// Error type wrapping config errors.
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
/// Error with parsing of config file
|
||||
ParseError(String, String),
|
||||
|
||||
/// Error with fileIO while reading config file
|
||||
FileIOError(String, String),
|
||||
|
||||
/// No file found
|
||||
FileNotFoundError(String),
|
||||
|
||||
/// Error serializing config values
|
||||
SerializationError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfigError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
ConfigError::ParseError(ref file_name, ref message) => write!(
|
||||
f,
|
||||
"Error parsing configuration file at {} - {}",
|
||||
file_name, message
|
||||
),
|
||||
ConfigError::FileIOError(ref file_name, ref message) => {
|
||||
write!(f, "{} {}", message, file_name)
|
||||
}
|
||||
ConfigError::FileNotFoundError(ref file_name) => {
|
||||
write!(f, "Configuration file not found: {}", file_name)
|
||||
}
|
||||
ConfigError::SerializationError(ref message) => {
|
||||
write!(f, "Error serializing configuration: {}", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for ConfigError {
|
||||
fn from(error: io::Error) -> ConfigError {
|
||||
ConfigError::FileIOError(
|
||||
String::from(""),
|
||||
String::from(format!("Error loading config file: {}", error)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet should be split into a separate configuration file
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GlobalWalletConfig {
|
||||
/// Keep track of the file we've read
|
||||
pub config_file_path: Option<PathBuf>,
|
||||
/// Wallet members
|
||||
pub members: Option<GlobalWalletConfigMembers>,
|
||||
}
|
||||
|
||||
/// Wallet internal members
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GlobalWalletConfigMembers {
|
||||
/// Wallet configuration
|
||||
#[serde(default)]
|
||||
pub wallet: WalletConfig,
|
||||
/// Logging config
|
||||
pub logging: Option<LoggingConfig>,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "grin_libwallet"
|
||||
version = "1.1.0"
|
||||
authors = ["Grin Developers <mimblewimble@lists.launchpad.net>"]
|
||||
description = "Simple, private and scalable cryptocurrency implementation based on the MimbleWimble chain format."
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/mimblewimble/grin"
|
||||
keywords = [ "crypto", "grin", "mimblewimble" ]
|
||||
readme = "README.md"
|
||||
exclude = ["**/*.grin", "**/*.grin2"]
|
||||
#build = "src/build/build.rs"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
blake2-rfc = "0.2"
|
||||
failure = "0.1"
|
||||
failure_derive = "0.1"
|
||||
rand = "0.5"
|
||||
serde = "1"
|
||||
serde_derive = "1"
|
||||
serde_json = "1"
|
||||
log = "0.4"
|
||||
uuid = { version = "0.6", features = ["serde", "v4"] }
|
||||
chrono = { version = "0.4.4", features = ["serde"] }
|
||||
|
||||
grin_core = { path = "../../grin/core", version = "1.1.0" }
|
||||
grin_keychain = { path = "../../grin/keychain", version = "1.1.0" }
|
||||
grin_util = { path = "../../grin/util", version = "1.1.0" }
|
||||
@@ -0,0 +1,968 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Main interface into all wallet API functions.
|
||||
//! Wallet APIs are split into two seperate blocks of functionality
|
||||
//! called the 'Owner' and 'Foreign' APIs:
|
||||
//! * The 'Owner' API is intended to expose methods that are to be
|
||||
//! used by the wallet owner only. It is vital that this API is not
|
||||
//! exposed to anyone other than the owner of the wallet (i.e. the
|
||||
//! person with access to the seed and password.
|
||||
//! * The 'Foreign' API contains methods that other wallets will
|
||||
//! use to interact with the owner's wallet. This API can be exposed
|
||||
//! to the outside world, with the consideration as to how that can
|
||||
//! be done securely up to the implementor.
|
||||
//!
|
||||
//! Methods in both APIs are intended to be 'single use', that is to say each
|
||||
//! method will 'open' the wallet (load the keychain with its master seed), perform
|
||||
//! its operation, then 'close' the wallet (unloading references to the keychain and master
|
||||
//! seed).
|
||||
|
||||
use crate::util::Mutex;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::core::hash::Hashed;
|
||||
use crate::core::core::Transaction;
|
||||
use crate::core::ser;
|
||||
use crate::keychain::{Identifier, Keychain};
|
||||
use crate::internal::{keys, tx, updater};
|
||||
use crate::slate::Slate;
|
||||
use crate::types::{
|
||||
AcctPathMapping, BlockFees, CbData, NodeClient, OutputData, OutputLockFn, TxLogEntry,
|
||||
TxLogEntryType, TxWrapper, WalletBackend, WalletInfo,
|
||||
};
|
||||
use crate::{Error, ErrorKind};
|
||||
use crate::util;
|
||||
use crate::util::secp::{pedersen, ContextFlag, Secp256k1};
|
||||
|
||||
const USER_MESSAGE_MAX_LEN: usize = 256;
|
||||
|
||||
/// Functions intended for use by the owner (e.g. master seed holder) of the wallet.
|
||||
pub struct APIOwner<W: ?Sized, C, K>
|
||||
where
|
||||
W: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// A reference-counted mutex to an implementation of the
|
||||
/// [`WalletBackend`](../types/trait.WalletBackend.html) trait.
|
||||
pub wallet: Arc<Mutex<W>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<W: ?Sized, C, K> APIOwner<W, C, K>
|
||||
where
|
||||
W: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Create a new API instance with the given wallet instance. All subsequent
|
||||
/// API calls will operate on this instance of the wallet.
|
||||
///
|
||||
/// Each method will call the [`WalletBackend`](../types/trait.WalletBackend.html)'s
|
||||
/// [`open_with_credentials`](../types/trait.WalletBackend.html#tymethod.open_with_credentials)
|
||||
/// (initialising a keychain with the master seed,) perform its operation, then close the keychain
|
||||
/// with a call to [`close`](../types/trait.WalletBackend.html#tymethod.close)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `wallet_in` - A reference-counted mutex containing an implementation of the
|
||||
/// [`WalletBackend`](../types/trait.WalletBackend.html) trait.
|
||||
///
|
||||
/// # Returns
|
||||
/// * An instance of the OwnerAPI holding a reference to the provided wallet
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
///
|
||||
/// use std::sync::Arc;
|
||||
/// use util::Mutex;
|
||||
///
|
||||
/// use keychain::ExtKeychain;
|
||||
/// use wallet::libwallet::api::APIOwner;
|
||||
///
|
||||
/// // These contain sample implementations of each part needed for a wallet
|
||||
/// use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
///
|
||||
/// let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
///
|
||||
/// // A NodeClient must first be created to handle communication between
|
||||
/// // the wallet and the node.
|
||||
///
|
||||
/// let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// Arc::new(Mutex::new(
|
||||
/// LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
/// // .. perform wallet operations
|
||||
///
|
||||
/// ```
|
||||
|
||||
pub fn new(wallet_in: Arc<Mutex<W>>) -> Self {
|
||||
APIOwner {
|
||||
wallet: wallet_in,
|
||||
phantom: PhantomData,
|
||||
phantom_c: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of accounts stored in the wallet (i.e. mappings between
|
||||
/// user-specified labels and BIP32 derivation paths.
|
||||
///
|
||||
/// # Returns
|
||||
/// * Result Containing:
|
||||
/// * A Vector of [`AcctPathMapping`](../types/struct.AcctPathMapping.html) data
|
||||
/// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// * A wallet should always have the path with the label 'default' path defined,
|
||||
/// with path m/0/0
|
||||
/// * This method does not need to use the wallet seed or keychain.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
///
|
||||
/// let result = api_owner.accounts();
|
||||
///
|
||||
/// if let Ok(accts) = result {
|
||||
/// //...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn accounts(&self) -> Result<Vec<AcctPathMapping>, Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
keys::accounts(&mut *w)
|
||||
}
|
||||
|
||||
/// Creates a new 'account', which is a mapping of a user-specified
|
||||
/// label to a BIP32 path
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `label` - A human readable label to which to map the new BIP32 Path
|
||||
///
|
||||
/// # Returns
|
||||
/// * Result Containing:
|
||||
/// * A [Keychain Identifier](#) for the new path
|
||||
/// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// * Wallets should be initialised with the 'default' path mapped to `m/0/0`
|
||||
/// * Each call to this function will increment the first element of the path
|
||||
/// so the first call will create an account at `m/1/0` and the second at
|
||||
/// `m/2/0` etc. . .
|
||||
/// * The account path is used throughout as the parent key for most key-derivation
|
||||
/// operations. See [`set_active_account`](struct.APIOwner.html#method.set_active_account) for
|
||||
/// further details.
|
||||
///
|
||||
/// * This function does not need to use the root wallet seed or keychain.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
///
|
||||
/// let result = api_owner.create_account_path("account1");
|
||||
///
|
||||
/// if let Ok(identifier) = result {
|
||||
/// //...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn create_account_path(&self, label: &str) -> Result<Identifier, Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
keys::new_acct_path(&mut *w, label)
|
||||
}
|
||||
|
||||
/// Sets the wallet's currently active account. This sets the
|
||||
/// BIP32 parent path used for most key-derivation operations.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `label` - The human readable label for the account. Accounts can be retrieved via
|
||||
/// the [`account`](struct.APIOwner.html#method.accounts) method
|
||||
///
|
||||
/// # Returns
|
||||
/// * Result Containing:
|
||||
/// * `Ok(())` if the path was correctly set
|
||||
/// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// * Wallet parent paths are 2 path elements long, e.g. `m/0/0` is the path
|
||||
/// labelled 'default'. Keys derived from this parent path are 3 elements long,
|
||||
/// e.g. the secret keys derived from the `m/0/0` path will be at paths `m/0/0/0`,
|
||||
/// `m/0/0/1` etc...
|
||||
///
|
||||
/// * This function does not need to use the root wallet seed or keychain.
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
///
|
||||
/// let result = api_owner.create_account_path("account1");
|
||||
///
|
||||
/// if let Ok(identifier) = result {
|
||||
/// // set the account active
|
||||
/// let result2 = api_owner.set_active_account("account1");
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn set_active_account(&self, label: &str) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.set_parent_key_id_by_name(label)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a list of outputs from the active account in the wallet.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `include_spent` - If `true`, outputs that have been marked as 'spent'
|
||||
/// in the wallet will be returned. If `false`, spent outputs will omitted
|
||||
/// from the results.
|
||||
/// * `refresh_from_node` - If true, the wallet will attempt to contact
|
||||
/// a node (via the [`NodeClient`](../types/trait.NodeClient.html)
|
||||
/// provided during wallet instantiation). If `false`, the results will
|
||||
/// contain output information that may be out-of-date (from the last time
|
||||
/// the wallet's output set was refreshed against the node).
|
||||
/// * `tx_id` - If `Some(i)`, only return the outputs associated with
|
||||
/// the transaction log entry of id `i`.
|
||||
///
|
||||
/// # Returns
|
||||
/// * (`bool`, `Vec<OutputData, Commitment>`) - A tuple:
|
||||
/// * The first `bool` element indicates whether the data was successfully
|
||||
/// refreshed from the node (note this may be false even if the `refresh_from_node`
|
||||
/// argument was set to `true`.
|
||||
/// * The second element contains the result set, of which each element is
|
||||
/// a mapping between the wallet's internal [OutputData](../types/struct.OutputData.html)
|
||||
/// and the Output commitment as identified in the chain's UTXO set
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
/// let show_spent = false;
|
||||
/// let update_from_node = true;
|
||||
/// let tx_id = None;
|
||||
///
|
||||
/// let result = api_owner.retrieve_outputs(show_spent, update_from_node, tx_id);
|
||||
///
|
||||
/// if let Ok((was_updated, output_mapping)) = result {
|
||||
/// //...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn retrieve_outputs(
|
||||
&self,
|
||||
include_spent: bool,
|
||||
refresh_from_node: bool,
|
||||
tx_id: Option<u32>,
|
||||
) -> Result<(bool, Vec<(OutputData, pedersen::Commitment)>), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
|
||||
let mut validated = false;
|
||||
if refresh_from_node {
|
||||
validated = self.update_outputs(&mut w, false);
|
||||
}
|
||||
|
||||
let res = Ok((
|
||||
validated,
|
||||
updater::retrieve_outputs(&mut *w, include_spent, tx_id, Some(&parent_key_id))?,
|
||||
));
|
||||
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns a list of [Transaction Log Entries](../types/struct.TxLogEntry.html)
|
||||
/// from the active account in the wallet.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `refresh_from_node` - If true, the wallet will attempt to contact
|
||||
/// a node (via the [`NodeClient`](../types/trait.NodeClient.html)
|
||||
/// provided during wallet instantiation). If `false`, the results will
|
||||
/// contain transaction information that may be out-of-date (from the last time
|
||||
/// the wallet's output set was refreshed against the node).
|
||||
/// * `tx_id` - If `Some(i)`, only return the transactions associated with
|
||||
/// the transaction log entry of id `i`.
|
||||
/// * `tx_slate_id` - If `Some(uuid)`, only return transactions associated with
|
||||
/// the given [`Slate`](../../libtx/slate/struct.Slate.html) uuid.
|
||||
///
|
||||
/// # Returns
|
||||
/// * (`bool`, `Vec<[TxLogEntry](../types/struct.TxLogEntry.html)>`) - A tuple:
|
||||
/// * The first `bool` element indicates whether the data was successfully
|
||||
/// refreshed from the node (note this may be false even if the `refresh_from_node`
|
||||
/// argument was set to `true`.
|
||||
/// * The second element contains the set of retrieved
|
||||
/// [TxLogEntries](../types/struct/TxLogEntry.html)
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let api_owner = APIOwner::new(wallet.clone());
|
||||
/// let update_from_node = true;
|
||||
/// let tx_id = None;
|
||||
/// let tx_slate_id = None;
|
||||
///
|
||||
/// // Return all TxLogEntries
|
||||
/// let result = api_owner.retrieve_txs(update_from_node, tx_id, tx_slate_id);
|
||||
///
|
||||
/// if let Ok((was_updated, tx_log_entries)) = result {
|
||||
/// //...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn retrieve_txs(
|
||||
&self,
|
||||
refresh_from_node: bool,
|
||||
tx_id: Option<u32>,
|
||||
tx_slate_id: Option<Uuid>,
|
||||
) -> Result<(bool, Vec<TxLogEntry>), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
|
||||
let mut validated = false;
|
||||
if refresh_from_node {
|
||||
validated = self.update_outputs(&mut w, false);
|
||||
}
|
||||
|
||||
let res = Ok((
|
||||
validated,
|
||||
updater::retrieve_txs(&mut *w, tx_id, tx_slate_id, Some(&parent_key_id), false)?,
|
||||
));
|
||||
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns summary information from the active account in the wallet.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `refresh_from_node` - If true, the wallet will attempt to contact
|
||||
/// a node (via the [`NodeClient`](../types/trait.NodeClient.html)
|
||||
/// provided during wallet instantiation). If `false`, the results will
|
||||
/// contain transaction information that may be out-of-date (from the last time
|
||||
/// the wallet's output set was refreshed against the node).
|
||||
/// * `minimum_confirmations` - The minimum number of confirmations an output
|
||||
/// should have before it's included in the 'amount_currently_spendable' total
|
||||
///
|
||||
/// # Returns
|
||||
/// * (`bool`, [`WalletInfo`](../types/struct.WalletInfo.html)) - A tuple:
|
||||
/// * The first `bool` element indicates whether the data was successfully
|
||||
/// refreshed from the node (note this may be false even if the `refresh_from_node`
|
||||
/// argument was set to `true`.
|
||||
/// * The second element contains the Summary [`WalletInfo`](../types/struct.WalletInfo.html)
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [`new`](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let mut api_owner = APIOwner::new(wallet.clone());
|
||||
/// let update_from_node = true;
|
||||
/// let minimum_confirmations=10;
|
||||
///
|
||||
/// // Return summary info for active account
|
||||
/// let result = api_owner.retrieve_summary_info(update_from_node, minimum_confirmations);
|
||||
///
|
||||
/// if let Ok((was_updated, summary_info)) = result {
|
||||
/// //...
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn retrieve_summary_info(
|
||||
&mut self,
|
||||
refresh_from_node: bool,
|
||||
minimum_confirmations: u64,
|
||||
) -> Result<(bool, WalletInfo), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
|
||||
let mut validated = false;
|
||||
if refresh_from_node {
|
||||
validated = self.update_outputs(&mut w, false);
|
||||
}
|
||||
|
||||
let wallet_info = updater::retrieve_info(&mut *w, &parent_key_id, minimum_confirmations)?;
|
||||
let res = Ok((validated, wallet_info));
|
||||
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Initiates a new transaction as the sender, creating a new
|
||||
/// [`Slate`](../../libtx/slate/struct.Slate.html) object containing
|
||||
/// the sender's inputs, change outputs, and public signature data. This slate can
|
||||
/// then be sent to the recipient to continue the transaction via the
|
||||
/// [Foreign API's `receive_tx`](struct.APIForeign.html#method.receive_tx) method.
|
||||
///
|
||||
/// When a transaction is created, the wallet must also lock inputs (and create unconfirmed
|
||||
/// outputs) corresponding to the transaction created in the slate, so that the wallet doesn't
|
||||
/// attempt to re-spend outputs that are already included in a transaction before the transaction
|
||||
/// is confirmed. This method also returns a function that will perform that locking, and it is
|
||||
/// up to the caller to decide the best time to call the lock function
|
||||
/// (via the [`tx_lock_outputs`](struct.APIOwner.html#method.tx_lock_outputs) method).
|
||||
/// If the exchange method is intended to be synchronous (such as via a direct http call,)
|
||||
/// then the lock call can wait until the response is confirmed. If it is asynchronous, (such
|
||||
/// as via file transfer,) the lock call should happen immediately (before the file is sent
|
||||
/// to the recipient).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `src_acct_name` - The human readable account name from which to draw outputs
|
||||
/// for the transaction, overriding whatever the active account is as set via the
|
||||
/// [`set_active_account`](struct.APIOwner.html#method.set_active_account) method.
|
||||
/// If None, the transaction will use the active account.
|
||||
/// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`)
|
||||
/// * `minimum_confirmations` - The minimum number of confirmations an output
|
||||
/// should have in order to be included in the transaction.
|
||||
/// * `max_outputs` - By default, the wallet selects as many inputs as possible in a
|
||||
/// transaction, to reduce the Output set and the fees. The wallet will attempt to spend
|
||||
/// include up to `max_outputs` in a transaction, however if this is not enough to cover
|
||||
/// the whole amount, the wallet will include more outputs. This parameter should be considered
|
||||
/// a soft limit.
|
||||
/// * `num_change_outputs` - The target number of change outputs to create in the transaction.
|
||||
/// The actual number created will be `num_change_outputs` + whatever remainder is needed.
|
||||
/// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as
|
||||
/// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps
|
||||
/// to reduce the size of the UTXO set and the amount of data stored in the wallet, and
|
||||
/// minimizes fees. This will generally result in many inputs and a large change output(s),
|
||||
/// usually much larger than the amount being sent. If `false`, the transaction will include
|
||||
/// as many outputs as are needed to meet the amount, (and no more) starting with the smallest
|
||||
/// value outputs.
|
||||
/// * `message` - An optional participant message to include alongside the sender's public
|
||||
/// ParticipantData within the slate. This message will include a signature created with the
|
||||
/// sender's private keys, and will be publically verifiable. Note this message is for
|
||||
/// the convenience of the participants during the exchange; it is not included in the final
|
||||
/// transaction sent to the chain. The message will be truncated to 256 characters.
|
||||
/// Validation of this message is optional.
|
||||
///
|
||||
/// # Returns
|
||||
/// * a result containing:
|
||||
/// * ([`Slate`](../../libtx/slate/struct.Slate.html), lock_function) - A tuple:
|
||||
/// * The transaction Slate, which can be forwarded to the recieving party by any means.
|
||||
/// * A lock function, which should be called when the caller deems it appropriate to lock
|
||||
/// the transaction outputs (i.e. there is relative certaintly that the slate will be
|
||||
/// transmitted to the receiving party). Must be called before calling
|
||||
/// [`finalize_tx`](struct.APIOwner.html#method.finalize_tx).
|
||||
/// * or [`libwallet::Error`](../struct.Error.html) if an error is encountered.
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// * This method requires an active connection to a node, and will fail with error if a node
|
||||
/// cannot be contacted to refresh output statuses.
|
||||
/// * This method will store a partially completed transaction in the wallet's transaction log,
|
||||
/// which will be updated on the corresponding call to [`finalize_tx`](struct.APIOwner.html#method.finalize_tx).
|
||||
///
|
||||
/// # Example
|
||||
/// Set up as in [new](struct.APIOwner.html#method.new) method above.
|
||||
/// ```
|
||||
/// # extern crate grin_wallet as wallet;
|
||||
/// # extern crate grin_keychain as keychain;
|
||||
/// # extern crate grin_util as util;
|
||||
/// # use std::sync::Arc;
|
||||
/// # use util::Mutex;
|
||||
/// # use keychain::ExtKeychain;
|
||||
/// # use wallet::libwallet::api::APIOwner;
|
||||
/// # use wallet::{LMDBBackend, HTTPNodeClient, WalletBackend, WalletConfig};
|
||||
/// # let mut wallet_config = WalletConfig::default();
|
||||
/// # wallet_config.data_file_dir = "test_output/doc/wallet1".to_owned();
|
||||
/// # let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
/// # let mut wallet:Arc<Mutex<WalletBackend<HTTPNodeClient, ExtKeychain>>> =
|
||||
/// # Arc::new(Mutex::new(
|
||||
/// # LMDBBackend::new(wallet_config.clone(), "", node_client).unwrap()
|
||||
/// # ));
|
||||
///
|
||||
/// let mut api_owner = APIOwner::new(wallet.clone());
|
||||
/// let amount = 2_000_000_000;
|
||||
///
|
||||
/// // Attempt to create a transaction using the 'default' account
|
||||
/// let result = api_owner.initiate_tx(
|
||||
/// None,
|
||||
/// amount, // amount
|
||||
/// 10, // minimum confirmations
|
||||
/// 500, // max outputs
|
||||
/// 1, // num change outputs
|
||||
/// true, // select all outputs
|
||||
/// Some("Have some Grins. Love, Yeastplume".to_owned()),
|
||||
/// );
|
||||
///
|
||||
/// if let Ok((slate, lock_fn)) = result {
|
||||
/// // Send slate somehow
|
||||
/// // ...
|
||||
/// // Lock our outputs if we're happy the slate was (or is being) sent
|
||||
/// api_owner.tx_lock_outputs(&slate, lock_fn);
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
pub fn initiate_tx(
|
||||
&mut self,
|
||||
src_acct_name: Option<&str>,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
num_change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
message: Option<String>,
|
||||
) -> Result<(Slate, OutputLockFn<W, C, K>), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = match src_acct_name {
|
||||
Some(d) => {
|
||||
let pm = w.get_acct_path(d.to_owned())?;
|
||||
match pm {
|
||||
Some(p) => p.path,
|
||||
None => w.parent_key_id(),
|
||||
}
|
||||
}
|
||||
None => w.parent_key_id(),
|
||||
};
|
||||
|
||||
let message = match message {
|
||||
Some(mut m) => {
|
||||
m.truncate(USER_MESSAGE_MAX_LEN);
|
||||
Some(m)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut slate = tx::new_tx_slate(&mut *w, amount, 2)?;
|
||||
|
||||
let (context, lock_fn) = tx::add_inputs_to_slate(
|
||||
&mut *w,
|
||||
&mut slate,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
num_change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
&parent_key_id,
|
||||
0,
|
||||
message,
|
||||
)?;
|
||||
|
||||
// Save the aggsig context in our DB for when we
|
||||
// recieve the transaction back
|
||||
{
|
||||
let mut batch = w.batch()?;
|
||||
batch.save_private_context(slate.id.as_bytes(), &context)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
w.close()?;
|
||||
Ok((slate, lock_fn))
|
||||
}
|
||||
|
||||
/// Estimates the amount to be locked and fee for the transaction without creating one
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `src_acct_name` - The human readable account name from which to draw outputs
|
||||
/// for the transaction, overriding whatever the active account is as set via the
|
||||
/// [`set_active_account`](struct.APIOwner.html#method.set_active_account) method.
|
||||
/// If None, the transaction will use the active account.
|
||||
/// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`)
|
||||
/// * `minimum_confirmations` - The minimum number of confirmations an output
|
||||
/// should have in order to be included in the transaction.
|
||||
/// * `max_outputs` - By default, the wallet selects as many inputs as possible in a
|
||||
/// transaction, to reduce the Output set and the fees. The wallet will attempt to spend
|
||||
/// include up to `max_outputs` in a transaction, however if this is not enough to cover
|
||||
/// the whole amount, the wallet will include more outputs. This parameter should be considered
|
||||
/// a soft limit.
|
||||
/// * `num_change_outputs` - The target number of change outputs to create in the transaction.
|
||||
/// The actual number created will be `num_change_outputs` + whatever remainder is needed.
|
||||
/// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as
|
||||
/// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps
|
||||
/// to reduce the size of the UTXO set and the amount of data stored in the wallet, and
|
||||
/// minimizes fees. This will generally result in many inputs and a large change output(s),
|
||||
/// usually much larger than the amount being sent. If `false`, the transaction will include
|
||||
/// as many outputs as are needed to meet the amount, (and no more) starting with the smallest
|
||||
/// value outputs.
|
||||
///
|
||||
/// # Returns
|
||||
/// * a result containing:
|
||||
/// * (total, fee) - A tuple:
|
||||
/// * Total amount to be locked.
|
||||
/// * Transaction fee
|
||||
pub fn estimate_initiate_tx(
|
||||
&mut self,
|
||||
src_acct_name: Option<&str>,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
num_change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
) -> Result<
|
||||
(
|
||||
u64, // total
|
||||
u64, // fee
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = match src_acct_name {
|
||||
Some(d) => {
|
||||
let pm = w.get_acct_path(d.to_owned())?;
|
||||
match pm {
|
||||
Some(p) => p.path,
|
||||
None => w.parent_key_id(),
|
||||
}
|
||||
}
|
||||
None => w.parent_key_id(),
|
||||
};
|
||||
tx::estimate_send_tx(
|
||||
&mut *w,
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
num_change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
&parent_key_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Lock outputs associated with a given slate/transaction
|
||||
pub fn tx_lock_outputs(
|
||||
&mut self,
|
||||
slate: &Slate,
|
||||
mut lock_fn: OutputLockFn<W, C, K>,
|
||||
) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
lock_fn(&mut *w, &slate.tx, PhantomData, PhantomData)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sender finalization of the transaction. Takes the file returned by the
|
||||
/// sender as well as the private file generate on the first send step.
|
||||
/// Builds the complete transaction and sends it to a grin node for
|
||||
/// propagation.
|
||||
pub fn finalize_tx(&mut self, slate: &mut Slate) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let context = w.get_private_context(slate.id.as_bytes())?;
|
||||
tx::complete_tx(&mut *w, slate, 0, &context)?;
|
||||
tx::update_stored_tx(&mut *w, slate)?;
|
||||
tx::update_message(&mut *w, slate)?;
|
||||
{
|
||||
let mut batch = w.batch()?;
|
||||
batch.delete_private_context(slate.id.as_bytes())?;
|
||||
batch.commit()?;
|
||||
}
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Roll back a transaction and all associated outputs with a given
|
||||
/// transaction id This means delete all change outputs, (or recipient
|
||||
/// output if you're recipient), and unlock all locked outputs associated
|
||||
/// with the transaction used when a transaction is created but never
|
||||
/// posted
|
||||
pub fn cancel_tx(
|
||||
&mut self,
|
||||
tx_id: Option<u32>,
|
||||
tx_slate_id: Option<Uuid>,
|
||||
) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = w.parent_key_id();
|
||||
if !self.update_outputs(&mut w, false) {
|
||||
return Err(ErrorKind::TransactionCancellationError(
|
||||
"Can't contact running Grin node. Not Cancelling.",
|
||||
))?;
|
||||
}
|
||||
tx::cancel_tx(&mut *w, &parent_key_id, tx_id, tx_slate_id)?;
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a stored transaction from a TxLogEntry
|
||||
pub fn get_stored_tx(&self, entry: &TxLogEntry) -> Result<Option<Transaction>, Error> {
|
||||
let w = self.wallet.lock();
|
||||
w.get_stored_tx(entry)
|
||||
}
|
||||
|
||||
/// Posts a transaction to the chain
|
||||
pub fn post_tx(&self, tx: &Transaction, fluff: bool) -> Result<(), Error> {
|
||||
let tx_hex = util::to_hex(ser::ser_vec(tx).unwrap());
|
||||
let client = {
|
||||
let mut w = self.wallet.lock();
|
||||
w.w2n_client().clone()
|
||||
};
|
||||
let res = client.post_tx(&TxWrapper { tx_hex: tx_hex }, fluff);
|
||||
if let Err(e) = res {
|
||||
error!("api: post_tx: failed with error: {}", e);
|
||||
Err(e)
|
||||
} else {
|
||||
debug!(
|
||||
"api: post_tx: successfully posted tx: {}, fluff? {}",
|
||||
tx.hash(),
|
||||
fluff
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies all messages in the slate match their public keys
|
||||
pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly);
|
||||
slate.verify_messages(&secp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to restore contents of wallet
|
||||
pub fn restore(&mut self) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
w.restore()?;
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to check and fix the contents of the wallet
|
||||
pub fn check_repair(&mut self) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
self.update_outputs(&mut w, true);
|
||||
w.check_repair()?;
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve current height from node
|
||||
pub fn node_height(&mut self) -> Result<(u64, bool), Error> {
|
||||
let res = {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
w.w2n_client().get_chain_height()
|
||||
};
|
||||
match res {
|
||||
Ok(height) => Ok((height, true)),
|
||||
Err(_) => {
|
||||
let outputs = self.retrieve_outputs(true, false, None)?;
|
||||
let height = match outputs.1.iter().map(|(out, _)| out.height).max() {
|
||||
Some(height) => height,
|
||||
None => 0,
|
||||
};
|
||||
Ok((height, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to update outputs in wallet, return whether it was successful
|
||||
fn update_outputs(&self, w: &mut W, update_all: bool) -> bool {
|
||||
let parent_key_id = w.parent_key_id();
|
||||
match updater::refresh_outputs(&mut *w, &parent_key_id, update_all) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around external API functions, intended to communicate
|
||||
/// with other parties
|
||||
pub struct APIForeign<W: ?Sized, C, K>
|
||||
where
|
||||
W: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Wallet, contains its keychain (TODO: Split these up into 2 traits
|
||||
/// perhaps)
|
||||
pub wallet: Arc<Mutex<W>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, W: ?Sized, C, K> APIForeign<W, C, K>
|
||||
where
|
||||
W: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Create new API instance
|
||||
pub fn new(wallet_in: Arc<Mutex<W>>) -> Box<Self> {
|
||||
Box::new(APIForeign {
|
||||
wallet: wallet_in,
|
||||
phantom: PhantomData,
|
||||
phantom_c: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a new (potential) coinbase transaction in the wallet
|
||||
pub fn build_coinbase(&mut self, block_fees: &BlockFees) -> Result<CbData, Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let res = updater::build_coinbase(&mut *w, block_fees);
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Verifies all messages in the slate match their public keys
|
||||
pub fn verify_slate_messages(&mut self, slate: &Slate) -> Result<(), Error> {
|
||||
let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly);
|
||||
slate.verify_messages(&secp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive a transaction from a sender
|
||||
pub fn receive_tx(
|
||||
&mut self,
|
||||
slate: &mut Slate,
|
||||
dest_acct_name: Option<&str>,
|
||||
message: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let mut w = self.wallet.lock();
|
||||
w.open_with_credentials()?;
|
||||
let parent_key_id = match dest_acct_name {
|
||||
Some(d) => {
|
||||
let pm = w.get_acct_path(d.to_owned())?;
|
||||
match pm {
|
||||
Some(p) => p.path,
|
||||
None => w.parent_key_id(),
|
||||
}
|
||||
}
|
||||
None => w.parent_key_id(),
|
||||
};
|
||||
// Don't do this multiple times
|
||||
let tx = updater::retrieve_txs(&mut *w, None, Some(slate.id), Some(&parent_key_id), false)?;
|
||||
for t in &tx {
|
||||
if t.tx_type == TxLogEntryType::TxReceived {
|
||||
return Err(ErrorKind::TransactionAlreadyReceived(slate.id.to_string()).into());
|
||||
}
|
||||
}
|
||||
|
||||
let message = match message {
|
||||
Some(mut m) => {
|
||||
m.truncate(USER_MESSAGE_MAX_LEN);
|
||||
Some(m)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let (_, mut create_fn) =
|
||||
tx::add_output_to_slate(&mut *w, slate, &parent_key_id, 1, message)?;
|
||||
create_fn(&mut *w, &slate.tx, PhantomData, PhantomData)?;
|
||||
tx::update_message(&mut *w, slate)?;
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Error types for libwallet
|
||||
|
||||
use crate::core::core::{committed, transaction};
|
||||
use crate::core::libtx;
|
||||
use crate::keychain;
|
||||
use crate::util::secp;
|
||||
use failure::{Backtrace, Context, Fail};
|
||||
use std::env;
|
||||
use std::fmt::{self, Display};
|
||||
use std::io;
|
||||
|
||||
/// Error definition
|
||||
#[derive(Debug, Fail)]
|
||||
pub struct Error {
|
||||
inner: Context<ErrorKind>,
|
||||
}
|
||||
|
||||
/// Wallet errors, mostly wrappers around underlying crypto or I/O errors.
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
|
||||
pub enum ErrorKind {
|
||||
/// Not enough funds
|
||||
#[fail(
|
||||
display = "Not enough funds. Required: {}, Available: {}",
|
||||
needed_disp, available_disp
|
||||
)]
|
||||
NotEnoughFunds {
|
||||
/// available funds
|
||||
available: u64,
|
||||
/// Display friendly
|
||||
available_disp: String,
|
||||
/// Needed funds
|
||||
needed: u64,
|
||||
/// Display friendly
|
||||
needed_disp: String,
|
||||
},
|
||||
|
||||
/// Fee error
|
||||
#[fail(display = "Fee Error: {}", _0)]
|
||||
Fee(String),
|
||||
|
||||
/// LibTX Error
|
||||
#[fail(display = "LibTx Error")]
|
||||
LibTX(libtx::ErrorKind),
|
||||
|
||||
/// Keychain error
|
||||
#[fail(display = "Keychain error")]
|
||||
Keychain(keychain::Error),
|
||||
|
||||
/// Transaction Error
|
||||
#[fail(display = "Transaction error")]
|
||||
Transaction(transaction::Error),
|
||||
|
||||
/// API Error
|
||||
#[fail(display = "Client Callback Error: {}", _0)]
|
||||
ClientCallback(String),
|
||||
|
||||
/// Secp Error
|
||||
#[fail(display = "Secp error")]
|
||||
Secp(secp::Error),
|
||||
|
||||
/// Callback implementation error conversion
|
||||
#[fail(display = "Trait Implementation error")]
|
||||
CallbackImpl(&'static str),
|
||||
|
||||
/// Wallet backend error
|
||||
#[fail(display = "Wallet store error: {}", _0)]
|
||||
Backend(String),
|
||||
|
||||
/// Callback implementation error conversion
|
||||
#[fail(display = "Restore Error")]
|
||||
Restore,
|
||||
|
||||
/// An error in the format of the JSON structures exchanged by the wallet
|
||||
#[fail(display = "JSON format error: {}", _0)]
|
||||
Format(String),
|
||||
|
||||
/// Other serialization errors
|
||||
#[fail(display = "Ser/Deserialization error")]
|
||||
Deser(crate::core::ser::Error),
|
||||
|
||||
/// IO Error
|
||||
#[fail(display = "I/O error")]
|
||||
IO,
|
||||
|
||||
/// Error when contacting a node through its API
|
||||
#[fail(display = "Node API error")]
|
||||
Node,
|
||||
|
||||
/// Error contacting wallet API
|
||||
#[fail(display = "Wallet Communication Error: {}", _0)]
|
||||
WalletComms(String),
|
||||
|
||||
/// Error originating from hyper.
|
||||
#[fail(display = "Hyper error")]
|
||||
Hyper,
|
||||
|
||||
/// Error originating from hyper uri parsing.
|
||||
#[fail(display = "Uri parsing error")]
|
||||
Uri,
|
||||
|
||||
/// Signature error
|
||||
#[fail(display = "Signature error: {}", _0)]
|
||||
Signature(String),
|
||||
|
||||
/// Attempt to use duplicate transaction id in separate transactions
|
||||
#[fail(display = "Duplicate transaction ID error")]
|
||||
DuplicateTransactionId,
|
||||
|
||||
/// Wallet seed already exists
|
||||
#[fail(display = "Wallet seed exists error")]
|
||||
WalletSeedExists,
|
||||
|
||||
/// Wallet seed doesn't exist
|
||||
#[fail(display = "Wallet seed doesn't exist error")]
|
||||
WalletSeedDoesntExist,
|
||||
|
||||
/// Wallet seed doesn't exist
|
||||
#[fail(display = "Wallet seed decryption error")]
|
||||
WalletSeedDecryption,
|
||||
|
||||
/// Transaction doesn't exist
|
||||
#[fail(display = "Transaction {} doesn't exist", _0)]
|
||||
TransactionDoesntExist(String),
|
||||
|
||||
/// Transaction already rolled back
|
||||
#[fail(display = "Transaction {} cannot be cancelled", _0)]
|
||||
TransactionNotCancellable(String),
|
||||
|
||||
/// Cancellation error
|
||||
#[fail(display = "Cancellation Error: {}", _0)]
|
||||
TransactionCancellationError(&'static str),
|
||||
|
||||
/// Cancellation error
|
||||
#[fail(display = "Tx dump Error: {}", _0)]
|
||||
TransactionDumpError(&'static str),
|
||||
|
||||
/// Attempt to repost a transaction that's already confirmed
|
||||
#[fail(display = "Transaction already confirmed error")]
|
||||
TransactionAlreadyConfirmed,
|
||||
|
||||
/// Transaction has already been received
|
||||
#[fail(display = "Transaction {} has already been received", _0)]
|
||||
TransactionAlreadyReceived(String),
|
||||
|
||||
/// Attempt to repost a transaction that's not completed and stored
|
||||
#[fail(display = "Transaction building not completed: {}", _0)]
|
||||
TransactionBuildingNotCompleted(u32),
|
||||
|
||||
/// Invalid BIP-32 Depth
|
||||
#[fail(display = "Invalid BIP32 Depth (must be 1 or greater)")]
|
||||
InvalidBIP32Depth,
|
||||
|
||||
/// Attempt to add an account that exists
|
||||
#[fail(display = "Account Label '{}' already exists", _0)]
|
||||
AccountLabelAlreadyExists(String),
|
||||
|
||||
/// Reference unknown account label
|
||||
#[fail(display = "Unknown Account Label '{}'", _0)]
|
||||
UnknownAccountLabel(String),
|
||||
|
||||
/// Error from summing commitments via committed trait.
|
||||
#[fail(display = "Committed Error")]
|
||||
Committed(committed::Error),
|
||||
|
||||
/// Other
|
||||
#[fail(display = "Generic error: {}", _0)]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let show_bt = match env::var("RUST_BACKTRACE") {
|
||||
Ok(r) => {
|
||||
if r == "1" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
let backtrace = match self.backtrace() {
|
||||
Some(b) => format!("{}", b),
|
||||
None => String::from("Unknown"),
|
||||
};
|
||||
let inner_output = format!("{}", self.inner,);
|
||||
let backtrace_output = format!("\n Backtrace: {}", backtrace);
|
||||
let mut output = inner_output.clone();
|
||||
if show_bt {
|
||||
output.push_str(&backtrace_output);
|
||||
}
|
||||
Display::fmt(&output, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// get kind
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
self.inner.get_context().clone()
|
||||
}
|
||||
/// get cause string
|
||||
pub fn cause_string(&self) -> String {
|
||||
match self.cause() {
|
||||
Some(k) => format!("{}", k),
|
||||
None => format!("Unknown"),
|
||||
}
|
||||
}
|
||||
/// get cause
|
||||
pub fn cause(&self) -> Option<&dyn Fail> {
|
||||
self.inner.cause()
|
||||
}
|
||||
/// get backtrace
|
||||
pub fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.inner.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Error {
|
||||
Error {
|
||||
inner: Context::new(kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(inner: Context<ErrorKind>) -> Error {
|
||||
Error { inner: inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(_error: io::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::IO),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<keychain::Error> for Error {
|
||||
fn from(error: keychain::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Keychain(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libtx::Error> for Error {
|
||||
fn from(error: crate::core::libtx::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::LibTX(error.kind())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<transaction::Error> for Error {
|
||||
fn from(error: transaction::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Transaction(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::core::ser::Error> for Error {
|
||||
fn from(error: crate::core::ser::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Deser(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<secp::Error> for Error {
|
||||
fn from(error: secp::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Secp(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<committed::Error> for Error {
|
||||
fn from(error: committed::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Committed(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! lower-level wallet functions which build upon core::libtx to perform wallet
|
||||
//! operations
|
||||
|
||||
#![deny(non_upper_case_globals)]
|
||||
#![deny(non_camel_case_types)]
|
||||
#![deny(non_snake_case)]
|
||||
#![deny(unused_mut)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod keys;
|
||||
pub mod restore;
|
||||
pub mod selection;
|
||||
pub mod tx;
|
||||
pub mod updater;
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Wallet key management functions
|
||||
use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain};
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::types::{AcctPathMapping, NodeClient, WalletBackend};
|
||||
|
||||
/// Get next available key in the wallet for a given parent
|
||||
pub fn next_available_key<T: ?Sized, C, K>(wallet: &mut T) -> Result<Identifier, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let child = wallet.next_child()?;
|
||||
Ok(child)
|
||||
}
|
||||
|
||||
/// Retrieve an existing key from a wallet
|
||||
pub fn retrieve_existing_key<T: ?Sized, C, K>(
|
||||
wallet: &T,
|
||||
key_id: Identifier,
|
||||
mmr_index: Option<u64>,
|
||||
) -> Result<(Identifier, u32), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let existing = wallet.get(&key_id, &mmr_index)?;
|
||||
let key_id = existing.key_id.clone();
|
||||
let derivation = existing.n_child;
|
||||
Ok((key_id, derivation))
|
||||
}
|
||||
|
||||
/// Returns a list of account to BIP32 path mappings
|
||||
pub fn accounts<T: ?Sized, C, K>(wallet: &mut T) -> Result<Vec<AcctPathMapping>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
Ok(wallet.acct_path_iter().collect())
|
||||
}
|
||||
|
||||
/// Adds an new parent account path with a given label
|
||||
pub fn new_acct_path<T: ?Sized, C, K>(wallet: &mut T, label: &str) -> Result<Identifier, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let label = label.to_owned();
|
||||
if let Some(_) = wallet.acct_path_iter().find(|l| l.label == label) {
|
||||
return Err(ErrorKind::AccountLabelAlreadyExists(label.clone()).into());
|
||||
}
|
||||
|
||||
// We're always using paths at m/k/0 for parent keys for output derivations
|
||||
// so find the highest of those, then increment (to conform with external/internal
|
||||
// derivation chains in BIP32 spec)
|
||||
|
||||
let highest_entry = wallet.acct_path_iter().max_by(|a, b| {
|
||||
<u32>::from(a.path.to_path().path[0]).cmp(&<u32>::from(b.path.to_path().path[0]))
|
||||
});
|
||||
|
||||
let return_id = {
|
||||
if let Some(e) = highest_entry {
|
||||
let mut p = e.path.to_path();
|
||||
p.path[0] = ChildNumber::from(<u32>::from(p.path[0]) + 1);
|
||||
p.to_identifier()
|
||||
} else {
|
||||
ExtKeychain::derive_key_id(2, 0, 0, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
let save_path = AcctPathMapping {
|
||||
label: label.to_owned(),
|
||||
path: return_id.clone(),
|
||||
};
|
||||
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save_acct_path(save_path)?;
|
||||
batch.commit()?;
|
||||
Ok(return_id)
|
||||
}
|
||||
|
||||
/// Adds/sets a particular account path with a given label
|
||||
pub fn set_acct_path<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
label: &str,
|
||||
path: &Identifier,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let label = label.to_owned();
|
||||
let save_path = AcctPathMapping {
|
||||
label: label.to_owned(),
|
||||
path: path.clone(),
|
||||
};
|
||||
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save_acct_path(save_path)?;
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//! Functions to restore a wallet's outputs from just the master seed
|
||||
|
||||
use crate::core::global;
|
||||
use crate::core::libtx::proof;
|
||||
use crate::keychain::{ExtKeychain, Identifier, Keychain};
|
||||
use crate::internal::{keys, updater};
|
||||
use crate::types::*;
|
||||
use crate::Error;
|
||||
use crate::util::secp::{key::SecretKey, pedersen};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Utility struct for return values from below
|
||||
#[derive(Clone)]
|
||||
struct OutputResult {
|
||||
///
|
||||
pub commit: pedersen::Commitment,
|
||||
///
|
||||
pub key_id: Identifier,
|
||||
///
|
||||
pub n_child: u32,
|
||||
///
|
||||
pub mmr_index: u64,
|
||||
///
|
||||
pub value: u64,
|
||||
///
|
||||
pub height: u64,
|
||||
///
|
||||
pub lock_height: u64,
|
||||
///
|
||||
pub is_coinbase: bool,
|
||||
///
|
||||
pub blinding: SecretKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Collect stats in case we want to just output a single tx log entry
|
||||
/// for restored non-coinbase outputs
|
||||
struct RestoredTxStats {
|
||||
///
|
||||
pub log_id: u32,
|
||||
///
|
||||
pub amount_credited: u64,
|
||||
///
|
||||
pub num_outputs: usize,
|
||||
}
|
||||
|
||||
fn identify_utxo_outputs<T, C, K>(
|
||||
wallet: &mut T,
|
||||
outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>,
|
||||
) -> Result<Vec<OutputResult>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut wallet_outputs: Vec<OutputResult> = Vec::new();
|
||||
|
||||
warn!(
|
||||
"Scanning {} outputs in the current Grin utxo set",
|
||||
outputs.len(),
|
||||
);
|
||||
|
||||
for output in outputs.iter() {
|
||||
let (commit, proof, is_coinbase, height, mmr_index) = output;
|
||||
// attempt to unwind message from the RP and get a value
|
||||
// will fail if it's not ours
|
||||
let info = proof::rewind(wallet.keychain(), *commit, None, *proof)?;
|
||||
|
||||
if !info.success {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lock_height = if *is_coinbase {
|
||||
*height + global::coinbase_maturity()
|
||||
} else {
|
||||
*height
|
||||
};
|
||||
|
||||
// TODO: Output paths are always going to be length 3 for now, but easy enough to grind
|
||||
// through to find the right path if required later
|
||||
let key_id = Identifier::from_serialized_path(3u8, &info.message.as_bytes());
|
||||
|
||||
info!(
|
||||
"Output found: {:?}, amount: {:?}, key_id: {:?}, mmr_index: {},",
|
||||
commit, info.value, key_id, mmr_index,
|
||||
);
|
||||
|
||||
wallet_outputs.push(OutputResult {
|
||||
commit: *commit,
|
||||
key_id: key_id.clone(),
|
||||
n_child: key_id.to_path().last_path_index(),
|
||||
value: info.value,
|
||||
height: *height,
|
||||
lock_height: lock_height,
|
||||
is_coinbase: *is_coinbase,
|
||||
blinding: info.blinding,
|
||||
mmr_index: *mmr_index,
|
||||
});
|
||||
}
|
||||
Ok(wallet_outputs)
|
||||
}
|
||||
|
||||
fn collect_chain_outputs<T, C, K>(wallet: &mut T) -> Result<Vec<OutputResult>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let batch_size = 1000;
|
||||
let mut start_index = 1;
|
||||
let mut result_vec: Vec<OutputResult> = vec![];
|
||||
loop {
|
||||
let (highest_index, last_retrieved_index, outputs) = wallet
|
||||
.w2n_client()
|
||||
.get_outputs_by_pmmr_index(start_index, batch_size)?;
|
||||
warn!(
|
||||
"Checking {} outputs, up to index {}. (Highest index: {})",
|
||||
outputs.len(),
|
||||
highest_index,
|
||||
last_retrieved_index,
|
||||
);
|
||||
|
||||
result_vec.append(&mut identify_utxo_outputs(wallet, outputs.clone())?);
|
||||
|
||||
if highest_index == last_retrieved_index {
|
||||
break;
|
||||
}
|
||||
start_index = last_retrieved_index + 1;
|
||||
}
|
||||
Ok(result_vec)
|
||||
}
|
||||
|
||||
///
|
||||
fn restore_missing_output<T, C, K>(
|
||||
wallet: &mut T,
|
||||
output: OutputResult,
|
||||
found_parents: &mut HashMap<Identifier, u32>,
|
||||
tx_stats: &mut Option<&mut HashMap<Identifier, RestoredTxStats>>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let commit = wallet.calc_commit_for_cache(output.value, &output.key_id)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
|
||||
let parent_key_id = output.key_id.parent_path();
|
||||
if !found_parents.contains_key(&parent_key_id) {
|
||||
found_parents.insert(parent_key_id.clone(), 0);
|
||||
if let Some(ref mut s) = tx_stats {
|
||||
s.insert(
|
||||
parent_key_id.clone(),
|
||||
RestoredTxStats {
|
||||
log_id: batch.next_tx_log_id(&parent_key_id)?,
|
||||
amount_credited: 0,
|
||||
num_outputs: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let log_id = if tx_stats.is_none() || output.is_coinbase {
|
||||
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
||||
let entry_type = match output.is_coinbase {
|
||||
true => TxLogEntryType::ConfirmedCoinbase,
|
||||
false => TxLogEntryType::TxReceived,
|
||||
};
|
||||
let mut t = TxLogEntry::new(parent_key_id.clone(), entry_type, log_id);
|
||||
t.confirmed = true;
|
||||
t.amount_credited = output.value;
|
||||
t.num_outputs = 1;
|
||||
t.update_confirmation_ts();
|
||||
batch.save_tx_log_entry(t, &parent_key_id)?;
|
||||
log_id
|
||||
} else {
|
||||
if let Some(ref mut s) = tx_stats {
|
||||
let ts = s.get(&parent_key_id).unwrap().clone();
|
||||
s.insert(
|
||||
parent_key_id.clone(),
|
||||
RestoredTxStats {
|
||||
log_id: ts.log_id,
|
||||
amount_credited: ts.amount_credited + output.value,
|
||||
num_outputs: ts.num_outputs + 1,
|
||||
},
|
||||
);
|
||||
ts.log_id
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
let _ = batch.save(OutputData {
|
||||
root_key_id: parent_key_id.clone(),
|
||||
key_id: output.key_id,
|
||||
n_child: output.n_child,
|
||||
mmr_index: Some(output.mmr_index),
|
||||
commit: commit,
|
||||
value: output.value,
|
||||
status: OutputStatus::Unspent,
|
||||
height: output.height,
|
||||
lock_height: output.lock_height,
|
||||
is_coinbase: output.is_coinbase,
|
||||
tx_log_entry: Some(log_id),
|
||||
});
|
||||
|
||||
let max_child_index = found_parents.get(&parent_key_id).unwrap().clone();
|
||||
if output.n_child >= max_child_index {
|
||||
found_parents.insert(parent_key_id.clone(), output.n_child);
|
||||
}
|
||||
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
fn cancel_tx_log_entry<T, C, K>(wallet: &mut T, output: &OutputData) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let parent_key_id = output.key_id.parent_path();
|
||||
let updated_tx_entry = if output.tx_log_entry.is_some() {
|
||||
let entries = updater::retrieve_txs(
|
||||
wallet,
|
||||
output.tx_log_entry.clone(),
|
||||
None,
|
||||
Some(&parent_key_id),
|
||||
false,
|
||||
)?;
|
||||
if entries.len() > 0 {
|
||||
let mut entry = entries[0].clone();
|
||||
match entry.tx_type {
|
||||
TxLogEntryType::TxSent => entry.tx_type = TxLogEntryType::TxSentCancelled,
|
||||
TxLogEntryType::TxReceived => entry.tx_type = TxLogEntryType::TxReceivedCancelled,
|
||||
_ => {}
|
||||
}
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut batch = wallet.batch()?;
|
||||
if let Some(t) = updated_tx_entry {
|
||||
batch.save_tx_log_entry(t, &parent_key_id)?;
|
||||
}
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check / repair wallet contents
|
||||
/// assume wallet contents have been freshly updated with contents
|
||||
/// of latest block
|
||||
pub fn check_repair<T, C, K>(wallet: &mut T) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// First, get a definitive list of outputs we own from the chain
|
||||
warn!("Starting wallet check.");
|
||||
let chain_outs = collect_chain_outputs(wallet)?;
|
||||
warn!(
|
||||
"Identified {} wallet_outputs as belonging to this wallet",
|
||||
chain_outs.len(),
|
||||
);
|
||||
|
||||
// Now, get all outputs owned by this wallet (regardless of account)
|
||||
let wallet_outputs = {
|
||||
let res = updater::retrieve_outputs(&mut *wallet, true, None, None)?;
|
||||
res
|
||||
};
|
||||
|
||||
let mut missing_outs = vec![];
|
||||
let mut accidental_spend_outs = vec![];
|
||||
let mut locked_outs = vec![];
|
||||
|
||||
// check all definitive outputs exist in the wallet outputs
|
||||
for deffo in chain_outs.into_iter() {
|
||||
let matched_out = wallet_outputs.iter().find(|wo| wo.1 == deffo.commit);
|
||||
match matched_out {
|
||||
Some(s) => {
|
||||
if s.0.status == OutputStatus::Spent {
|
||||
accidental_spend_outs.push((s.0.clone(), deffo.clone()));
|
||||
}
|
||||
if s.0.status == OutputStatus::Locked {
|
||||
locked_outs.push((s.0.clone(), deffo.clone()));
|
||||
}
|
||||
}
|
||||
None => missing_outs.push(deffo),
|
||||
}
|
||||
}
|
||||
|
||||
// mark problem spent outputs as unspent (confirmed against a short-lived fork, for example)
|
||||
for m in accidental_spend_outs.into_iter() {
|
||||
let mut o = m.0;
|
||||
warn!(
|
||||
"Output for {} with ID {} ({:?}) marked as spent but exists in UTXO set. \
|
||||
Marking unspent and cancelling any associated transaction log entries.",
|
||||
o.value, o.key_id, m.1.commit,
|
||||
);
|
||||
o.status = OutputStatus::Unspent;
|
||||
// any transactions associated with this should be cancelled
|
||||
cancel_tx_log_entry(wallet, &o)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save(o)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
let mut found_parents: HashMap<Identifier, u32> = HashMap::new();
|
||||
|
||||
// Restore missing outputs, adding transaction for it back to the log
|
||||
for m in missing_outs.into_iter() {
|
||||
warn!(
|
||||
"Confirmed output for {} with ID {} ({:?}) exists in UTXO set but not in wallet. \
|
||||
Restoring.",
|
||||
m.value, m.key_id, m.commit,
|
||||
);
|
||||
restore_missing_output(wallet, m, &mut found_parents, &mut None)?;
|
||||
}
|
||||
|
||||
// Unlock locked outputs
|
||||
for m in locked_outs.into_iter() {
|
||||
let mut o = m.0;
|
||||
warn!(
|
||||
"Confirmed output for {} with ID {} ({:?}) exists in UTXO set and is locked. \
|
||||
Unlocking and cancelling associated transaction log entries.",
|
||||
o.value, o.key_id, m.1.commit,
|
||||
);
|
||||
o.status = OutputStatus::Unspent;
|
||||
cancel_tx_log_entry(wallet, &o)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save(o)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
let unconfirmed_outs: Vec<&(OutputData, pedersen::Commitment)> = wallet_outputs
|
||||
.iter()
|
||||
.filter(|o| o.0.status == OutputStatus::Unconfirmed)
|
||||
.collect();
|
||||
// Delete unconfirmed outputs
|
||||
for m in unconfirmed_outs.into_iter() {
|
||||
let o = m.0.clone();
|
||||
warn!(
|
||||
"Unconfirmed output for {} with ID {} ({:?}) not in UTXO set. \
|
||||
Deleting and cancelling associated transaction log entries.",
|
||||
o.value, o.key_id, m.1,
|
||||
);
|
||||
cancel_tx_log_entry(wallet, &o)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.delete(&o.key_id, &o.mmr_index)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
// restore labels, account paths and child derivation indices
|
||||
let label_base = "account";
|
||||
let mut acct_index = 1;
|
||||
for (path, max_child_index) in found_parents.iter() {
|
||||
// default path already exists
|
||||
if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) {
|
||||
let label = format!("{}_{}", label_base, acct_index);
|
||||
keys::set_acct_path(wallet, &label, path)?;
|
||||
acct_index += 1;
|
||||
}
|
||||
let mut batch = wallet.batch()?;
|
||||
debug!("Next child for account {} is {}", path, max_child_index + 1);
|
||||
batch.save_child_index(path, max_child_index + 1)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restore a wallet
|
||||
pub fn restore<T, C, K>(wallet: &mut T) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// Don't proceed if wallet_data has anything in it
|
||||
let is_empty = wallet.iter().next().is_none();
|
||||
if !is_empty {
|
||||
error!("Not restoring. Please back up and remove existing db directory first.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
warn!("Starting restore.");
|
||||
|
||||
let result_vec = collect_chain_outputs(wallet)?;
|
||||
|
||||
warn!(
|
||||
"Identified {} wallet_outputs as belonging to this wallet",
|
||||
result_vec.len(),
|
||||
);
|
||||
|
||||
let mut found_parents: HashMap<Identifier, u32> = HashMap::new();
|
||||
let mut restore_stats = HashMap::new();
|
||||
|
||||
// Now save what we have
|
||||
for output in result_vec {
|
||||
restore_missing_output(
|
||||
wallet,
|
||||
output,
|
||||
&mut found_parents,
|
||||
&mut Some(&mut restore_stats),
|
||||
)?;
|
||||
}
|
||||
|
||||
// restore labels, account paths and child derivation indices
|
||||
let label_base = "account";
|
||||
let mut acct_index = 1;
|
||||
for (path, max_child_index) in found_parents.iter() {
|
||||
// default path already exists
|
||||
if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) {
|
||||
let label = format!("{}_{}", label_base, acct_index);
|
||||
keys::set_acct_path(wallet, &label, path)?;
|
||||
acct_index += 1;
|
||||
}
|
||||
// restore tx log entry for non-coinbase outputs
|
||||
if let Some(s) = restore_stats.get(path) {
|
||||
let mut batch = wallet.batch()?;
|
||||
let mut t = TxLogEntry::new(path.clone(), TxLogEntryType::TxReceived, s.log_id);
|
||||
t.confirmed = true;
|
||||
t.amount_credited = s.amount_credited;
|
||||
t.num_outputs = s.num_outputs;
|
||||
t.update_confirmation_ts();
|
||||
batch.save_tx_log_entry(t, &path)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save_child_index(path, max_child_index + 1)?;
|
||||
debug!("Next child for account {} is {}", path, max_child_index + 1);
|
||||
batch.commit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Selection of inputs for building transactions
|
||||
|
||||
use crate::core::core::{amount_to_hr_string, Transaction};
|
||||
use crate::core::libtx::{build, tx_fee};
|
||||
use crate::keychain::{Identifier, Keychain};
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::internal::keys;
|
||||
use crate::slate::Slate;
|
||||
use crate::types::*;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Initialize a transaction on the sender side, returns a corresponding
|
||||
/// libwallet transaction slate with the appropriate inputs selected,
|
||||
/// and saves the private wallet identifiers of our selected outputs
|
||||
/// into our transaction context
|
||||
|
||||
pub fn build_send_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
slate: &mut Slate,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
parent_key_id: Identifier,
|
||||
) -> Result<(Context, OutputLockFn<T, C, K>), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let (elems, inputs, change_amounts_derivations, fee) = select_send_tx(
|
||||
wallet,
|
||||
slate.amount,
|
||||
slate.height,
|
||||
minimum_confirmations,
|
||||
slate.lock_height,
|
||||
max_outputs,
|
||||
change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
&parent_key_id,
|
||||
)?;
|
||||
|
||||
slate.fee = fee;
|
||||
|
||||
let keychain = wallet.keychain().clone();
|
||||
let blinding = slate.add_transaction_elements(&keychain, elems)?;
|
||||
|
||||
// Create our own private context
|
||||
let mut context = Context::new(
|
||||
wallet.keychain().secp(),
|
||||
blinding.secret_key(&keychain.secp()).unwrap(),
|
||||
);
|
||||
|
||||
// Store our private identifiers for each input
|
||||
for input in inputs {
|
||||
context.add_input(&input.key_id, &input.mmr_index);
|
||||
}
|
||||
|
||||
let mut commits: HashMap<Identifier, Option<String>> = HashMap::new();
|
||||
|
||||
// Store change output(s) and cached commits
|
||||
for (change_amount, id, mmr_index) in &change_amounts_derivations {
|
||||
context.add_output(&id, &mmr_index);
|
||||
commits.insert(
|
||||
id.clone(),
|
||||
wallet.calc_commit_for_cache(*change_amount, &id)?,
|
||||
);
|
||||
}
|
||||
|
||||
let lock_inputs_in = context.get_inputs().clone();
|
||||
let _lock_outputs = context.get_outputs().clone();
|
||||
let messages_in = Some(slate.participant_messages());
|
||||
let slate_id_in = slate.id.clone();
|
||||
let height_in = slate.height;
|
||||
|
||||
// Return a closure to acquire wallet lock and lock the coins being spent
|
||||
// so we avoid accidental double spend attempt.
|
||||
let update_sender_wallet_fn =
|
||||
move |wallet: &mut T, tx: &Transaction, _: PhantomData<C>, _: PhantomData<K>| {
|
||||
let tx_entry = {
|
||||
// These ensure the closure remains FnMut
|
||||
let lock_inputs = lock_inputs_in.clone();
|
||||
let messages = messages_in.clone();
|
||||
let slate_id = slate_id_in.clone();
|
||||
let height = height_in.clone();
|
||||
let mut batch = wallet.batch()?;
|
||||
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
||||
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxSent, log_id);
|
||||
t.tx_slate_id = Some(slate_id.clone());
|
||||
let filename = format!("{}.grintx", slate_id);
|
||||
t.stored_tx = Some(filename);
|
||||
t.fee = Some(fee);
|
||||
let mut amount_debited = 0;
|
||||
t.num_inputs = lock_inputs.len();
|
||||
for id in lock_inputs {
|
||||
let mut coin = batch.get(&id.0, &id.1).unwrap();
|
||||
coin.tx_log_entry = Some(log_id);
|
||||
amount_debited = amount_debited + coin.value;
|
||||
batch.lock_output(&mut coin)?;
|
||||
}
|
||||
|
||||
t.amount_debited = amount_debited;
|
||||
t.messages = messages;
|
||||
|
||||
// write the output representing our change
|
||||
for (change_amount, id, _) in &change_amounts_derivations {
|
||||
t.num_outputs += 1;
|
||||
t.amount_credited += change_amount;
|
||||
let commit = commits.get(&id).unwrap().clone();
|
||||
batch.save(OutputData {
|
||||
root_key_id: parent_key_id.clone(),
|
||||
key_id: id.clone(),
|
||||
n_child: id.to_path().last_path_index(),
|
||||
commit: commit,
|
||||
mmr_index: None,
|
||||
value: change_amount.clone(),
|
||||
status: OutputStatus::Unconfirmed,
|
||||
height: height,
|
||||
lock_height: 0,
|
||||
is_coinbase: false,
|
||||
tx_log_entry: Some(log_id),
|
||||
})?;
|
||||
}
|
||||
batch.save_tx_log_entry(t.clone(), &parent_key_id)?;
|
||||
batch.commit()?;
|
||||
t
|
||||
};
|
||||
wallet.store_tx(&format!("{}", tx_entry.tx_slate_id.unwrap()), tx)?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
Ok((context, Box::new(update_sender_wallet_fn)))
|
||||
}
|
||||
|
||||
/// Creates a new output in the wallet for the recipient,
|
||||
/// returning the key of the fresh output and a closure
|
||||
/// that actually performs the addition of the output to the
|
||||
/// wallet
|
||||
pub fn build_recipient_output<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
slate: &mut Slate,
|
||||
parent_key_id: Identifier,
|
||||
) -> Result<(Identifier, Context, OutputLockFn<T, C, K>), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// Create a potential output for this transaction
|
||||
let key_id = keys::next_available_key(wallet).unwrap();
|
||||
|
||||
let keychain = wallet.keychain().clone();
|
||||
let key_id_inner = key_id.clone();
|
||||
let amount = slate.amount;
|
||||
let height = slate.height;
|
||||
|
||||
let slate_id = slate.id.clone();
|
||||
let blinding =
|
||||
slate.add_transaction_elements(&keychain, vec![build::output(amount, key_id.clone())])?;
|
||||
|
||||
// Add blinding sum to our context
|
||||
let mut context = Context::new(
|
||||
keychain.secp(),
|
||||
blinding
|
||||
.secret_key(wallet.keychain().clone().secp())
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
context.add_output(&key_id, &None);
|
||||
let messages_in = Some(slate.participant_messages());
|
||||
|
||||
// Create closure that adds the output to recipient's wallet
|
||||
// (up to the caller to decide when to do)
|
||||
let wallet_add_fn =
|
||||
move |wallet: &mut T, _tx: &Transaction, _: PhantomData<C>, _: PhantomData<K>| {
|
||||
// Ensure closure remains FnMut
|
||||
let messages = messages_in.clone();
|
||||
let commit = wallet.calc_commit_for_cache(amount, &key_id_inner)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
let log_id = batch.next_tx_log_id(&parent_key_id)?;
|
||||
let mut t = TxLogEntry::new(parent_key_id.clone(), TxLogEntryType::TxReceived, log_id);
|
||||
t.tx_slate_id = Some(slate_id);
|
||||
t.amount_credited = amount;
|
||||
t.num_outputs = 1;
|
||||
t.messages = messages;
|
||||
batch.save(OutputData {
|
||||
root_key_id: parent_key_id.clone(),
|
||||
key_id: key_id_inner.clone(),
|
||||
mmr_index: None,
|
||||
n_child: key_id_inner.to_path().last_path_index(),
|
||||
commit: commit,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
height: height,
|
||||
lock_height: 0,
|
||||
is_coinbase: false,
|
||||
tx_log_entry: Some(log_id),
|
||||
})?;
|
||||
batch.save_tx_log_entry(t, &parent_key_id)?;
|
||||
batch.commit()?;
|
||||
//TODO: Check whether we want to call this
|
||||
//wallet.store_tx(&format!("{}", t.tx_slate_id.unwrap()), tx)?;
|
||||
Ok(())
|
||||
};
|
||||
Ok((key_id, context, Box::new(wallet_add_fn)))
|
||||
}
|
||||
|
||||
/// Builds a transaction to send to someone from the HD seed associated with the
|
||||
/// wallet and the amount to send. Handles reading through the wallet data file,
|
||||
/// selecting outputs to spend and building the change.
|
||||
pub fn select_send_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64,
|
||||
lock_height: u64,
|
||||
max_outputs: usize,
|
||||
change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
parent_key_id: &Identifier,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<Box<build::Append<K>>>,
|
||||
Vec<OutputData>,
|
||||
Vec<(u64, Identifier, Option<u64>)>, // change amounts and derivations
|
||||
u64, // fee
|
||||
),
|
||||
Error,
|
||||
>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let (coins, _total, amount, fee) = select_coins_and_fee(
|
||||
wallet,
|
||||
amount,
|
||||
current_height,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
&parent_key_id,
|
||||
)?;
|
||||
|
||||
// build transaction skeleton with inputs and change
|
||||
let (mut parts, change_amounts_derivations) =
|
||||
inputs_and_change(&coins, wallet, amount, fee, change_outputs)?;
|
||||
|
||||
// This is more proof of concept than anything but here we set lock_height
|
||||
// on tx being sent (based on current chain height via api).
|
||||
parts.push(build::with_lock_height(lock_height));
|
||||
|
||||
Ok((parts, coins, change_amounts_derivations, fee))
|
||||
}
|
||||
|
||||
/// Select outputs and calculating fee.
|
||||
pub fn select_coins_and_fee<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
parent_key_id: &Identifier,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<OutputData>,
|
||||
u64, // total
|
||||
u64, // amount
|
||||
u64, // fee
|
||||
),
|
||||
Error,
|
||||
>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// select some spendable coins from the wallet
|
||||
let (max_outputs, mut coins) = select_coins(
|
||||
wallet,
|
||||
amount,
|
||||
current_height,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
parent_key_id,
|
||||
);
|
||||
|
||||
// sender is responsible for setting the fee on the partial tx
|
||||
// recipient should double check the fee calculation and not blindly trust the
|
||||
// sender
|
||||
|
||||
// TODO - Is it safe to spend without a change output? (1 input -> 1 output)
|
||||
// TODO - Does this not potentially reveal the senders private key?
|
||||
//
|
||||
// First attempt to spend without change
|
||||
let mut fee = tx_fee(coins.len(), 1, 1, None);
|
||||
let mut total: u64 = coins.iter().map(|c| c.value).sum();
|
||||
let mut amount_with_fee = amount + fee;
|
||||
|
||||
if total == 0 {
|
||||
return Err(ErrorKind::NotEnoughFunds {
|
||||
available: 0,
|
||||
available_disp: amount_to_hr_string(0, false),
|
||||
needed: amount_with_fee as u64,
|
||||
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
|
||||
})?;
|
||||
}
|
||||
|
||||
// The amount with fee is more than the total values of our max outputs
|
||||
if total < amount_with_fee && coins.len() == max_outputs {
|
||||
return Err(ErrorKind::NotEnoughFunds {
|
||||
available: total,
|
||||
available_disp: amount_to_hr_string(total, false),
|
||||
needed: amount_with_fee as u64,
|
||||
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
|
||||
})?;
|
||||
}
|
||||
|
||||
let num_outputs = change_outputs + 1;
|
||||
|
||||
// We need to add a change address or amount with fee is more than total
|
||||
if total != amount_with_fee {
|
||||
fee = tx_fee(coins.len(), num_outputs, 1, None);
|
||||
amount_with_fee = amount + fee;
|
||||
|
||||
// Here check if we have enough outputs for the amount including fee otherwise
|
||||
// look for other outputs and check again
|
||||
while total < amount_with_fee {
|
||||
// End the loop if we have selected all the outputs and still not enough funds
|
||||
if coins.len() == max_outputs {
|
||||
return Err(ErrorKind::NotEnoughFunds {
|
||||
available: total as u64,
|
||||
available_disp: amount_to_hr_string(total, false),
|
||||
needed: amount_with_fee as u64,
|
||||
needed_disp: amount_to_hr_string(amount_with_fee as u64, false),
|
||||
})?;
|
||||
}
|
||||
|
||||
// select some spendable coins from the wallet
|
||||
coins = select_coins(
|
||||
wallet,
|
||||
amount_with_fee,
|
||||
current_height,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
parent_key_id,
|
||||
)
|
||||
.1;
|
||||
fee = tx_fee(coins.len(), num_outputs, 1, None);
|
||||
total = coins.iter().map(|c| c.value).sum();
|
||||
amount_with_fee = amount + fee;
|
||||
}
|
||||
}
|
||||
Ok((coins, total, amount, fee))
|
||||
}
|
||||
|
||||
/// Selects inputs and change for a transaction
|
||||
pub fn inputs_and_change<T: ?Sized, C, K>(
|
||||
coins: &Vec<OutputData>,
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
fee: u64,
|
||||
num_change_outputs: usize,
|
||||
) -> Result<
|
||||
(
|
||||
Vec<Box<build::Append<K>>>,
|
||||
Vec<(u64, Identifier, Option<u64>)>,
|
||||
),
|
||||
Error,
|
||||
>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut parts = vec![];
|
||||
|
||||
// calculate the total across all inputs, and how much is left
|
||||
let total: u64 = coins.iter().map(|c| c.value).sum();
|
||||
|
||||
parts.push(build::with_fee(fee));
|
||||
|
||||
// if we are spending 10,000 coins to send 1,000 then our change will be 9,000
|
||||
// if the fee is 80 then the recipient will receive 1000 and our change will be
|
||||
// 8,920
|
||||
let change = total - amount - fee;
|
||||
|
||||
// build inputs using the appropriate derived key_ids
|
||||
for coin in coins {
|
||||
if coin.is_coinbase {
|
||||
parts.push(build::coinbase_input(coin.value, coin.key_id.clone()));
|
||||
} else {
|
||||
parts.push(build::input(coin.value, coin.key_id.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut change_amounts_derivations = vec![];
|
||||
|
||||
if change == 0 {
|
||||
debug!("No change (sending exactly amount + fee), no change outputs to build");
|
||||
} else {
|
||||
debug!(
|
||||
"Building change outputs: total change: {} ({} outputs)",
|
||||
change, num_change_outputs
|
||||
);
|
||||
|
||||
let part_change = change / num_change_outputs as u64;
|
||||
let remainder_change = change % part_change;
|
||||
|
||||
for x in 0..num_change_outputs {
|
||||
// n-1 equal change_outputs and a final one accounting for any remainder
|
||||
let change_amount = if x == (num_change_outputs - 1) {
|
||||
part_change + remainder_change
|
||||
} else {
|
||||
part_change
|
||||
};
|
||||
|
||||
let change_key = wallet.next_child().unwrap();
|
||||
|
||||
change_amounts_derivations.push((change_amount, change_key.clone(), None));
|
||||
parts.push(build::output(change_amount, change_key));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((parts, change_amounts_derivations))
|
||||
}
|
||||
|
||||
/// Select spendable coins from a wallet.
|
||||
/// Default strategy is to spend the maximum number of outputs (up to
|
||||
/// max_outputs). Alternative strategy is to spend smallest outputs first
|
||||
/// but only as many as necessary. When we introduce additional strategies
|
||||
/// we should pass something other than a bool in.
|
||||
/// TODO: Possibly move this into another trait to be owned by a wallet?
|
||||
|
||||
pub fn select_coins<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
current_height: u64,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
select_all: bool,
|
||||
parent_key_id: &Identifier,
|
||||
) -> (usize, Vec<OutputData>)
|
||||
// max_outputs_available, Outputs
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// first find all eligible outputs based on number of confirmations
|
||||
let mut eligible = wallet
|
||||
.iter()
|
||||
.filter(|out| {
|
||||
out.root_key_id == *parent_key_id
|
||||
&& out.eligible_to_spend(current_height, minimum_confirmations)
|
||||
})
|
||||
.collect::<Vec<OutputData>>();
|
||||
|
||||
let max_available = eligible.len();
|
||||
|
||||
// sort eligible outputs by increasing value
|
||||
eligible.sort_by_key(|out| out.value);
|
||||
|
||||
// use a sliding window to identify potential sets of possible outputs to spend
|
||||
// Case of amount > total amount of max_outputs(500):
|
||||
// The limit exists because by default, we always select as many inputs as
|
||||
// possible in a transaction, to reduce both the Output set and the fees.
|
||||
// But that only makes sense up to a point, hence the limit to avoid being too
|
||||
// greedy. But if max_outputs(500) is actually not enough to cover the whole
|
||||
// amount, the wallet should allow going over it to satisfy what the user
|
||||
// wants to send. So the wallet considers max_outputs more of a soft limit.
|
||||
if eligible.len() > max_outputs {
|
||||
for window in eligible.windows(max_outputs) {
|
||||
let windowed_eligibles = window.iter().cloned().collect::<Vec<_>>();
|
||||
if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) {
|
||||
return (max_available, outputs);
|
||||
}
|
||||
}
|
||||
// Not exist in any window of which total amount >= amount.
|
||||
// Then take coins from the smallest one up to the total amount of selected
|
||||
// coins = the amount.
|
||||
if let Some(outputs) = select_from(amount, false, eligible.clone()) {
|
||||
debug!(
|
||||
"Extending maximum number of outputs. {} outputs selected.",
|
||||
outputs.len()
|
||||
);
|
||||
return (max_available, outputs);
|
||||
}
|
||||
} else {
|
||||
if let Some(outputs) = select_from(amount, select_all, eligible.clone()) {
|
||||
return (max_available, outputs);
|
||||
}
|
||||
}
|
||||
|
||||
// we failed to find a suitable set of outputs to spend,
|
||||
// so return the largest amount we can so we can provide guidance on what is
|
||||
// possible
|
||||
eligible.reverse();
|
||||
(
|
||||
max_available,
|
||||
eligible.iter().take(max_outputs).cloned().collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn select_from(amount: u64, select_all: bool, outputs: Vec<OutputData>) -> Option<Vec<OutputData>> {
|
||||
let total = outputs.iter().fold(0, |acc, x| acc + x.value);
|
||||
if total >= amount {
|
||||
if select_all {
|
||||
return Some(outputs.iter().cloned().collect());
|
||||
} else {
|
||||
let mut selected_amount = 0;
|
||||
return Some(
|
||||
outputs
|
||||
.iter()
|
||||
.take_while(|out| {
|
||||
let res = selected_amount < amount;
|
||||
selected_amount += out.value;
|
||||
res
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Transaction building functions
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::keychain::{Identifier, Keychain};
|
||||
use crate::internal::{selection, updater};
|
||||
use crate::slate::Slate;
|
||||
use crate::types::{Context, NodeClient, OutputLockFn, TxLogEntryType, WalletBackend};
|
||||
use crate::{Error, ErrorKind};
|
||||
|
||||
/// Creates a new slate for a transaction, can be called by anyone involved in
|
||||
/// the transaction (sender(s), receiver(s))
|
||||
pub fn new_tx_slate<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
num_participants: usize,
|
||||
) -> Result<Slate, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let current_height = wallet.w2n_client().get_chain_height()?;
|
||||
let mut slate = Slate::blank(num_participants);
|
||||
slate.amount = amount;
|
||||
slate.height = current_height;
|
||||
slate.lock_height = current_height;
|
||||
Ok(slate)
|
||||
}
|
||||
|
||||
/// Estimates locked amount and fee for the transaction without creating one
|
||||
pub fn estimate_send_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
amount: u64,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
num_change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
parent_key_id: &Identifier,
|
||||
) -> Result<
|
||||
(
|
||||
u64, // total
|
||||
u64, // fee
|
||||
),
|
||||
Error,
|
||||
>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// Get lock height
|
||||
let current_height = wallet.w2n_client().get_chain_height()?;
|
||||
// ensure outputs we're selecting are up to date
|
||||
updater::refresh_outputs(wallet, parent_key_id, false)?;
|
||||
|
||||
// Sender selects outputs into a new slate and save our corresponding keys in
|
||||
// a transaction context. The secret key in our transaction context will be
|
||||
// randomly selected. This returns the public slate, and a closure that locks
|
||||
// our inputs and outputs once we're convinced the transaction exchange went
|
||||
// according to plan
|
||||
// This function is just a big helper to do all of that, in theory
|
||||
// this process can be split up in any way
|
||||
let (_coins, total, _amount, fee) = selection::select_coins_and_fee(
|
||||
wallet,
|
||||
amount,
|
||||
current_height,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
num_change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
parent_key_id,
|
||||
)?;
|
||||
Ok((total, fee))
|
||||
}
|
||||
|
||||
/// Add inputs to the slate (effectively becoming the sender)
|
||||
pub fn add_inputs_to_slate<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
slate: &mut Slate,
|
||||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
num_change_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
parent_key_id: &Identifier,
|
||||
participant_id: usize,
|
||||
message: Option<String>,
|
||||
) -> Result<(Context, OutputLockFn<T, C, K>), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// sender should always refresh outputs
|
||||
updater::refresh_outputs(wallet, parent_key_id, false)?;
|
||||
|
||||
// Sender selects outputs into a new slate and save our corresponding keys in
|
||||
// a transaction context. The secret key in our transaction context will be
|
||||
// randomly selected. This returns the public slate, and a closure that locks
|
||||
// our inputs and outputs once we're convinced the transaction exchange went
|
||||
// according to plan
|
||||
// This function is just a big helper to do all of that, in theory
|
||||
// this process can be split up in any way
|
||||
let (mut context, sender_lock_fn) = selection::build_send_tx(
|
||||
wallet,
|
||||
slate,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
num_change_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
parent_key_id.clone(),
|
||||
)?;
|
||||
|
||||
// Generate a kernel offset and subtract from our context's secret key. Store
|
||||
// the offset in the slate's transaction kernel, and adds our public key
|
||||
// information to the slate
|
||||
let _ = slate.fill_round_1(
|
||||
wallet.keychain(),
|
||||
&mut context.sec_key,
|
||||
&context.sec_nonce,
|
||||
participant_id,
|
||||
message,
|
||||
)?;
|
||||
|
||||
Ok((context, sender_lock_fn))
|
||||
}
|
||||
|
||||
/// Add outputs to the slate, becoming the recipient
|
||||
pub fn add_output_to_slate<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
slate: &mut Slate,
|
||||
parent_key_id: &Identifier,
|
||||
participant_id: usize,
|
||||
message: Option<String>,
|
||||
) -> Result<(Context, OutputLockFn<T, C, K>), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// create an output using the amount in the slate
|
||||
let (_, mut context, create_fn) =
|
||||
selection::build_recipient_output(wallet, slate, parent_key_id.clone())?;
|
||||
|
||||
// fill public keys
|
||||
let _ = slate.fill_round_1(
|
||||
wallet.keychain(),
|
||||
&mut context.sec_key,
|
||||
&context.sec_nonce,
|
||||
1,
|
||||
message,
|
||||
)?;
|
||||
|
||||
// perform partial sig
|
||||
let _ = slate.fill_round_2(
|
||||
wallet.keychain(),
|
||||
&context.sec_key,
|
||||
&context.sec_nonce,
|
||||
participant_id,
|
||||
)?;
|
||||
|
||||
Ok((context, create_fn))
|
||||
}
|
||||
|
||||
/// Complete a transaction as the sender
|
||||
pub fn complete_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
slate: &mut Slate,
|
||||
participant_id: usize,
|
||||
context: &Context,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let _ = slate.fill_round_2(
|
||||
wallet.keychain(),
|
||||
&context.sec_key,
|
||||
&context.sec_nonce,
|
||||
participant_id,
|
||||
)?;
|
||||
// Final transaction can be built by anyone at this stage
|
||||
slate.finalize(wallet.keychain())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rollback outputs associated with a transaction in the wallet
|
||||
pub fn cancel_tx<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
parent_key_id: &Identifier,
|
||||
tx_id: Option<u32>,
|
||||
tx_slate_id: Option<Uuid>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut tx_id_string = String::new();
|
||||
if let Some(tx_id) = tx_id {
|
||||
tx_id_string = tx_id.to_string();
|
||||
} else if let Some(tx_slate_id) = tx_slate_id {
|
||||
tx_id_string = tx_slate_id.to_string();
|
||||
}
|
||||
let tx_vec = updater::retrieve_txs(wallet, tx_id, tx_slate_id, Some(&parent_key_id), false)?;
|
||||
if tx_vec.len() != 1 {
|
||||
return Err(ErrorKind::TransactionDoesntExist(tx_id_string))?;
|
||||
}
|
||||
let tx = tx_vec[0].clone();
|
||||
if tx.tx_type != TxLogEntryType::TxSent && tx.tx_type != TxLogEntryType::TxReceived {
|
||||
return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?;
|
||||
}
|
||||
if tx.confirmed == true {
|
||||
return Err(ErrorKind::TransactionNotCancellable(tx_id_string))?;
|
||||
}
|
||||
// get outputs associated with tx
|
||||
let res = updater::retrieve_outputs(wallet, false, Some(tx.id), Some(&parent_key_id))?;
|
||||
let outputs = res.iter().map(|(out, _)| out).cloned().collect();
|
||||
updater::cancel_tx_and_outputs(wallet, tx, outputs, parent_key_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve the associated stored finalised hex Transaction for a given transaction Id
|
||||
/// as well as whether it's been confirmed
|
||||
pub fn retrieve_tx_hex<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
parent_key_id: &Identifier,
|
||||
tx_id: u32,
|
||||
) -> Result<(bool, Option<String>), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let tx_vec = updater::retrieve_txs(wallet, Some(tx_id), None, Some(parent_key_id), false)?;
|
||||
if tx_vec.len() != 1 {
|
||||
return Err(ErrorKind::TransactionDoesntExist(tx_id.to_string()))?;
|
||||
}
|
||||
let tx = tx_vec[0].clone();
|
||||
Ok((tx.confirmed, tx.stored_tx))
|
||||
}
|
||||
|
||||
/// Update the stored transaction (this update needs to happen when the TX is finalised)
|
||||
pub fn update_stored_tx<T: ?Sized, C, K>(wallet: &mut T, slate: &Slate) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// finalize command
|
||||
let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, false)?;
|
||||
let mut tx = None;
|
||||
// don't want to assume this is the right tx, in case of self-sending
|
||||
for t in tx_vec {
|
||||
if t.tx_type == TxLogEntryType::TxSent {
|
||||
tx = Some(t.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let tx = match tx {
|
||||
Some(t) => t,
|
||||
None => return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?,
|
||||
};
|
||||
wallet.store_tx(&format!("{}", tx.tx_slate_id.unwrap()), &slate.tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the transaction participant messages
|
||||
pub fn update_message<T: ?Sized, C, K>(wallet: &mut T, slate: &Slate) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let tx_vec = updater::retrieve_txs(wallet, None, Some(slate.id), None, false)?;
|
||||
if tx_vec.is_empty() {
|
||||
return Err(ErrorKind::TransactionDoesntExist(slate.id.to_string()))?;
|
||||
}
|
||||
let mut batch = wallet.batch()?;
|
||||
for mut tx in tx_vec.into_iter() {
|
||||
tx.messages = Some(slate.participant_messages());
|
||||
let parent_key = tx.parent_key_id.clone();
|
||||
batch.save_tx_log_entry(tx, &parent_key)?;
|
||||
}
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::core::libtx::build;
|
||||
use crate::keychain::{ExtKeychain, ExtKeychainPath, Keychain};
|
||||
|
||||
#[test]
|
||||
// demonstrate that input.commitment == referenced output.commitment
|
||||
// based on the public key and amount begin spent
|
||||
fn output_commitment_equals_input_commitment_on_spend() {
|
||||
let keychain = ExtKeychain::from_random_seed(false).unwrap();
|
||||
let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier();
|
||||
|
||||
let tx1 = build::transaction(vec![build::output(105, key_id1.clone())], &keychain).unwrap();
|
||||
let tx2 = build::transaction(vec![build::input(105, key_id1.clone())], &keychain).unwrap();
|
||||
|
||||
assert_eq!(tx1.outputs()[0].features, tx2.inputs()[0].features);
|
||||
assert_eq!(tx1.outputs()[0].commitment(), tx2.inputs()[0].commitment());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Utilities to check the status of all the outputs we have stored in
|
||||
//! the wallet storage and update them.
|
||||
|
||||
use failure::ResultExt;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::consensus::reward;
|
||||
use crate::core::core::{Output, TxKernel};
|
||||
use crate::core::libtx::reward;
|
||||
use crate::core::{global, ser};
|
||||
use crate::keychain::{Identifier, Keychain};
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::internal::keys;
|
||||
use crate::types::{
|
||||
BlockFees, CbData, NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType,
|
||||
WalletBackend, WalletInfo,
|
||||
};
|
||||
use crate::util;
|
||||
use crate::util::secp::pedersen;
|
||||
|
||||
/// Retrieve all of the outputs (doesn't attempt to update from node)
|
||||
pub fn retrieve_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
show_spent: bool,
|
||||
tx_id: Option<u32>,
|
||||
parent_key_id: Option<&Identifier>,
|
||||
) -> Result<Vec<(OutputData, pedersen::Commitment)>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// just read the wallet here, no need for a write lock
|
||||
let mut outputs = wallet
|
||||
.iter()
|
||||
.filter(|out| show_spent || out.status != OutputStatus::Spent)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// only include outputs with a given tx_id if provided
|
||||
if let Some(id) = tx_id {
|
||||
outputs = outputs
|
||||
.into_iter()
|
||||
.filter(|out| out.tx_log_entry == Some(id))
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
if let Some(k) = parent_key_id {
|
||||
outputs = outputs
|
||||
.iter()
|
||||
.filter(|o| o.root_key_id == *k)
|
||||
.map(|o| o.clone())
|
||||
.collect();
|
||||
}
|
||||
|
||||
outputs.sort_by_key(|out| out.n_child);
|
||||
let keychain = wallet.keychain().clone();
|
||||
|
||||
let res = outputs
|
||||
.into_iter()
|
||||
.map(|out| {
|
||||
let commit = match out.commit.clone() {
|
||||
Some(c) => pedersen::Commitment::from_vec(util::from_hex(c).unwrap()),
|
||||
None => keychain.commit(out.value, &out.key_id).unwrap(),
|
||||
};
|
||||
(out, commit)
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Retrieve all of the transaction entries, or a particular entry
|
||||
/// if `parent_key_id` is set, only return entries from that key
|
||||
pub fn retrieve_txs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
tx_id: Option<u32>,
|
||||
tx_slate_id: Option<Uuid>,
|
||||
parent_key_id: Option<&Identifier>,
|
||||
outstanding_only: bool,
|
||||
) -> Result<Vec<TxLogEntry>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut txs: Vec<TxLogEntry> = wallet
|
||||
.tx_log_iter()
|
||||
.filter(|tx_entry| {
|
||||
let f_pk = match parent_key_id {
|
||||
Some(k) => tx_entry.parent_key_id == *k,
|
||||
None => true,
|
||||
};
|
||||
let f_tx_id = match tx_id {
|
||||
Some(i) => tx_entry.id == i,
|
||||
None => true,
|
||||
};
|
||||
let f_txs = match tx_slate_id {
|
||||
Some(t) => tx_entry.tx_slate_id == Some(t),
|
||||
None => true,
|
||||
};
|
||||
let f_outstanding = match outstanding_only {
|
||||
true => {
|
||||
!tx_entry.confirmed
|
||||
&& (tx_entry.tx_type == TxLogEntryType::TxReceived
|
||||
|| tx_entry.tx_type == TxLogEntryType::TxSent)
|
||||
}
|
||||
false => true,
|
||||
};
|
||||
f_pk && f_tx_id && f_txs && f_outstanding
|
||||
})
|
||||
.collect();
|
||||
txs.sort_by_key(|tx| tx.creation_ts);
|
||||
Ok(txs)
|
||||
}
|
||||
|
||||
/// Refreshes the outputs in a wallet with the latest information
|
||||
/// from a node
|
||||
pub fn refresh_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
parent_key_id: &Identifier,
|
||||
update_all: bool,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let height = wallet.w2n_client().get_chain_height()?;
|
||||
refresh_output_state(wallet, height, parent_key_id, update_all)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// build a local map of wallet outputs keyed by commit
|
||||
/// and a list of outputs we want to query the node for
|
||||
pub fn map_wallet_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
parent_key_id: &Identifier,
|
||||
update_all: bool,
|
||||
) -> Result<HashMap<pedersen::Commitment, (Identifier, Option<u64>)>, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut wallet_outputs: HashMap<pedersen::Commitment, (Identifier, Option<u64>)> =
|
||||
HashMap::new();
|
||||
let keychain = wallet.keychain().clone();
|
||||
let unspents: Vec<OutputData> = wallet
|
||||
.iter()
|
||||
.filter(|x| x.root_key_id == *parent_key_id && x.status != OutputStatus::Spent)
|
||||
.collect();
|
||||
|
||||
let tx_entries = retrieve_txs(wallet, None, None, Some(&parent_key_id), true)?;
|
||||
|
||||
// Only select outputs that are actually involved in an outstanding transaction
|
||||
let unspents: Vec<OutputData> = match update_all {
|
||||
false => unspents
|
||||
.into_iter()
|
||||
.filter(|x| match x.tx_log_entry.as_ref() {
|
||||
Some(t) => {
|
||||
if let Some(_) = tx_entries.iter().find(|&te| te.id == *t) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
None => true,
|
||||
})
|
||||
.collect(),
|
||||
true => unspents,
|
||||
};
|
||||
|
||||
for out in unspents {
|
||||
let commit = match out.commit.clone() {
|
||||
Some(c) => pedersen::Commitment::from_vec(util::from_hex(c).unwrap()),
|
||||
None => keychain.commit(out.value, &out.key_id).unwrap(),
|
||||
};
|
||||
wallet_outputs.insert(commit, (out.key_id.clone(), out.mmr_index));
|
||||
}
|
||||
Ok(wallet_outputs)
|
||||
}
|
||||
|
||||
/// Cancel transaction and associated outputs
|
||||
pub fn cancel_tx_and_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
tx: TxLogEntry,
|
||||
outputs: Vec<OutputData>,
|
||||
parent_key_id: &Identifier,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut batch = wallet.batch()?;
|
||||
|
||||
for mut o in outputs {
|
||||
// unlock locked outputs
|
||||
if o.status == OutputStatus::Unconfirmed {
|
||||
batch.delete(&o.key_id, &o.mmr_index)?;
|
||||
}
|
||||
if o.status == OutputStatus::Locked {
|
||||
o.status = OutputStatus::Unspent;
|
||||
batch.save(o)?;
|
||||
}
|
||||
}
|
||||
let mut tx = tx.clone();
|
||||
if tx.tx_type == TxLogEntryType::TxSent {
|
||||
tx.tx_type = TxLogEntryType::TxSentCancelled;
|
||||
}
|
||||
if tx.tx_type == TxLogEntryType::TxReceived {
|
||||
tx.tx_type = TxLogEntryType::TxReceivedCancelled;
|
||||
}
|
||||
batch.save_tx_log_entry(tx, parent_key_id)?;
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply refreshed API output data to the wallet
|
||||
pub fn apply_api_outputs<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
wallet_outputs: &HashMap<pedersen::Commitment, (Identifier, Option<u64>)>,
|
||||
api_outputs: &HashMap<pedersen::Commitment, (String, u64, u64)>,
|
||||
height: u64,
|
||||
parent_key_id: &Identifier,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
// now for each commit, find the output in the wallet and the corresponding
|
||||
// api output (if it exists) and refresh it in-place in the wallet.
|
||||
// Note: minimizing the time we spend holding the wallet lock.
|
||||
{
|
||||
let last_confirmed_height = wallet.last_confirmed_height()?;
|
||||
// If the server height is less than our confirmed height, don't apply
|
||||
// these changes as the chain is syncing, incorrect or forking
|
||||
if height < last_confirmed_height {
|
||||
warn!(
|
||||
"Not updating outputs as the height of the node's chain \
|
||||
is less than the last reported wallet update height."
|
||||
);
|
||||
warn!("Please wait for sync on node to complete or fork to resolve and try again.");
|
||||
return Ok(());
|
||||
}
|
||||
let mut batch = wallet.batch()?;
|
||||
for (commit, (id, mmr_index)) in wallet_outputs.iter() {
|
||||
if let Ok(mut output) = batch.get(id, mmr_index) {
|
||||
match api_outputs.get(&commit) {
|
||||
Some(o) => {
|
||||
// if this is a coinbase tx being confirmed, it's recordable in tx log
|
||||
if output.is_coinbase && output.status == OutputStatus::Unconfirmed {
|
||||
let log_id = batch.next_tx_log_id(parent_key_id)?;
|
||||
let mut t = TxLogEntry::new(
|
||||
parent_key_id.clone(),
|
||||
TxLogEntryType::ConfirmedCoinbase,
|
||||
log_id,
|
||||
);
|
||||
t.confirmed = true;
|
||||
t.amount_credited = output.value;
|
||||
t.amount_debited = 0;
|
||||
t.num_outputs = 1;
|
||||
t.update_confirmation_ts();
|
||||
output.tx_log_entry = Some(log_id);
|
||||
batch.save_tx_log_entry(t, &parent_key_id)?;
|
||||
}
|
||||
// also mark the transaction in which this output is involved as confirmed
|
||||
// note that one involved input/output confirmation SHOULD be enough
|
||||
// to reliably confirm the tx
|
||||
if !output.is_coinbase && output.status == OutputStatus::Unconfirmed {
|
||||
let tx = batch.tx_log_iter().find(|t| {
|
||||
Some(t.id) == output.tx_log_entry
|
||||
&& t.parent_key_id == *parent_key_id
|
||||
});
|
||||
if let Some(mut t) = tx {
|
||||
t.update_confirmation_ts();
|
||||
t.confirmed = true;
|
||||
batch.save_tx_log_entry(t, &parent_key_id)?;
|
||||
}
|
||||
}
|
||||
output.height = o.1;
|
||||
output.mark_unspent();
|
||||
}
|
||||
None => output.mark_spent(),
|
||||
};
|
||||
batch.save(output)?;
|
||||
}
|
||||
}
|
||||
{
|
||||
batch.save_last_confirmed_height(parent_key_id, height)?;
|
||||
}
|
||||
batch.commit()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a single api query to retrieve the latest output data from the node.
|
||||
/// So we can refresh the local wallet outputs.
|
||||
fn refresh_output_state<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
height: u64,
|
||||
parent_key_id: &Identifier,
|
||||
update_all: bool,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
debug!("Refreshing wallet outputs");
|
||||
|
||||
// build a local map of wallet outputs keyed by commit
|
||||
// and a list of outputs we want to query the node for
|
||||
let wallet_outputs = map_wallet_outputs(wallet, parent_key_id, update_all)?;
|
||||
|
||||
let wallet_output_keys = wallet_outputs.keys().map(|commit| commit.clone()).collect();
|
||||
|
||||
let api_outputs = wallet
|
||||
.w2n_client()
|
||||
.get_outputs_from_node(wallet_output_keys)?;
|
||||
apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height, parent_key_id)?;
|
||||
clean_old_unconfirmed(wallet, height)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clean_old_unconfirmed<T: ?Sized, C, K>(wallet: &mut T, height: u64) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
if height < 50 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut ids_to_del = vec![];
|
||||
for out in wallet.iter() {
|
||||
if out.status == OutputStatus::Unconfirmed
|
||||
&& out.height > 0
|
||||
&& out.height < height - 50
|
||||
&& out.is_coinbase
|
||||
{
|
||||
ids_to_del.push(out.key_id.clone())
|
||||
}
|
||||
}
|
||||
let mut batch = wallet.batch()?;
|
||||
for id in ids_to_del {
|
||||
batch.delete(&id, &None)?;
|
||||
}
|
||||
batch.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve summary info about the wallet
|
||||
/// caller should refresh first if desired
|
||||
pub fn retrieve_info<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
parent_key_id: &Identifier,
|
||||
minimum_confirmations: u64,
|
||||
) -> Result<WalletInfo, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let current_height = wallet.last_confirmed_height()?;
|
||||
let outputs = wallet
|
||||
.iter()
|
||||
.filter(|out| out.root_key_id == *parent_key_id);
|
||||
|
||||
let mut unspent_total = 0;
|
||||
let mut immature_total = 0;
|
||||
let mut unconfirmed_total = 0;
|
||||
let mut locked_total = 0;
|
||||
|
||||
for out in outputs {
|
||||
match out.status {
|
||||
OutputStatus::Unspent => {
|
||||
if out.is_coinbase && out.lock_height > current_height {
|
||||
immature_total += out.value;
|
||||
} else if out.num_confirmations(current_height) < minimum_confirmations {
|
||||
// Treat anything less than minimum confirmations as "unconfirmed".
|
||||
unconfirmed_total += out.value;
|
||||
} else {
|
||||
unspent_total += out.value;
|
||||
}
|
||||
}
|
||||
OutputStatus::Unconfirmed => {
|
||||
// We ignore unconfirmed coinbase outputs completely.
|
||||
if !out.is_coinbase {
|
||||
if minimum_confirmations == 0 {
|
||||
unspent_total += out.value;
|
||||
} else {
|
||||
unconfirmed_total += out.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
OutputStatus::Locked => {
|
||||
locked_total += out.value;
|
||||
}
|
||||
OutputStatus::Spent => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(WalletInfo {
|
||||
last_confirmed_height: current_height,
|
||||
minimum_confirmations,
|
||||
total: unspent_total + unconfirmed_total + immature_total,
|
||||
amount_awaiting_confirmation: unconfirmed_total,
|
||||
amount_immature: immature_total,
|
||||
amount_locked: locked_total,
|
||||
amount_currently_spendable: unspent_total,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a coinbase output and insert into wallet
|
||||
pub fn build_coinbase<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
block_fees: &BlockFees,
|
||||
) -> Result<CbData, Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let (out, kern, block_fees) = receive_coinbase(wallet, block_fees).context(ErrorKind::Node)?;
|
||||
|
||||
let out_bin = ser::ser_vec(&out).context(ErrorKind::Node)?;
|
||||
|
||||
let kern_bin = ser::ser_vec(&kern).context(ErrorKind::Node)?;
|
||||
|
||||
let key_id_bin = match block_fees.key_id {
|
||||
Some(key_id) => ser::ser_vec(&key_id).context(ErrorKind::Node)?,
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
Ok(CbData {
|
||||
output: util::to_hex(out_bin),
|
||||
kernel: util::to_hex(kern_bin),
|
||||
key_id: util::to_hex(key_id_bin),
|
||||
})
|
||||
}
|
||||
|
||||
//TODO: Split up the output creation and the wallet insertion
|
||||
/// Build a coinbase output and the corresponding kernel
|
||||
pub fn receive_coinbase<T: ?Sized, C, K>(
|
||||
wallet: &mut T,
|
||||
block_fees: &BlockFees,
|
||||
) -> Result<(Output, TxKernel, BlockFees), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let height = block_fees.height;
|
||||
let lock_height = height + global::coinbase_maturity();
|
||||
let key_id = block_fees.key_id();
|
||||
let parent_key_id = wallet.parent_key_id();
|
||||
|
||||
let key_id = match key_id {
|
||||
Some(key_id) => match keys::retrieve_existing_key(wallet, key_id, None) {
|
||||
Ok(k) => k.0,
|
||||
Err(_) => keys::next_available_key(wallet)?,
|
||||
},
|
||||
None => keys::next_available_key(wallet)?,
|
||||
};
|
||||
|
||||
{
|
||||
// Now acquire the wallet lock and write the new output.
|
||||
let amount = reward(block_fees.fees);
|
||||
let commit = wallet.calc_commit_for_cache(amount, &key_id)?;
|
||||
let mut batch = wallet.batch()?;
|
||||
batch.save(OutputData {
|
||||
root_key_id: parent_key_id,
|
||||
key_id: key_id.clone(),
|
||||
n_child: key_id.to_path().last_path_index(),
|
||||
mmr_index: None,
|
||||
commit: commit,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
height: height,
|
||||
lock_height: lock_height,
|
||||
is_coinbase: true,
|
||||
tx_log_entry: None,
|
||||
})?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"receive_coinbase: built candidate output - {:?}, {}",
|
||||
key_id.clone(),
|
||||
key_id,
|
||||
);
|
||||
|
||||
let mut block_fees = block_fees.clone();
|
||||
block_fees.key_id = Some(key_id.clone());
|
||||
|
||||
debug!("receive_coinbase: {:?}", block_fees);
|
||||
|
||||
let (out, kern) = reward::output(wallet.keychain(), &key_id, block_fees.fees).unwrap();
|
||||
/* .context(ErrorKind::Keychain)?; */
|
||||
Ok((out, kern, block_fees))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2019 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Higher level wallet functions which can be used by callers to operate
|
||||
//! on the wallet, as well as helpers to invoke and instantiate wallets
|
||||
//! and listeners
|
||||
|
||||
#![deny(non_upper_case_globals)]
|
||||
#![deny(non_camel_case_types)]
|
||||
#![deny(non_snake_case)]
|
||||
#![deny(unused_mut)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate grin_core as core;
|
||||
|
||||
extern crate grin_keychain as keychain;
|
||||
extern crate grin_util as util;
|
||||
|
||||
use blake2_rfc as blake2;
|
||||
|
||||
use failure;
|
||||
extern crate failure_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod api;
|
||||
mod error;
|
||||
pub mod internal;
|
||||
pub mod slate;
|
||||
pub mod slate_versions;
|
||||
pub mod types;
|
||||
|
||||
pub use crate::error::{Error, ErrorKind};
|
||||
@@ -0,0 +1,559 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Functions for building partial transactions to be passed
|
||||
//! around during an interactive wallet exchange
|
||||
|
||||
use crate::blake2::blake2b::blake2b;
|
||||
use crate::keychain::{BlindSum, BlindingFactor, Keychain};
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::slate_versions::v0::SlateV0;
|
||||
use crate::util::secp;
|
||||
use crate::util::secp::key::{PublicKey, SecretKey};
|
||||
use crate::util::secp::Signature;
|
||||
use crate::util::RwLock;
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
use grin_core::core::committed::Committed;
|
||||
use grin_core::core::transaction::{kernel_features, kernel_sig_msg, Transaction, Weighting};
|
||||
use grin_core::core::verifier_cache::LruVerifierCache;
|
||||
use grin_core::libtx::{aggsig, build, secp_ser, tx_fee};
|
||||
use rand::thread_rng;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
const CURRENT_SLATE_VERSION: u64 = 1;
|
||||
|
||||
/// A wrapper around slates the enables support for versioning
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum VersionedSlate {
|
||||
/// Pre versioning version
|
||||
V0(SlateV0),
|
||||
/// Version 1 with versioning and hex serialization - current
|
||||
V1(Slate),
|
||||
}
|
||||
|
||||
impl From<VersionedSlate> for Slate {
|
||||
fn from(ver: VersionedSlate) -> Self {
|
||||
match ver {
|
||||
VersionedSlate::V0(slate_v0) => Slate::from(slate_v0),
|
||||
VersionedSlate::V1(slate) => slate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public data for each participant in the slate
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ParticipantData {
|
||||
/// Id of participant in the transaction. (For now, 0=sender, 1=rec)
|
||||
pub id: u64,
|
||||
/// Public key corresponding to private blinding factor
|
||||
#[serde(with = "secp_ser::pubkey_serde")]
|
||||
pub public_blind_excess: PublicKey,
|
||||
/// Public key corresponding to private nonce
|
||||
#[serde(with = "secp_ser::pubkey_serde")]
|
||||
pub public_nonce: PublicKey,
|
||||
/// Public partial signature
|
||||
#[serde(with = "secp_ser::option_sig_serde")]
|
||||
pub part_sig: Option<Signature>,
|
||||
/// A message for other participants
|
||||
pub message: Option<String>,
|
||||
/// Signature, created with private key corresponding to 'public_blind_excess'
|
||||
#[serde(with = "secp_ser::option_sig_serde")]
|
||||
pub message_sig: Option<Signature>,
|
||||
}
|
||||
|
||||
impl ParticipantData {
|
||||
/// A helper to return whether this participant
|
||||
/// has completed round 1 and round 2;
|
||||
/// Round 1 has to be completed before instantiation of this struct
|
||||
/// anyhow, and for each participant consists of:
|
||||
/// -Inputs added to transaction
|
||||
/// -Outputs added to transaction
|
||||
/// -Public signature nonce chosen and added
|
||||
/// -Public contribution to blinding factor chosen and added
|
||||
/// Round 2 can only be completed after all participants have
|
||||
/// performed round 1, and adds:
|
||||
/// -Part sig is filled out
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.part_sig.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Public message data (for serialising and storage)
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ParticipantMessageData {
|
||||
/// id of the particpant in the tx
|
||||
pub id: u64,
|
||||
/// Public key
|
||||
#[serde(with = "secp_ser::pubkey_serde")]
|
||||
pub public_key: PublicKey,
|
||||
/// Message,
|
||||
pub message: Option<String>,
|
||||
/// Signature
|
||||
#[serde(with = "secp_ser::option_sig_serde")]
|
||||
pub message_sig: Option<Signature>,
|
||||
}
|
||||
|
||||
impl ParticipantMessageData {
|
||||
/// extract relevant message data from participant data
|
||||
pub fn from_participant_data(p: &ParticipantData) -> ParticipantMessageData {
|
||||
ParticipantMessageData {
|
||||
id: p.id,
|
||||
public_key: p.public_blind_excess,
|
||||
message: p.message.clone(),
|
||||
message_sig: p.message_sig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 'Slate' is passed around to all parties to build up all of the public
|
||||
/// transaction data needed to create a finalized transaction. Callers can pass
|
||||
/// the slate around by whatever means they choose, (but we can provide some
|
||||
/// binary or JSON serialization helpers here).
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Slate {
|
||||
/// The number of participants intended to take part in this transaction
|
||||
pub num_participants: usize,
|
||||
/// Unique transaction ID, selected by sender
|
||||
pub id: Uuid,
|
||||
/// The core transaction data:
|
||||
/// inputs, outputs, kernels, kernel offset
|
||||
pub tx: Transaction,
|
||||
/// base amount (excluding fee)
|
||||
pub amount: u64,
|
||||
/// fee amount
|
||||
pub fee: u64,
|
||||
/// Block height for the transaction
|
||||
pub height: u64,
|
||||
/// Lock height
|
||||
pub lock_height: u64,
|
||||
/// Participant data, each participant in the transaction will
|
||||
/// insert their public data here. For now, 0 is sender and 1
|
||||
/// is receiver, though this will change for multi-party
|
||||
pub participant_data: Vec<ParticipantData>,
|
||||
/// Slate format version
|
||||
#[serde(default = "no_version")]
|
||||
pub version: u64,
|
||||
}
|
||||
|
||||
fn no_version() -> u64 {
|
||||
0
|
||||
}
|
||||
|
||||
/// Helper just to facilitate serialization
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ParticipantMessages {
|
||||
/// included messages
|
||||
pub messages: Vec<ParticipantMessageData>,
|
||||
}
|
||||
|
||||
impl Slate {
|
||||
/// Create a new slate
|
||||
pub fn blank(num_participants: usize) -> Slate {
|
||||
Slate {
|
||||
num_participants: num_participants,
|
||||
id: Uuid::new_v4(),
|
||||
tx: Transaction::empty(),
|
||||
amount: 0,
|
||||
fee: 0,
|
||||
height: 0,
|
||||
lock_height: 0,
|
||||
participant_data: vec![],
|
||||
version: CURRENT_SLATE_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds selected inputs and outputs to the slate's transaction
|
||||
/// Returns blinding factor
|
||||
pub fn add_transaction_elements<K>(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
mut elems: Vec<Box<build::Append<K>>>,
|
||||
) -> Result<BlindingFactor, Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
// Append to the exiting transaction
|
||||
if self.tx.kernels().len() != 0 {
|
||||
elems.insert(0, build::initial_tx(self.tx.clone()));
|
||||
}
|
||||
let (tx, blind) = build::partial_transaction(elems, keychain)?;
|
||||
self.tx = tx;
|
||||
Ok(blind)
|
||||
}
|
||||
|
||||
/// Completes callers part of round 1, adding public key info
|
||||
/// to the slate
|
||||
pub fn fill_round_1<K>(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
sec_key: &mut SecretKey,
|
||||
sec_nonce: &SecretKey,
|
||||
participant_id: usize,
|
||||
message: Option<String>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
// Whoever does this first generates the offset
|
||||
if self.tx.offset == BlindingFactor::zero() {
|
||||
self.generate_offset(keychain, sec_key)?;
|
||||
}
|
||||
self.add_participant_info(
|
||||
keychain,
|
||||
&sec_key,
|
||||
&sec_nonce,
|
||||
participant_id,
|
||||
None,
|
||||
message,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This is the msg that we will sign as part of the tx kernel.
|
||||
// Currently includes the fee and the lock_height.
|
||||
fn msg_to_sign(&self) -> Result<secp::Message, Error> {
|
||||
// Currently we only support interactively creating a tx with a "default" kernel.
|
||||
let features = kernel_features(self.lock_height);
|
||||
let msg = kernel_sig_msg(self.fee, self.lock_height, features)?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Completes caller's part of round 2, completing signatures
|
||||
pub fn fill_round_2<K>(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
sec_key: &SecretKey,
|
||||
sec_nonce: &SecretKey,
|
||||
participant_id: usize,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
self.check_fees()?;
|
||||
|
||||
self.verify_part_sigs(keychain.secp())?;
|
||||
let sig_part = aggsig::calculate_partial_sig(
|
||||
keychain.secp(),
|
||||
sec_key,
|
||||
sec_nonce,
|
||||
&self.pub_nonce_sum(keychain.secp())?,
|
||||
Some(&self.pub_blind_sum(keychain.secp())?),
|
||||
&self.msg_to_sign()?,
|
||||
)?;
|
||||
self.participant_data[participant_id].part_sig = Some(sig_part);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates the final signature, callable by either the sender or recipient
|
||||
/// (after phase 3: sender confirmation)
|
||||
/// TODO: Only callable by receiver at the moment
|
||||
pub fn finalize<K>(&mut self, keychain: &K) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
let final_sig = self.finalize_signature(keychain)?;
|
||||
self.finalize_transaction(keychain, &final_sig)
|
||||
}
|
||||
|
||||
/// Return the sum of public nonces
|
||||
fn pub_nonce_sum(&self, secp: &secp::Secp256k1) -> Result<PublicKey, Error> {
|
||||
let pub_nonces = self
|
||||
.participant_data
|
||||
.iter()
|
||||
.map(|p| &p.public_nonce)
|
||||
.collect();
|
||||
match PublicKey::from_combination(secp, pub_nonces) {
|
||||
Ok(k) => Ok(k),
|
||||
Err(e) => Err(ErrorKind::Secp(e))?,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the sum of public blinding factors
|
||||
fn pub_blind_sum(&self, secp: &secp::Secp256k1) -> Result<PublicKey, Error> {
|
||||
let pub_blinds = self
|
||||
.participant_data
|
||||
.iter()
|
||||
.map(|p| &p.public_blind_excess)
|
||||
.collect();
|
||||
match PublicKey::from_combination(secp, pub_blinds) {
|
||||
Ok(k) => Ok(k),
|
||||
Err(e) => Err(ErrorKind::Secp(e))?,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return vector of all partial sigs
|
||||
fn part_sigs(&self) -> Vec<&Signature> {
|
||||
self.participant_data
|
||||
.iter()
|
||||
.map(|p| p.part_sig.as_ref().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Adds participants public keys to the slate data
|
||||
/// and saves participant's transaction context
|
||||
/// sec_key can be overridden to replace the blinding
|
||||
/// factor (by whoever split the offset)
|
||||
fn add_participant_info<K>(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
sec_key: &SecretKey,
|
||||
sec_nonce: &SecretKey,
|
||||
id: usize,
|
||||
part_sig: Option<Signature>,
|
||||
message: Option<String>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
// Add our public key and nonce to the slate
|
||||
let pub_key = PublicKey::from_secret_key(keychain.secp(), &sec_key)?;
|
||||
let pub_nonce = PublicKey::from_secret_key(keychain.secp(), &sec_nonce)?;
|
||||
// Sign the provided message
|
||||
let message_sig = {
|
||||
if let Some(m) = message.clone() {
|
||||
let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &m.as_bytes()[..]);
|
||||
let m = secp::Message::from_slice(&hashed.as_bytes())?;
|
||||
let res = aggsig::sign_single(&keychain.secp(), &m, &sec_key, Some(&pub_key))?;
|
||||
Some(res)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
self.participant_data.push(ParticipantData {
|
||||
id: id as u64,
|
||||
public_blind_excess: pub_key,
|
||||
public_nonce: pub_nonce,
|
||||
part_sig: part_sig,
|
||||
message: message,
|
||||
message_sig: message_sig,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// helper to return all participant messages
|
||||
pub fn participant_messages(&self) -> ParticipantMessages {
|
||||
let mut ret = ParticipantMessages { messages: vec![] };
|
||||
for ref m in self.participant_data.iter() {
|
||||
ret.messages
|
||||
.push(ParticipantMessageData::from_participant_data(m));
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Somebody involved needs to generate an offset with their private key
|
||||
/// For now, we'll have the transaction initiator be responsible for it
|
||||
/// Return offset private key for the participant to use later in the
|
||||
/// transaction
|
||||
fn generate_offset<K>(&mut self, keychain: &K, sec_key: &mut SecretKey) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
// Generate a random kernel offset here
|
||||
// and subtract it from the blind_sum so we create
|
||||
// the aggsig context with the "split" key
|
||||
self.tx.offset =
|
||||
BlindingFactor::from_secret_key(SecretKey::new(&keychain.secp(), &mut thread_rng()));
|
||||
let blind_offset = keychain.blind_sum(
|
||||
&BlindSum::new()
|
||||
.add_blinding_factor(BlindingFactor::from_secret_key(sec_key.clone()))
|
||||
.sub_blinding_factor(self.tx.offset),
|
||||
)?;
|
||||
*sec_key = blind_offset.secret_key(&keychain.secp())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks the fees in the transaction in the given slate are valid
|
||||
fn check_fees(&self) -> Result<(), Error> {
|
||||
// double check the fee amount included in the partial tx
|
||||
// we don't necessarily want to just trust the sender
|
||||
// we could just overwrite the fee here (but we won't) due to the sig
|
||||
let fee = tx_fee(
|
||||
self.tx.inputs().len(),
|
||||
self.tx.outputs().len(),
|
||||
self.tx.kernels().len(),
|
||||
None,
|
||||
);
|
||||
if fee > self.tx.fee() {
|
||||
return Err(ErrorKind::Fee(
|
||||
format!("Fee Dispute Error: {}, {}", self.tx.fee(), fee,).to_string(),
|
||||
))?;
|
||||
}
|
||||
|
||||
if fee > self.amount + self.fee {
|
||||
let reason = format!(
|
||||
"Rejected the transfer because transaction fee ({}) exceeds received amount ({}).",
|
||||
amount_to_hr_string(fee, false),
|
||||
amount_to_hr_string(self.amount + self.fee, false)
|
||||
);
|
||||
info!("{}", reason);
|
||||
return Err(ErrorKind::Fee(reason.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies all of the partial signatures in the Slate are valid
|
||||
fn verify_part_sigs(&self, secp: &secp::Secp256k1) -> Result<(), Error> {
|
||||
// collect public nonces
|
||||
for p in self.participant_data.iter() {
|
||||
if p.is_complete() {
|
||||
aggsig::verify_partial_sig(
|
||||
secp,
|
||||
p.part_sig.as_ref().unwrap(),
|
||||
&self.pub_nonce_sum(secp)?,
|
||||
&p.public_blind_excess,
|
||||
Some(&self.pub_blind_sum(secp)?),
|
||||
&self.msg_to_sign()?,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies any messages in the slate's participant data match their signatures
|
||||
pub fn verify_messages(&self, secp: &secp::Secp256k1) -> Result<(), Error> {
|
||||
for p in self.participant_data.iter() {
|
||||
if let Some(msg) = &p.message {
|
||||
let hashed = blake2b(secp::constants::MESSAGE_SIZE, &[], &msg.as_bytes()[..]);
|
||||
let m = secp::Message::from_slice(&hashed.as_bytes())?;
|
||||
let signature = match p.message_sig {
|
||||
None => {
|
||||
error!("verify_messages - participant message doesn't have signature. Message: \"{}\"",
|
||||
String::from_utf8_lossy(&msg.as_bytes()[..]));
|
||||
return Err(ErrorKind::Signature(
|
||||
"Optional participant messages doesn't have signature".to_owned(),
|
||||
))?;
|
||||
}
|
||||
Some(s) => s,
|
||||
};
|
||||
if !aggsig::verify_single(
|
||||
secp,
|
||||
&signature,
|
||||
&m,
|
||||
None,
|
||||
&p.public_blind_excess,
|
||||
Some(&p.public_blind_excess),
|
||||
false,
|
||||
) {
|
||||
error!("verify_messages - participant message doesn't match signature. Message: \"{}\"",
|
||||
String::from_utf8_lossy(&msg.as_bytes()[..]));
|
||||
return Err(ErrorKind::Signature(
|
||||
"Optional participant messages do not match signatures".to_owned(),
|
||||
))?;
|
||||
} else {
|
||||
info!(
|
||||
"verify_messages - signature verified ok. Participant message: \"{}\"",
|
||||
String::from_utf8_lossy(&msg.as_bytes()[..])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This should be callable by either the sender or receiver
|
||||
/// once phase 3 is done
|
||||
///
|
||||
/// Receive Part 3 of interactive transactions from sender, Sender
|
||||
/// Confirmation Return Ok/Error
|
||||
/// -Receiver receives sS
|
||||
/// -Receiver verifies sender's sig, by verifying that
|
||||
/// kS * G + e *xS * G = sS* G
|
||||
/// -Receiver calculates final sig as s=(sS+sR, kS * G+kR * G)
|
||||
/// -Receiver puts into TX kernel:
|
||||
///
|
||||
/// Signature S
|
||||
/// pubkey xR * G+xS * G
|
||||
/// fee (= M)
|
||||
///
|
||||
/// Returns completed transaction ready for posting to the chain
|
||||
|
||||
fn finalize_signature<K>(&mut self, keychain: &K) -> Result<Signature, Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
self.verify_part_sigs(keychain.secp())?;
|
||||
|
||||
let part_sigs = self.part_sigs();
|
||||
let pub_nonce_sum = self.pub_nonce_sum(keychain.secp())?;
|
||||
let final_pubkey = self.pub_blind_sum(keychain.secp())?;
|
||||
// get the final signature
|
||||
let final_sig = aggsig::add_signatures(&keychain.secp(), part_sigs, &pub_nonce_sum)?;
|
||||
|
||||
// Calculate the final public key (for our own sanity check)
|
||||
|
||||
// Check our final sig verifies
|
||||
aggsig::verify_completed_sig(
|
||||
&keychain.secp(),
|
||||
&final_sig,
|
||||
&final_pubkey,
|
||||
Some(&final_pubkey),
|
||||
&self.msg_to_sign()?,
|
||||
)?;
|
||||
|
||||
Ok(final_sig)
|
||||
}
|
||||
|
||||
/// builds a final transaction after the aggregated sig exchange
|
||||
fn finalize_transaction<K>(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
final_sig: &secp::Signature,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
let kernel_offset = self.tx.offset;
|
||||
|
||||
self.check_fees()?;
|
||||
|
||||
let mut final_tx = self.tx.clone();
|
||||
|
||||
// build the final excess based on final tx and offset
|
||||
let final_excess = {
|
||||
// sum the input/output commitments on the final tx
|
||||
let overage = final_tx.fee() as i64;
|
||||
let tx_excess = final_tx.sum_commitments(overage)?;
|
||||
|
||||
// subtract the kernel_excess (built from kernel_offset)
|
||||
let offset_excess = keychain
|
||||
.secp()
|
||||
.commit(0, kernel_offset.secret_key(&keychain.secp())?)?;
|
||||
keychain
|
||||
.secp()
|
||||
.commit_sum(vec![tx_excess], vec![offset_excess])?
|
||||
};
|
||||
|
||||
// update the tx kernel to reflect the offset excess and sig
|
||||
assert_eq!(final_tx.kernels().len(), 1);
|
||||
final_tx.kernels_mut()[0].excess = final_excess.clone();
|
||||
final_tx.kernels_mut()[0].excess_sig = final_sig.clone();
|
||||
|
||||
// confirm the kernel verifies successfully before proceeding
|
||||
debug!("Validating final transaction");
|
||||
final_tx.kernels()[0].verify()?;
|
||||
|
||||
// confirm the overall transaction is valid (including the updated kernel)
|
||||
// accounting for tx weight limits
|
||||
let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new()));
|
||||
let _ = final_tx.validate(Weighting::AsTransaction, verifier_cache)?;
|
||||
|
||||
self.tx = final_tx;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright 2019 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! This module contains old slate versions and conversions to the newest slate version
|
||||
//! Used for serialization and deserialization of slates in a backwards compatible way.
|
||||
#[allow(missing_docs)]
|
||||
pub mod v0;
|
||||
@@ -0,0 +1,370 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Contains V0 of the slate
|
||||
use crate::core::core::transaction::{
|
||||
Input, KernelFeatures, Output, OutputFeatures, Transaction, TransactionBody, TxKernel,
|
||||
};
|
||||
use crate::keychain::BlindingFactor;
|
||||
use crate::slate::{ParticipantData, Slate};
|
||||
use crate::util::secp;
|
||||
use crate::util::secp::key::PublicKey;
|
||||
use crate::util::secp::pedersen::{Commitment, RangeProof};
|
||||
use crate::util::secp::Signature;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct SlateV0 {
|
||||
/// The number of participants intended to take part in this transaction
|
||||
pub num_participants: usize,
|
||||
/// Unique transaction ID, selected by sender
|
||||
pub id: Uuid,
|
||||
/// The core transaction data:
|
||||
/// inputs, outputs, kernels, kernel offset
|
||||
pub tx: TransactionV0,
|
||||
/// base amount (excluding fee)
|
||||
pub amount: u64,
|
||||
/// fee amount
|
||||
pub fee: u64,
|
||||
/// Block height for the transaction
|
||||
pub height: u64,
|
||||
/// Lock height
|
||||
pub lock_height: u64,
|
||||
/// Participant data, each participant in the transaction will
|
||||
/// insert their public data here. For now, 0 is sender and 1
|
||||
/// is receiver, though this will change for multi-party
|
||||
pub participant_data: Vec<ParticipantDataV0>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ParticipantDataV0 {
|
||||
/// Id of participant in the transaction. (For now, 0=sender, 1=rec)
|
||||
pub id: u64,
|
||||
/// Public key corresponding to private blinding factor
|
||||
pub public_blind_excess: PublicKey,
|
||||
/// Public key corresponding to private nonce
|
||||
pub public_nonce: PublicKey,
|
||||
/// Public partial signature
|
||||
pub part_sig: Option<Signature>,
|
||||
/// A message for other participants
|
||||
pub message: Option<String>,
|
||||
/// Signature, created with private key corresponding to 'public_blind_excess'
|
||||
pub message_sig: Option<Signature>,
|
||||
}
|
||||
|
||||
/// A transaction
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct TransactionV0 {
|
||||
/// The kernel "offset" k2
|
||||
/// excess is k1G after splitting the key k = k1 + k2
|
||||
pub offset: BlindingFactor,
|
||||
/// The transaction body - inputs/outputs/kernels
|
||||
pub body: TransactionBodyV0,
|
||||
}
|
||||
|
||||
/// TransactionBody is a common abstraction for transaction and block
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct TransactionBodyV0 {
|
||||
/// List of inputs spent by the transaction.
|
||||
pub inputs: Vec<InputV0>,
|
||||
/// List of outputs the transaction produces.
|
||||
pub outputs: Vec<OutputV0>,
|
||||
/// List of kernels that make up this transaction (usually a single kernel).
|
||||
pub kernels: Vec<TxKernelV0>,
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct InputV0 {
|
||||
/// The features of the output being spent.
|
||||
/// We will check maturity for coinbase output.
|
||||
pub features: OutputFeatures,
|
||||
/// The commit referencing the output being spent.
|
||||
pub commit: Commitment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct OutputV0 {
|
||||
/// Options for an output's structure or use
|
||||
pub features: OutputFeatures,
|
||||
/// The homomorphic commitment representing the output amount
|
||||
pub commit: Commitment,
|
||||
/// A proof that the commitment is in the right range
|
||||
pub proof: RangeProof,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct TxKernelV0 {
|
||||
/// Options for a kernel's structure or use
|
||||
pub features: KernelFeatures,
|
||||
/// Fee originally included in the transaction this proof is for.
|
||||
pub fee: u64,
|
||||
/// This kernel is not valid earlier than lock_height blocks
|
||||
/// The max lock_height of all *inputs* to this transaction
|
||||
pub lock_height: u64,
|
||||
/// Remainder of the sum of all transaction commitments. If the transaction
|
||||
/// is well formed, amounts components should sum to zero and the excess
|
||||
/// is hence a valid public key.
|
||||
pub excess: Commitment,
|
||||
/// The signature proving the excess is a valid public key, which signs
|
||||
/// the transaction fee.
|
||||
pub excess_sig: secp::Signature,
|
||||
}
|
||||
|
||||
impl From<SlateV0> for Slate {
|
||||
fn from(slate: SlateV0) -> Slate {
|
||||
let SlateV0 {
|
||||
num_participants,
|
||||
id,
|
||||
tx,
|
||||
amount,
|
||||
fee,
|
||||
height,
|
||||
lock_height,
|
||||
participant_data,
|
||||
} = slate;
|
||||
let tx = Transaction::from(tx);
|
||||
let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data));
|
||||
let version = 0;
|
||||
Slate {
|
||||
num_participants,
|
||||
id,
|
||||
tx,
|
||||
amount,
|
||||
fee,
|
||||
height,
|
||||
lock_height,
|
||||
participant_data,
|
||||
version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ParticipantDataV0> for ParticipantData {
|
||||
fn from(data: &ParticipantDataV0) -> ParticipantData {
|
||||
let ParticipantDataV0 {
|
||||
id,
|
||||
public_blind_excess,
|
||||
public_nonce,
|
||||
part_sig,
|
||||
message,
|
||||
message_sig,
|
||||
} = data;
|
||||
let id = *id;
|
||||
let public_blind_excess = *public_blind_excess;
|
||||
let public_nonce = *public_nonce;
|
||||
let part_sig = *part_sig;
|
||||
let message: Option<String> = message.as_ref().map(|t| String::from(&**t));
|
||||
let message_sig = *message_sig;
|
||||
ParticipantData {
|
||||
id,
|
||||
public_blind_excess,
|
||||
public_nonce,
|
||||
part_sig,
|
||||
message,
|
||||
message_sig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransactionV0> for Transaction {
|
||||
fn from(tx: TransactionV0) -> Transaction {
|
||||
let TransactionV0 { offset, body } = tx;
|
||||
let body = TransactionBody::from(&body);
|
||||
let transaction = Transaction::new(body.inputs, body.outputs, body.kernels);
|
||||
transaction.with_offset(offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TransactionBodyV0> for TransactionBody {
|
||||
fn from(body: &TransactionBodyV0) -> Self {
|
||||
let TransactionBodyV0 {
|
||||
inputs,
|
||||
outputs,
|
||||
kernels,
|
||||
} = body;
|
||||
|
||||
let inputs = map_vec!(inputs, |inp| Input::from(inp));
|
||||
let outputs = map_vec!(outputs, |out| Output::from(out));
|
||||
let kernels = map_vec!(kernels, |kern| TxKernel::from(kern));
|
||||
TransactionBody {
|
||||
inputs,
|
||||
outputs,
|
||||
kernels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&InputV0> for Input {
|
||||
fn from(input: &InputV0) -> Input {
|
||||
let InputV0 { features, commit } = *input;
|
||||
Input { features, commit }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OutputV0> for Output {
|
||||
fn from(output: &OutputV0) -> Output {
|
||||
let OutputV0 {
|
||||
features,
|
||||
commit,
|
||||
proof,
|
||||
} = *output;
|
||||
Output {
|
||||
features,
|
||||
commit,
|
||||
proof,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TxKernelV0> for TxKernel {
|
||||
fn from(kernel: &TxKernelV0) -> TxKernel {
|
||||
let TxKernelV0 {
|
||||
features,
|
||||
fee,
|
||||
lock_height,
|
||||
excess,
|
||||
excess_sig,
|
||||
} = *kernel;
|
||||
TxKernel {
|
||||
features,
|
||||
fee,
|
||||
lock_height,
|
||||
excess,
|
||||
excess_sig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Slate> for SlateV0 {
|
||||
fn from(slate: Slate) -> SlateV0 {
|
||||
let Slate {
|
||||
num_participants,
|
||||
id,
|
||||
tx,
|
||||
amount,
|
||||
fee,
|
||||
height,
|
||||
lock_height,
|
||||
participant_data,
|
||||
version: _,
|
||||
} = slate;
|
||||
let tx = TransactionV0::from(tx);
|
||||
let participant_data = map_vec!(participant_data, |data| ParticipantDataV0::from(data));
|
||||
SlateV0 {
|
||||
num_participants,
|
||||
id,
|
||||
tx,
|
||||
amount,
|
||||
fee,
|
||||
height,
|
||||
lock_height,
|
||||
participant_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ParticipantData> for ParticipantDataV0 {
|
||||
fn from(data: &ParticipantData) -> ParticipantDataV0 {
|
||||
let ParticipantData {
|
||||
id,
|
||||
public_blind_excess,
|
||||
public_nonce,
|
||||
part_sig,
|
||||
message,
|
||||
message_sig,
|
||||
} = data;
|
||||
let id = *id;
|
||||
let public_blind_excess = *public_blind_excess;
|
||||
let public_nonce = *public_nonce;
|
||||
let part_sig = *part_sig;
|
||||
let message: Option<String> = message.as_ref().map(|t| String::from(&**t));
|
||||
let message_sig = *message_sig;
|
||||
ParticipantDataV0 {
|
||||
id,
|
||||
public_blind_excess,
|
||||
public_nonce,
|
||||
part_sig,
|
||||
message,
|
||||
message_sig,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transaction> for TransactionV0 {
|
||||
fn from(tx: Transaction) -> TransactionV0 {
|
||||
let offset = tx.offset;
|
||||
let body: TransactionBody = tx.into();
|
||||
let body = TransactionBodyV0::from(&body);
|
||||
TransactionV0 { offset, body }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TransactionBody> for TransactionBodyV0 {
|
||||
fn from(body: &TransactionBody) -> Self {
|
||||
let TransactionBody {
|
||||
inputs,
|
||||
outputs,
|
||||
kernels,
|
||||
} = body;
|
||||
|
||||
let inputs = map_vec!(inputs, |inp| InputV0::from(inp));
|
||||
let outputs = map_vec!(outputs, |out| OutputV0::from(out));
|
||||
let kernels = map_vec!(kernels, |kern| TxKernelV0::from(kern));
|
||||
TransactionBodyV0 {
|
||||
inputs,
|
||||
outputs,
|
||||
kernels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Input> for InputV0 {
|
||||
fn from(input: &Input) -> Self {
|
||||
let Input { features, commit } = *input;
|
||||
InputV0 { features, commit }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Output> for OutputV0 {
|
||||
fn from(output: &Output) -> Self {
|
||||
let Output {
|
||||
features,
|
||||
commit,
|
||||
proof,
|
||||
} = *output;
|
||||
OutputV0 {
|
||||
features,
|
||||
commit,
|
||||
proof,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TxKernel> for TxKernelV0 {
|
||||
fn from(kernel: &TxKernel) -> Self {
|
||||
let TxKernel {
|
||||
features,
|
||||
fee,
|
||||
lock_height,
|
||||
excess,
|
||||
excess_sig,
|
||||
} = *kernel;
|
||||
TxKernelV0 {
|
||||
features,
|
||||
fee,
|
||||
lock_height,
|
||||
excess,
|
||||
excess_sig,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Types and traits that should be provided by a wallet
|
||||
//! implementation
|
||||
|
||||
use crate::core::core::hash::Hash;
|
||||
use crate::core::core::Transaction;
|
||||
use crate::core::libtx::aggsig;
|
||||
use crate::core::ser;
|
||||
use crate::keychain::{Identifier, Keychain};
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::slate::ParticipantMessages;
|
||||
use crate::util::secp::key::{PublicKey, SecretKey};
|
||||
use crate::util::secp::{self, pedersen, Secp256k1};
|
||||
use chrono::prelude::*;
|
||||
use failure::ResultExt;
|
||||
use serde;
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Lock function type
|
||||
pub type OutputLockFn<T, C, K> =
|
||||
Box<dyn FnMut(&mut T, &Transaction, PhantomData<C>, PhantomData<K>) -> Result<(), Error>>;
|
||||
|
||||
/// Combined trait to allow dynamic wallet dispatch
|
||||
pub trait WalletInst<C, K>: WalletBackend<C, K> + Send + Sync + 'static
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
}
|
||||
impl<T, C, K> WalletInst<C, K> for T
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
}
|
||||
|
||||
/// TODO:
|
||||
/// Wallets should implement this backend for their storage. All functions
|
||||
/// here expect that the wallet instance has instantiated itself or stored
|
||||
/// whatever credentials it needs
|
||||
pub trait WalletBackend<C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Initialize with whatever stored credentials we have
|
||||
fn open_with_credentials(&mut self) -> Result<(), Error>;
|
||||
|
||||
/// Close wallet and remove any stored credentials (TBD)
|
||||
fn close(&mut self) -> Result<(), Error>;
|
||||
|
||||
/// Return the keychain being used
|
||||
fn keychain(&mut self) -> &mut K;
|
||||
|
||||
/// Return the client being used to communicate with the node
|
||||
fn w2n_client(&mut self) -> &mut C;
|
||||
|
||||
/// return the commit for caching if allowed, none otherwise
|
||||
fn calc_commit_for_cache(
|
||||
&mut self,
|
||||
amount: u64,
|
||||
id: &Identifier,
|
||||
) -> Result<Option<String>, Error>;
|
||||
|
||||
/// Set parent key id by stored account name
|
||||
fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error>;
|
||||
|
||||
/// The BIP32 path of the parent path to use for all output-related
|
||||
/// functions, (essentially 'accounts' within a wallet.
|
||||
fn set_parent_key_id(&mut self, _: Identifier);
|
||||
|
||||
/// return the parent path
|
||||
fn parent_key_id(&mut self) -> Identifier;
|
||||
|
||||
/// Iterate over all output data stored by the backend
|
||||
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = OutputData> + 'a>;
|
||||
|
||||
/// Get output data by id
|
||||
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error>;
|
||||
|
||||
/// Get an (Optional) tx log entry by uuid
|
||||
fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
|
||||
|
||||
/// Retrieves the private context associated with a given slate id
|
||||
fn get_private_context(&mut self, slate_id: &[u8]) -> Result<Context, Error>;
|
||||
|
||||
/// Iterate over all output data stored by the backend
|
||||
fn tx_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = TxLogEntry> + 'a>;
|
||||
|
||||
/// Iterate over all stored account paths
|
||||
fn acct_path_iter<'a>(&'a self) -> Box<dyn Iterator<Item = AcctPathMapping> + 'a>;
|
||||
|
||||
/// Gets an account path for a given label
|
||||
fn get_acct_path(&self, label: String) -> Result<Option<AcctPathMapping>, Error>;
|
||||
|
||||
/// Stores a transaction
|
||||
fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error>;
|
||||
|
||||
/// Retrieves a stored transaction from a TxLogEntry
|
||||
fn get_stored_tx(&self, entry: &TxLogEntry) -> Result<Option<Transaction>, Error>;
|
||||
|
||||
/// Create a new write batch to update or remove output data
|
||||
fn batch<'a>(&'a mut self) -> Result<Box<dyn WalletOutputBatch<K> + 'a>, Error>;
|
||||
|
||||
/// Next child ID when we want to create a new output, based on current parent
|
||||
fn next_child<'a>(&mut self) -> Result<Identifier, Error>;
|
||||
|
||||
/// last verified height of outputs directly descending from the given parent key
|
||||
fn last_confirmed_height<'a>(&mut self) -> Result<u64, Error>;
|
||||
|
||||
/// Attempt to restore the contents of a wallet from seed
|
||||
fn restore(&mut self) -> Result<(), Error>;
|
||||
|
||||
/// Attempt to check and fix wallet state
|
||||
fn check_repair(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Batch trait to update the output data backend atomically. Trying to use a
|
||||
/// batch after commit MAY result in a panic. Due to this being a trait, the
|
||||
/// commit method can't take ownership.
|
||||
/// TODO: Should these be split into separate batch objects, for outputs,
|
||||
/// tx_log entries and meta/details?
|
||||
pub trait WalletOutputBatch<K>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
/// Return the keychain being used
|
||||
fn keychain(&mut self) -> &mut K;
|
||||
|
||||
/// Add or update data about an output to the backend
|
||||
fn save(&mut self, out: OutputData) -> Result<(), Error>;
|
||||
|
||||
/// Gets output data by id
|
||||
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error>;
|
||||
|
||||
/// Iterate over all output data stored by the backend
|
||||
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>>;
|
||||
|
||||
/// Delete data about an output from the backend
|
||||
fn delete(&mut self, id: &Identifier, mmr_index: &Option<u64>) -> Result<(), Error>;
|
||||
|
||||
/// Save last stored child index of a given parent
|
||||
fn save_child_index(&mut self, parent_key_id: &Identifier, child_n: u32) -> Result<(), Error>;
|
||||
|
||||
/// Save last confirmed height of outputs for a given parent
|
||||
fn save_last_confirmed_height(
|
||||
&mut self,
|
||||
parent_key_id: &Identifier,
|
||||
height: u64,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
/// get next tx log entry for the parent
|
||||
fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result<u32, Error>;
|
||||
|
||||
/// Iterate over tx log data stored by the backend
|
||||
fn tx_log_iter(&self) -> Box<dyn Iterator<Item = TxLogEntry>>;
|
||||
|
||||
/// save a tx log entry
|
||||
fn save_tx_log_entry(&mut self, t: TxLogEntry, parent_id: &Identifier) -> Result<(), Error>;
|
||||
|
||||
/// save an account label -> path mapping
|
||||
fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error>;
|
||||
|
||||
/// Iterate over account names stored in backend
|
||||
fn acct_path_iter(&self) -> Box<dyn Iterator<Item = AcctPathMapping>>;
|
||||
|
||||
/// Save an output as locked in the backend
|
||||
fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error>;
|
||||
|
||||
/// Saves the private context associated with a slate id
|
||||
fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error>;
|
||||
|
||||
/// Delete the private context associated with the slate id
|
||||
fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error>;
|
||||
|
||||
/// Write the wallet data to backend file
|
||||
fn commit(&self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Encapsulate all wallet-node communication functions. No functions within libwallet
|
||||
/// should care about communication details
|
||||
pub trait NodeClient: Sync + Send + Clone {
|
||||
/// Return the URL of the check node
|
||||
fn node_url(&self) -> &str;
|
||||
|
||||
/// Set the node URL
|
||||
fn set_node_url(&mut self, node_url: &str);
|
||||
|
||||
/// Return the node api secret
|
||||
fn node_api_secret(&self) -> Option<String>;
|
||||
|
||||
/// Change the API secret
|
||||
fn set_node_api_secret(&mut self, node_api_secret: Option<String>);
|
||||
|
||||
/// Posts a transaction to a grin node
|
||||
fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), Error>;
|
||||
|
||||
/// retrieves the current tip from the specified grin node
|
||||
fn get_chain_height(&self) -> Result<u64, Error>;
|
||||
|
||||
/// retrieve a list of outputs from the specified grin node
|
||||
/// need "by_height" and "by_id" variants
|
||||
fn get_outputs_from_node(
|
||||
&self,
|
||||
wallet_outputs: Vec<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, (String, u64, u64)>, Error>;
|
||||
|
||||
/// Get a list of outputs from the node by traversing the UTXO
|
||||
/// set in PMMR index order.
|
||||
/// Returns
|
||||
/// (last available output index, last insertion index retrieved,
|
||||
/// outputs(commit, proof, is_coinbase, height, mmr_index))
|
||||
fn get_outputs_by_pmmr_index(
|
||||
&self,
|
||||
start_height: u64,
|
||||
max_outputs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
u64,
|
||||
u64,
|
||||
Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>,
|
||||
),
|
||||
Error,
|
||||
>;
|
||||
}
|
||||
|
||||
/// Information about an output that's being tracked by the wallet. Must be
|
||||
/// enough to reconstruct the commitment associated with the ouput when the
|
||||
/// root private key is known.
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct OutputData {
|
||||
/// Root key_id that the key for this output is derived from
|
||||
pub root_key_id: Identifier,
|
||||
/// Derived key for this output
|
||||
pub key_id: Identifier,
|
||||
/// How many derivations down from the root key
|
||||
pub n_child: u32,
|
||||
/// The actual commit, optionally stored
|
||||
pub commit: Option<String>,
|
||||
/// PMMR Index, used on restore in case of duplicate wallets using the same
|
||||
/// key_id (2 wallets using same seed, for instance
|
||||
pub mmr_index: Option<u64>,
|
||||
/// Value of the output, necessary to rebuild the commitment
|
||||
pub value: u64,
|
||||
/// Current status of the output
|
||||
pub status: OutputStatus,
|
||||
/// Height of the output
|
||||
pub height: u64,
|
||||
/// Height we are locked until
|
||||
pub lock_height: u64,
|
||||
/// Is this a coinbase output? Is it subject to coinbase locktime?
|
||||
pub is_coinbase: bool,
|
||||
/// Optional corresponding internal entry in tx entry log
|
||||
pub tx_log_entry: Option<u32>,
|
||||
}
|
||||
|
||||
impl ser::Writeable for OutputData {
|
||||
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Readable for OutputData {
|
||||
fn read(reader: &mut dyn ser::Reader) -> Result<OutputData, ser::Error> {
|
||||
let data = reader.read_bytes_len_prefix()?;
|
||||
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputData {
|
||||
/// Lock a given output to avoid conflicting use
|
||||
pub fn lock(&mut self) {
|
||||
self.status = OutputStatus::Locked;
|
||||
}
|
||||
|
||||
/// How many confirmations has this output received?
|
||||
/// If height == 0 then we are either Unconfirmed or the output was
|
||||
/// cut-through
|
||||
/// so we do not actually know how many confirmations this output had (and
|
||||
/// never will).
|
||||
pub fn num_confirmations(&self, current_height: u64) -> u64 {
|
||||
if self.height > current_height {
|
||||
return 0;
|
||||
}
|
||||
if self.status == OutputStatus::Unconfirmed {
|
||||
0
|
||||
} else {
|
||||
// if an output has height n and we are at block n
|
||||
// then we have a single confirmation (the block it originated in)
|
||||
1 + (current_height - self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if output is eligible to spend based on state and height and
|
||||
/// confirmations
|
||||
pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool {
|
||||
if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) {
|
||||
return false;
|
||||
} else if self.status == OutputStatus::Unconfirmed && self.is_coinbase {
|
||||
return false;
|
||||
} else if self.lock_height > current_height {
|
||||
return false;
|
||||
} else if self.status == OutputStatus::Unspent
|
||||
&& self.num_confirmations(current_height) >= minimum_confirmations
|
||||
{
|
||||
return true;
|
||||
} else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks this output as unspent if it was previously unconfirmed
|
||||
pub fn mark_unspent(&mut self) {
|
||||
match self.status {
|
||||
OutputStatus::Unconfirmed => self.status = OutputStatus::Unspent,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an output as spent
|
||||
pub fn mark_spent(&mut self) {
|
||||
match self.status {
|
||||
OutputStatus::Unspent => self.status = OutputStatus::Spent,
|
||||
OutputStatus::Locked => self.status = OutputStatus::Spent,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Status of an output that's being tracked by the wallet. Can either be
|
||||
/// unconfirmed, spent, unspent, or locked (when it's been used to generate
|
||||
/// a transaction but we don't have confirmation that the transaction was
|
||||
/// broadcasted or mined).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub enum OutputStatus {
|
||||
/// Unconfirmed
|
||||
Unconfirmed,
|
||||
/// Unspent
|
||||
Unspent,
|
||||
/// Locked
|
||||
Locked,
|
||||
/// Spent
|
||||
Spent,
|
||||
}
|
||||
|
||||
impl fmt::Display for OutputStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
OutputStatus::Unconfirmed => write!(f, "Unconfirmed"),
|
||||
OutputStatus::Unspent => write!(f, "Unspent"),
|
||||
OutputStatus::Locked => write!(f, "Locked"),
|
||||
OutputStatus::Spent => write!(f, "Spent"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
/// Holds the context for a single aggsig transaction
|
||||
pub struct Context {
|
||||
/// Secret key (of which public is shared)
|
||||
pub sec_key: SecretKey,
|
||||
/// Secret nonce (of which public is shared)
|
||||
/// (basically a SecretKey)
|
||||
pub sec_nonce: SecretKey,
|
||||
/// store my outputs between invocations
|
||||
pub output_ids: Vec<(Identifier, Option<u64>)>,
|
||||
/// store my inputs
|
||||
pub input_ids: Vec<(Identifier, Option<u64>)>,
|
||||
/// store the calculated fee
|
||||
pub fee: u64,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Create a new context with defaults
|
||||
pub fn new(secp: &secp::Secp256k1, sec_key: SecretKey) -> Context {
|
||||
Context {
|
||||
sec_key: sec_key,
|
||||
sec_nonce: aggsig::create_secnonce(secp).unwrap(),
|
||||
input_ids: vec![],
|
||||
output_ids: vec![],
|
||||
fee: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Tracks an output contributing to my excess value (if it needs to
|
||||
/// be kept between invocations
|
||||
pub fn add_output(&mut self, output_id: &Identifier, mmr_index: &Option<u64>) {
|
||||
self.output_ids.push((output_id.clone(), mmr_index.clone()));
|
||||
}
|
||||
|
||||
/// Returns all stored outputs
|
||||
pub fn get_outputs(&self) -> Vec<(Identifier, Option<u64>)> {
|
||||
self.output_ids.clone()
|
||||
}
|
||||
|
||||
/// Tracks IDs of my inputs into the transaction
|
||||
/// be kept between invocations
|
||||
pub fn add_input(&mut self, input_id: &Identifier, mmr_index: &Option<u64>) {
|
||||
self.input_ids.push((input_id.clone(), mmr_index.clone()));
|
||||
}
|
||||
|
||||
/// Returns all stored input identifiers
|
||||
pub fn get_inputs(&self) -> Vec<(Identifier, Option<u64>)> {
|
||||
self.input_ids.clone()
|
||||
}
|
||||
|
||||
/// Returns private key, private nonce
|
||||
pub fn get_private_keys(&self) -> (SecretKey, SecretKey) {
|
||||
(self.sec_key.clone(), self.sec_nonce.clone())
|
||||
}
|
||||
|
||||
/// Returns public key, public nonce
|
||||
pub fn get_public_keys(&self, secp: &Secp256k1) -> (PublicKey, PublicKey) {
|
||||
(
|
||||
PublicKey::from_secret_key(secp, &self.sec_key).unwrap(),
|
||||
PublicKey::from_secret_key(secp, &self.sec_nonce).unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Writeable for Context {
|
||||
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Readable for Context {
|
||||
fn read(reader: &mut dyn ser::Reader) -> Result<Context, ser::Error> {
|
||||
let data = reader.read_bytes_len_prefix()?;
|
||||
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
|
||||
}
|
||||
}
|
||||
|
||||
/// Block Identifier
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct BlockIdentifier(pub Hash);
|
||||
|
||||
impl BlockIdentifier {
|
||||
/// return hash
|
||||
pub fn hash(&self) -> Hash {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// convert to hex string
|
||||
pub fn from_hex(hex: &str) -> Result<BlockIdentifier, Error> {
|
||||
let hash =
|
||||
Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex".to_owned()))?;
|
||||
Ok(BlockIdentifier(hash))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::Serialize for BlockIdentifier {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_hex())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::de::Deserialize<'de> for BlockIdentifier {
|
||||
fn deserialize<D>(deserializer: D) -> Result<BlockIdentifier, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(BlockIdentifierVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct BlockIdentifierVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor {
|
||||
type Value = BlockIdentifier;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a block hash")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let block_hash = Hash::from_hex(s).unwrap();
|
||||
Ok(BlockIdentifier(block_hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fees in block to use for coinbase amount calculation
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BlockFees {
|
||||
/// fees
|
||||
pub fees: u64,
|
||||
/// height
|
||||
pub height: u64,
|
||||
/// key id
|
||||
pub key_id: Option<Identifier>,
|
||||
}
|
||||
|
||||
impl BlockFees {
|
||||
/// return key id
|
||||
pub fn key_id(&self) -> Option<Identifier> {
|
||||
self.key_id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Response to build a coinbase output.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct CbData {
|
||||
/// Output
|
||||
pub output: String,
|
||||
/// Kernel
|
||||
pub kernel: String,
|
||||
/// Key Id
|
||||
pub key_id: String,
|
||||
}
|
||||
|
||||
/// a contained wallet info struct, so automated tests can parse wallet info
|
||||
/// can add more fields here over time as needed
|
||||
#[derive(Serialize, Eq, PartialEq, Deserialize, Debug, Clone)]
|
||||
pub struct WalletInfo {
|
||||
/// height from which info was taken
|
||||
pub last_confirmed_height: u64,
|
||||
/// Minimum number of confirmations for an output to be treated as "spendable".
|
||||
pub minimum_confirmations: u64,
|
||||
/// total amount in the wallet
|
||||
pub total: u64,
|
||||
/// amount awaiting confirmation
|
||||
pub amount_awaiting_confirmation: u64,
|
||||
/// coinbases waiting for lock height
|
||||
pub amount_immature: u64,
|
||||
/// amount currently spendable
|
||||
pub amount_currently_spendable: u64,
|
||||
/// amount locked via previous transactions
|
||||
pub amount_locked: u64,
|
||||
}
|
||||
|
||||
/// Types of transactions that can be contained within a TXLog entry
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
pub enum TxLogEntryType {
|
||||
/// A coinbase transaction becomes confirmed
|
||||
ConfirmedCoinbase,
|
||||
/// Outputs created when a transaction is received
|
||||
TxReceived,
|
||||
/// Inputs locked + change outputs when a transaction is created
|
||||
TxSent,
|
||||
/// Received transaction that was rolled back by user
|
||||
TxReceivedCancelled,
|
||||
/// Sent transaction that was rolled back by user
|
||||
TxSentCancelled,
|
||||
}
|
||||
|
||||
impl fmt::Display for TxLogEntryType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed \nCoinbase"),
|
||||
TxLogEntryType::TxReceived => write!(f, "Received Tx"),
|
||||
TxLogEntryType::TxSent => write!(f, "Sent Tx"),
|
||||
TxLogEntryType::TxReceivedCancelled => write!(f, "Received Tx\n- Cancelled"),
|
||||
TxLogEntryType::TxSentCancelled => write!(f, "Sent Tx\n- Cancelled"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional transaction information, recorded when an event happens
|
||||
/// to add or remove funds from a wallet. One Transaction log entry
|
||||
/// maps to one or many outputs
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct TxLogEntry {
|
||||
/// BIP32 account path used for creating this tx
|
||||
pub parent_key_id: Identifier,
|
||||
/// Local id for this transaction (distinct from a slate transaction id)
|
||||
pub id: u32,
|
||||
/// Slate transaction this entry is associated with, if any
|
||||
pub tx_slate_id: Option<Uuid>,
|
||||
/// Transaction type (as above)
|
||||
pub tx_type: TxLogEntryType,
|
||||
/// Time this tx entry was created
|
||||
/// #[serde(with = "tx_date_format")]
|
||||
pub creation_ts: DateTime<Utc>,
|
||||
/// Time this tx was confirmed (by this wallet)
|
||||
/// #[serde(default, with = "opt_tx_date_format")]
|
||||
pub confirmation_ts: Option<DateTime<Utc>>,
|
||||
/// Whether the inputs+outputs involved in this transaction have been
|
||||
/// confirmed (In all cases either all outputs involved in a tx should be
|
||||
/// confirmed, or none should be; otherwise there's a deeper problem)
|
||||
pub confirmed: bool,
|
||||
/// number of inputs involved in TX
|
||||
pub num_inputs: usize,
|
||||
/// number of outputs involved in TX
|
||||
pub num_outputs: usize,
|
||||
/// Amount credited via this transaction
|
||||
pub amount_credited: u64,
|
||||
/// Amount debited via this transaction
|
||||
pub amount_debited: u64,
|
||||
/// Fee
|
||||
pub fee: Option<u64>,
|
||||
/// Message data, stored as json
|
||||
pub messages: Option<ParticipantMessages>,
|
||||
/// Location of the store transaction, (reference or resending)
|
||||
pub stored_tx: Option<String>,
|
||||
}
|
||||
|
||||
impl ser::Writeable for TxLogEntry {
|
||||
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Readable for TxLogEntry {
|
||||
fn read(reader: &mut dyn ser::Reader) -> Result<TxLogEntry, ser::Error> {
|
||||
let data = reader.read_bytes_len_prefix()?;
|
||||
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
|
||||
}
|
||||
}
|
||||
|
||||
impl TxLogEntry {
|
||||
/// Return a new blank with TS initialised with next entry
|
||||
pub fn new(parent_key_id: Identifier, t: TxLogEntryType, id: u32) -> Self {
|
||||
TxLogEntry {
|
||||
parent_key_id: parent_key_id,
|
||||
tx_type: t,
|
||||
id: id,
|
||||
tx_slate_id: None,
|
||||
creation_ts: Utc::now(),
|
||||
confirmation_ts: None,
|
||||
confirmed: false,
|
||||
amount_credited: 0,
|
||||
amount_debited: 0,
|
||||
num_inputs: 0,
|
||||
num_outputs: 0,
|
||||
fee: None,
|
||||
messages: None,
|
||||
stored_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a vec of TX log entries, return credited + debited sums
|
||||
pub fn sum_confirmed(txs: &Vec<TxLogEntry>) -> (u64, u64) {
|
||||
txs.iter().fold((0, 0), |acc, tx| match tx.confirmed {
|
||||
true => (acc.0 + tx.amount_credited, acc.1 + tx.amount_debited),
|
||||
false => acc,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update confirmation TS with now
|
||||
pub fn update_confirmation_ts(&mut self) {
|
||||
self.confirmation_ts = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Map of named accounts to BIP32 paths
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AcctPathMapping {
|
||||
/// label used by user
|
||||
pub label: String,
|
||||
/// Corresponding parent BIP32 derivation path
|
||||
pub path: Identifier,
|
||||
}
|
||||
|
||||
impl ser::Writeable for AcctPathMapping {
|
||||
fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
|
||||
writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::Readable for AcctPathMapping {
|
||||
fn read(reader: &mut dyn ser::Reader) -> Result<AcctPathMapping, ser::Error> {
|
||||
let data = reader.read_bytes_len_prefix()?;
|
||||
serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dummy wrapper for the hex-encoded serialized transaction.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TxWrapper {
|
||||
/// hex representation of transaction
|
||||
pub tx_hex: String,
|
||||
}
|
||||
|
||||
/// Send TX API Args
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct SendTXArgs {
|
||||
/// amount to send
|
||||
pub amount: u64,
|
||||
/// minimum confirmations
|
||||
pub minimum_confirmations: u64,
|
||||
/// payment method
|
||||
pub method: String,
|
||||
/// destination url
|
||||
pub dest: String,
|
||||
/// Max number of outputs
|
||||
pub max_outputs: usize,
|
||||
/// Number of change outputs to generate
|
||||
pub num_change_outputs: usize,
|
||||
/// whether to use all outputs (combine)
|
||||
pub selection_strategy_is_use_all: bool,
|
||||
/// Optional message, that will be signed
|
||||
pub message: Option<String>,
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Grin configuration file output command
|
||||
use crate::config::{config, GlobalConfig, GlobalWalletConfig, GRIN_WALLET_DIR};
|
||||
use crate::core::global;
|
||||
use std::env;
|
||||
|
||||
/// Create a config file in the current directory
|
||||
pub fn config_command_server(chain_type: &global::ChainTypes, file_name: &str) {
|
||||
let mut default_config = GlobalConfig::for_chain(chain_type);
|
||||
let current_dir = env::current_dir().unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
let mut config_file_name = current_dir.clone();
|
||||
config_file_name.push(file_name);
|
||||
if config_file_name.exists() {
|
||||
panic!(
|
||||
"{} already exists in the current directory. Please remove it first",
|
||||
file_name
|
||||
);
|
||||
}
|
||||
default_config.update_paths(¤t_dir);
|
||||
default_config
|
||||
.write_to_file(config_file_name.to_str().unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
|
||||
println!(
|
||||
"{} file configured and created in current directory",
|
||||
file_name
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a config file in the current directory
|
||||
pub fn config_command_wallet(chain_type: &global::ChainTypes, file_name: &str) {
|
||||
let mut default_config = GlobalWalletConfig::for_chain(chain_type);
|
||||
let current_dir = env::current_dir().unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
let mut config_file_name = current_dir.clone();
|
||||
config_file_name.push(file_name);
|
||||
|
||||
let mut data_dir_name = current_dir.clone();
|
||||
data_dir_name.push(GRIN_WALLET_DIR);
|
||||
|
||||
if config_file_name.exists() && data_dir_name.exists() {
|
||||
panic!(
|
||||
"{} already exists in the target directory. Please remove it first",
|
||||
file_name
|
||||
);
|
||||
}
|
||||
|
||||
// just leave as is if file exists but there's no data dir
|
||||
if config_file_name.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
default_config.update_paths(¤t_dir);
|
||||
default_config
|
||||
.write_to_file(config_file_name.to_str().unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
|
||||
println!(
|
||||
"File {} configured and created",
|
||||
config_file_name.to_str().unwrap(),
|
||||
);
|
||||
|
||||
let mut api_secret_path = current_dir.clone();
|
||||
api_secret_path.push(config::API_SECRET_FILE_NAME);
|
||||
if !api_secret_path.exists() {
|
||||
config::init_api_secret(&api_secret_path).unwrap();
|
||||
} else {
|
||||
config::check_api_secret(&api_secret_path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod config;
|
||||
mod wallet;
|
||||
mod wallet_args;
|
||||
mod wallet_tests;
|
||||
|
||||
pub use self::config::{config_command_server, config_command_wallet};
|
||||
pub use self::wallet::{seed_exists, wallet_command};
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::cmd::wallet_args;
|
||||
use crate::config::GlobalWalletConfig;
|
||||
use clap::ArgMatches;
|
||||
use grin_wallet::{self, HTTPNodeClient, WalletConfig, WalletSeed};
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) {
|
||||
if let Err(_) = WalletSeed::from_file(&wallet_config, password) {
|
||||
WalletSeed::init_file(&wallet_config, 32, None, password)
|
||||
.expect("Failed to create wallet seed file.");
|
||||
};
|
||||
}
|
||||
|
||||
pub fn seed_exists(wallet_config: WalletConfig) -> bool {
|
||||
let mut data_file_dir = PathBuf::new();
|
||||
data_file_dir.push(wallet_config.data_file_dir);
|
||||
data_file_dir.push(grin_wallet::SEED_FILE);
|
||||
if data_file_dir.exists() {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wallet_command(wallet_args: &ArgMatches<'_>, config: GlobalWalletConfig) -> i32 {
|
||||
// just get defaults from the global config
|
||||
let wallet_config = config.members.unwrap().wallet;
|
||||
|
||||
// web wallet http server must be started from here
|
||||
// NB: Turned off for the time being
|
||||
/*let _ = match wallet_args.subcommand() {
|
||||
("web", Some(_)) => start_webwallet_server(),
|
||||
_ => {}
|
||||
};*/
|
||||
|
||||
let node_client = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, None);
|
||||
let res = wallet_args::wallet_command(wallet_args, wallet_config, node_client);
|
||||
|
||||
// we need to give log output a chance to catch up before exiting
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
if let Err(e) = res {
|
||||
println!("Wallet command failed: {}", e);
|
||||
1
|
||||
} else {
|
||||
println!(
|
||||
"Command '{}' completed successfully",
|
||||
wallet_args.subcommand().0
|
||||
);
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::api::TLSConfig;
|
||||
use crate::util::file::get_first_line;
|
||||
use crate::util::{Mutex, ZeroingString};
|
||||
/// Argument parsing and error handling for wallet commands
|
||||
use clap::ArgMatches;
|
||||
use failure::Fail;
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_wallet::{command, instantiate_wallet, NodeClient, WalletConfig, WalletInst, WalletSeed};
|
||||
use grin_wallet::{Error, ErrorKind};
|
||||
use linefeed::terminal::Signal;
|
||||
use linefeed::{Interface, ReadResult};
|
||||
use rpassword;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
// define what to do on argument error
|
||||
macro_rules! arg_parse {
|
||||
( $r:expr ) => {
|
||||
match $r {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
return Err(ErrorKind::ArgumentError(format!("{}", e)).into());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
/// Simple error definition, just so we can return errors from all commands
|
||||
/// and let the caller figure out what to do
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
|
||||
pub enum ParseError {
|
||||
#[fail(display = "Invalid Arguments: {}", _0)]
|
||||
ArgumentError(String),
|
||||
#[fail(display = "Parsing IO error: {}", _0)]
|
||||
IOError(String),
|
||||
#[fail(display = "User Cancelled")]
|
||||
CancelledError,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ParseError {
|
||||
fn from(e: std::io::Error) -> ParseError {
|
||||
ParseError::IOError(format!("{}", e))
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_password_stdout(prompt: &str) -> ZeroingString {
|
||||
ZeroingString::from(rpassword::prompt_password_stdout(prompt).unwrap())
|
||||
}
|
||||
|
||||
pub fn prompt_password(password: &Option<ZeroingString>) -> ZeroingString {
|
||||
match password {
|
||||
None => prompt_password_stdout("Password: "),
|
||||
Some(p) => p.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_password_confirm() -> ZeroingString {
|
||||
let mut first = ZeroingString::from("first");
|
||||
let mut second = ZeroingString::from("second");
|
||||
while first != second {
|
||||
first = prompt_password_stdout("Password: ");
|
||||
second = prompt_password_stdout("Confirm Password: ");
|
||||
}
|
||||
first
|
||||
}
|
||||
|
||||
fn prompt_replace_seed() -> Result<bool, ParseError> {
|
||||
let interface = Arc::new(Interface::new("replace_seed")?);
|
||||
interface.set_report_signal(Signal::Interrupt, true);
|
||||
interface.set_prompt("Replace seed? (y/n)> ")?;
|
||||
println!();
|
||||
println!("Existing wallet.seed file already exists. Continue?");
|
||||
println!("Continuing will back up your existing 'wallet.seed' file as 'wallet.seed.bak'");
|
||||
println!();
|
||||
loop {
|
||||
let res = interface.read_line()?;
|
||||
match res {
|
||||
ReadResult::Eof => return Ok(false),
|
||||
ReadResult::Signal(sig) => {
|
||||
if sig == Signal::Interrupt {
|
||||
interface.cancel_read_line()?;
|
||||
return Err(ParseError::CancelledError);
|
||||
}
|
||||
}
|
||||
ReadResult::Input(line) => match line.trim() {
|
||||
"Y" | "y" => return Ok(true),
|
||||
"N" | "n" => return Ok(false),
|
||||
_ => println!("Please respond y or n"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_recovery_phrase() -> Result<ZeroingString, ParseError> {
|
||||
let interface = Arc::new(Interface::new("recover")?);
|
||||
let mut phrase = ZeroingString::from("");
|
||||
interface.set_report_signal(Signal::Interrupt, true);
|
||||
interface.set_prompt("phrase> ")?;
|
||||
loop {
|
||||
println!("Please enter your recovery phrase:");
|
||||
let res = interface.read_line()?;
|
||||
match res {
|
||||
ReadResult::Eof => break,
|
||||
ReadResult::Signal(sig) => {
|
||||
if sig == Signal::Interrupt {
|
||||
interface.cancel_read_line()?;
|
||||
return Err(ParseError::CancelledError);
|
||||
}
|
||||
}
|
||||
ReadResult::Input(line) => {
|
||||
if WalletSeed::from_mnemonic(&line).is_ok() {
|
||||
phrase = ZeroingString::from(line);
|
||||
break;
|
||||
} else {
|
||||
println!();
|
||||
println!("Recovery word phrase is invalid.");
|
||||
println!();
|
||||
interface.set_buffer(&line)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(phrase)
|
||||
}
|
||||
|
||||
// instantiate wallet (needed by most functions)
|
||||
|
||||
pub fn inst_wallet(
|
||||
config: WalletConfig,
|
||||
g_args: &command::GlobalArgs,
|
||||
node_client: impl NodeClient + 'static,
|
||||
) -> Result<Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>, ParseError> {
|
||||
let passphrase = prompt_password(&g_args.password);
|
||||
let res = instantiate_wallet(config.clone(), node_client, &passphrase, &g_args.account);
|
||||
match res {
|
||||
Ok(p) => Ok(p),
|
||||
Err(e) => {
|
||||
let msg = {
|
||||
match e.kind() {
|
||||
ErrorKind::Encryption => {
|
||||
format!("Error decrypting wallet seed (check provided password)")
|
||||
}
|
||||
_ => format!("Error instantiating wallet: {}", e),
|
||||
}
|
||||
};
|
||||
Err(ParseError::ArgumentError(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parses a required value, or throws error with message otherwise
|
||||
fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, ParseError> {
|
||||
let arg = args.value_of(name);
|
||||
match arg {
|
||||
Some(ar) => Ok(ar),
|
||||
None => {
|
||||
let msg = format!("Value for argument '{}' is required in this context", name,);
|
||||
Err(ParseError::ArgumentError(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parses a number, or throws error with message otherwise
|
||||
fn parse_u64(arg: &str, name: &str) -> Result<u64, ParseError> {
|
||||
let val = arg.parse::<u64>();
|
||||
match val {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => {
|
||||
let msg = format!("Could not parse {} as a whole number. e={}", name, e);
|
||||
Err(ParseError::ArgumentError(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_global_args(
|
||||
config: &WalletConfig,
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::GlobalArgs, ParseError> {
|
||||
let account = parse_required(args, "account")?;
|
||||
let mut show_spent = false;
|
||||
if args.is_present("show_spent") {
|
||||
show_spent = true;
|
||||
}
|
||||
let node_api_secret = get_first_line(config.node_api_secret_path.clone());
|
||||
let password = match args.value_of("pass") {
|
||||
None => None,
|
||||
Some(p) => Some(ZeroingString::from(p)),
|
||||
};
|
||||
|
||||
let tls_conf = match config.tls_certificate_file.clone() {
|
||||
None => None,
|
||||
Some(file) => {
|
||||
let key = match config.tls_certificate_key.clone() {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
let msg = format!("Private key for certificate is not set");
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
};
|
||||
Some(TLSConfig::new(file, key))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(command::GlobalArgs {
|
||||
account: account.to_owned(),
|
||||
show_spent: show_spent,
|
||||
node_api_secret: node_api_secret,
|
||||
password: password,
|
||||
tls_conf: tls_conf,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_init_args(
|
||||
config: &WalletConfig,
|
||||
g_args: &command::GlobalArgs,
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::InitArgs, ParseError> {
|
||||
if let Err(e) = WalletSeed::seed_file_exists(config) {
|
||||
let msg = format!("Not creating wallet - {}", e.inner);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
let list_length = match args.is_present("short_wordlist") {
|
||||
false => 32,
|
||||
true => 16,
|
||||
};
|
||||
let recovery_phrase = match args.is_present("recover") {
|
||||
true => Some(prompt_recovery_phrase()?),
|
||||
false => None,
|
||||
};
|
||||
|
||||
if recovery_phrase.is_some() {
|
||||
println!("Please provide a new password for the recovered wallet");
|
||||
} else {
|
||||
println!("Please enter a password for your new wallet");
|
||||
}
|
||||
|
||||
let password = match g_args.password.clone() {
|
||||
Some(p) => p,
|
||||
None => prompt_password_confirm(),
|
||||
};
|
||||
|
||||
Ok(command::InitArgs {
|
||||
list_length: list_length,
|
||||
password: password,
|
||||
config: config.clone(),
|
||||
recovery_phrase: recovery_phrase,
|
||||
restore: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_recover_args(
|
||||
config: &WalletConfig,
|
||||
g_args: &command::GlobalArgs,
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::RecoverArgs, ParseError> {
|
||||
let (passphrase, recovery_phrase) = {
|
||||
match args.is_present("display") {
|
||||
true => (prompt_password(&g_args.password), None),
|
||||
false => {
|
||||
let cont = {
|
||||
if command::wallet_seed_exists(config).is_err() {
|
||||
prompt_replace_seed()?
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
if !cont {
|
||||
return Err(ParseError::CancelledError);
|
||||
}
|
||||
let phrase = prompt_recovery_phrase()?;
|
||||
println!("Please provide a new password for the recovered wallet");
|
||||
(prompt_password_confirm(), Some(phrase.to_owned()))
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(command::RecoverArgs {
|
||||
passphrase: passphrase,
|
||||
recovery_phrase: recovery_phrase,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_listen_args(
|
||||
config: &mut WalletConfig,
|
||||
g_args: &mut command::GlobalArgs,
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::ListenArgs, ParseError> {
|
||||
// listen args
|
||||
let pass = match g_args.password.clone() {
|
||||
Some(p) => Some(p.to_owned()),
|
||||
None => Some(prompt_password(&None)),
|
||||
};
|
||||
g_args.password = pass;
|
||||
if let Some(port) = args.value_of("port") {
|
||||
config.api_listen_port = port.parse().unwrap();
|
||||
}
|
||||
let method = parse_required(args, "method")?;
|
||||
Ok(command::ListenArgs {
|
||||
method: method.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_account_args(account_args: &ArgMatches) -> Result<command::AccountArgs, ParseError> {
|
||||
let create = match account_args.value_of("create") {
|
||||
None => None,
|
||||
Some(s) => Some(s.to_owned()),
|
||||
};
|
||||
Ok(command::AccountArgs { create: create })
|
||||
}
|
||||
|
||||
pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseError> {
|
||||
// amount
|
||||
let amount = parse_required(args, "amount")?;
|
||||
let amount = core::core::amount_from_hr_string(amount);
|
||||
let amount = match amount {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"Could not parse amount as a number with optional decimal point. e={}",
|
||||
e
|
||||
);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
};
|
||||
|
||||
// message
|
||||
let message = match args.is_present("message") {
|
||||
true => Some(args.value_of("message").unwrap().to_owned()),
|
||||
false => None,
|
||||
};
|
||||
|
||||
// minimum_confirmations
|
||||
let min_c = parse_required(args, "minimum_confirmations")?;
|
||||
let min_c = parse_u64(min_c, "minimum_confirmations")?;
|
||||
|
||||
// selection_strategy
|
||||
let selection_strategy = parse_required(args, "selection_strategy")?;
|
||||
|
||||
// estimate_selection_strategies
|
||||
let estimate_selection_strategies = args.is_present("estimate_selection_strategies");
|
||||
|
||||
// method
|
||||
let method = parse_required(args, "method")?;
|
||||
|
||||
// dest
|
||||
let dest = {
|
||||
if method == "self" {
|
||||
match args.value_of("dest") {
|
||||
Some(d) => d,
|
||||
None => "default",
|
||||
}
|
||||
} else {
|
||||
if !estimate_selection_strategies {
|
||||
parse_required(args, "dest")?
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
};
|
||||
if !estimate_selection_strategies
|
||||
&& method == "http"
|
||||
&& !dest.starts_with("http://")
|
||||
&& !dest.starts_with("https://")
|
||||
{
|
||||
let msg = format!(
|
||||
"HTTP Destination should start with http://: or https://: {}",
|
||||
dest,
|
||||
);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
|
||||
// change_outputs
|
||||
let change_outputs = parse_required(args, "change_outputs")?;
|
||||
let change_outputs = parse_u64(change_outputs, "change_outputs")? as usize;
|
||||
|
||||
// fluff
|
||||
let fluff = args.is_present("fluff");
|
||||
|
||||
// max_outputs
|
||||
let max_outputs = 500;
|
||||
|
||||
Ok(command::SendArgs {
|
||||
amount: amount,
|
||||
message: message,
|
||||
minimum_confirmations: min_c,
|
||||
selection_strategy: selection_strategy.to_owned(),
|
||||
estimate_selection_strategies,
|
||||
method: method.to_owned(),
|
||||
dest: dest.to_owned(),
|
||||
change_outputs: change_outputs,
|
||||
fluff: fluff,
|
||||
max_outputs: max_outputs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_receive_args(receive_args: &ArgMatches) -> Result<command::ReceiveArgs, ParseError> {
|
||||
// message
|
||||
let message = match receive_args.is_present("message") {
|
||||
true => Some(receive_args.value_of("message").unwrap().to_owned()),
|
||||
false => None,
|
||||
};
|
||||
|
||||
// input
|
||||
let tx_file = parse_required(receive_args, "input")?;
|
||||
|
||||
// validate input
|
||||
if !Path::new(&tx_file).is_file() {
|
||||
let msg = format!("File {} not found.", &tx_file);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
|
||||
Ok(command::ReceiveArgs {
|
||||
input: tx_file.to_owned(),
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_finalize_args(args: &ArgMatches) -> Result<command::FinalizeArgs, ParseError> {
|
||||
let fluff = args.is_present("fluff");
|
||||
let tx_file = parse_required(args, "input")?;
|
||||
|
||||
if !Path::new(&tx_file).is_file() {
|
||||
let msg = format!("File {} not found.", tx_file);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
Ok(command::FinalizeArgs {
|
||||
input: tx_file.to_owned(),
|
||||
fluff: fluff,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_info_args(args: &ArgMatches) -> Result<command::InfoArgs, ParseError> {
|
||||
// minimum_confirmations
|
||||
let mc = parse_required(args, "minimum_confirmations")?;
|
||||
let mc = parse_u64(mc, "minimum_confirmations")?;
|
||||
Ok(command::InfoArgs {
|
||||
minimum_confirmations: mc,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, ParseError> {
|
||||
let tx_id = match args.value_of("id") {
|
||||
None => None,
|
||||
Some(tx) => Some(parse_u64(tx, "id")? as u32),
|
||||
};
|
||||
Ok(command::TxsArgs { id: tx_id })
|
||||
}
|
||||
|
||||
pub fn parse_repost_args(args: &ArgMatches) -> Result<command::RepostArgs, ParseError> {
|
||||
let tx_id = match args.value_of("id") {
|
||||
None => None,
|
||||
Some(tx) => Some(parse_u64(tx, "id")? as u32),
|
||||
};
|
||||
|
||||
let fluff = args.is_present("fluff");
|
||||
let dump_file = match args.value_of("dumpfile") {
|
||||
None => None,
|
||||
Some(d) => Some(d.to_owned()),
|
||||
};
|
||||
|
||||
Ok(command::RepostArgs {
|
||||
id: tx_id.unwrap(),
|
||||
dump_file: dump_file,
|
||||
fluff: fluff,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_cancel_args(args: &ArgMatches) -> Result<command::CancelArgs, ParseError> {
|
||||
let mut tx_id_string = "";
|
||||
let tx_id = match args.value_of("id") {
|
||||
None => None,
|
||||
Some(tx) => Some(parse_u64(tx, "id")? as u32),
|
||||
};
|
||||
let tx_slate_id = match args.value_of("txid") {
|
||||
None => None,
|
||||
Some(tx) => match tx.parse() {
|
||||
Ok(t) => {
|
||||
tx_id_string = tx;
|
||||
Some(t)
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("Could not parse txid parameter. e={}", e);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
},
|
||||
};
|
||||
if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) {
|
||||
let msg = format!("'id' (-i) or 'txid' (-t) argument is required.");
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
Ok(command::CancelArgs {
|
||||
tx_id: tx_id,
|
||||
tx_slate_id: tx_slate_id,
|
||||
tx_id_string: tx_id_string.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn wallet_command(
|
||||
wallet_args: &ArgMatches,
|
||||
mut wallet_config: WalletConfig,
|
||||
mut node_client: impl NodeClient + 'static,
|
||||
) -> Result<String, Error> {
|
||||
if let Some(t) = wallet_config.chain_type.clone() {
|
||||
core::global::set_mining_mode(t);
|
||||
}
|
||||
|
||||
if wallet_args.is_present("external") {
|
||||
wallet_config.api_listen_interface = "0.0.0.0".to_string();
|
||||
}
|
||||
|
||||
if let Some(dir) = wallet_args.value_of("data_dir") {
|
||||
wallet_config.data_file_dir = dir.to_string().clone();
|
||||
}
|
||||
|
||||
if let Some(sa) = wallet_args.value_of("api_server_address") {
|
||||
wallet_config.check_node_api_http_addr = sa.to_string().clone();
|
||||
}
|
||||
|
||||
let global_wallet_args = arg_parse!(parse_global_args(&wallet_config, &wallet_args));
|
||||
|
||||
node_client.set_node_url(&wallet_config.check_node_api_http_addr);
|
||||
node_client.set_node_api_secret(global_wallet_args.node_api_secret.clone());
|
||||
|
||||
// closure to instantiate wallet as needed by each subcommand
|
||||
let inst_wallet = || {
|
||||
let res = inst_wallet(wallet_config.clone(), &global_wallet_args, node_client);
|
||||
res.unwrap_or_else(|e| {
|
||||
println!("{}", e);
|
||||
std::process::exit(1);
|
||||
})
|
||||
};
|
||||
|
||||
let res = match wallet_args.subcommand() {
|
||||
("init", Some(args)) => {
|
||||
let a = arg_parse!(parse_init_args(&wallet_config, &global_wallet_args, &args));
|
||||
command::init(&global_wallet_args, a)
|
||||
}
|
||||
("recover", Some(args)) => {
|
||||
let a = arg_parse!(parse_recover_args(
|
||||
&wallet_config,
|
||||
&global_wallet_args,
|
||||
&args
|
||||
));
|
||||
command::recover(&wallet_config, a)
|
||||
}
|
||||
("listen", Some(args)) => {
|
||||
let mut c = wallet_config.clone();
|
||||
let mut g = global_wallet_args.clone();
|
||||
let a = arg_parse!(parse_listen_args(&mut c, &mut g, &args));
|
||||
command::listen(&wallet_config, &a, &g)
|
||||
}
|
||||
("owner_api", Some(_)) => {
|
||||
let mut g = global_wallet_args.clone();
|
||||
g.tls_conf = None;
|
||||
command::owner_api(inst_wallet(), &wallet_config, &g)
|
||||
}
|
||||
("web", Some(_)) => command::owner_api(inst_wallet(), &wallet_config, &global_wallet_args),
|
||||
("account", Some(args)) => {
|
||||
let a = arg_parse!(parse_account_args(&args));
|
||||
command::account(inst_wallet(), a)
|
||||
}
|
||||
("send", Some(args)) => {
|
||||
let a = arg_parse!(parse_send_args(&args));
|
||||
command::send(
|
||||
inst_wallet(),
|
||||
a,
|
||||
wallet_config.dark_background_color_scheme.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
("receive", Some(args)) => {
|
||||
let a = arg_parse!(parse_receive_args(&args));
|
||||
command::receive(inst_wallet(), &global_wallet_args, a)
|
||||
}
|
||||
("finalize", Some(args)) => {
|
||||
let a = arg_parse!(parse_finalize_args(&args));
|
||||
command::finalize(inst_wallet(), a)
|
||||
}
|
||||
("info", Some(args)) => {
|
||||
let a = arg_parse!(parse_info_args(&args));
|
||||
command::info(
|
||||
inst_wallet(),
|
||||
&global_wallet_args,
|
||||
a,
|
||||
wallet_config.dark_background_color_scheme.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
("outputs", Some(_)) => command::outputs(
|
||||
inst_wallet(),
|
||||
&global_wallet_args,
|
||||
wallet_config.dark_background_color_scheme.unwrap_or(true),
|
||||
),
|
||||
("txs", Some(args)) => {
|
||||
let a = arg_parse!(parse_txs_args(&args));
|
||||
command::txs(
|
||||
inst_wallet(),
|
||||
&global_wallet_args,
|
||||
a,
|
||||
wallet_config.dark_background_color_scheme.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
("repost", Some(args)) => {
|
||||
let a = arg_parse!(parse_repost_args(&args));
|
||||
command::repost(inst_wallet(), a)
|
||||
}
|
||||
("cancel", Some(args)) => {
|
||||
let a = arg_parse!(parse_cancel_args(&args));
|
||||
command::cancel(inst_wallet(), a)
|
||||
}
|
||||
("restore", Some(_)) => command::restore(inst_wallet()),
|
||||
("check", Some(_)) => command::check_repair(inst_wallet()),
|
||||
_ => {
|
||||
let msg = format!("Unknown wallet command, use 'grin help wallet' for details");
|
||||
return Err(ErrorKind::ArgumentError(msg).into());
|
||||
}
|
||||
};
|
||||
if let Err(e) = res {
|
||||
Err(e)
|
||||
} else {
|
||||
Ok(wallet_args.subcommand().0.to_owned())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test wallet command line works as expected
|
||||
#[cfg(test)]
|
||||
mod wallet_tests {
|
||||
use clap;
|
||||
use grin_util as util;
|
||||
use grin_wallet;
|
||||
|
||||
use grin_wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
|
||||
use clap::{App, ArgMatches};
|
||||
use grin_util::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::{env, fs};
|
||||
|
||||
use grin_config::GlobalWalletConfig;
|
||||
use grin_core::global;
|
||||
use grin_core::global::ChainTypes;
|
||||
use grin_keychain::ExtKeychain;
|
||||
use grin_wallet::{LMDBBackend, WalletBackend, WalletConfig, WalletInst, WalletSeed};
|
||||
|
||||
use super::super::wallet_args;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// Create a wallet config file in the given current directory
|
||||
pub fn config_command_wallet(
|
||||
dir_name: &str,
|
||||
wallet_name: &str,
|
||||
) -> Result<(), grin_wallet::Error> {
|
||||
let mut current_dir;
|
||||
let mut default_config = GlobalWalletConfig::default();
|
||||
current_dir = env::current_dir().unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
current_dir.push(dir_name);
|
||||
current_dir.push(wallet_name);
|
||||
let _ = fs::create_dir_all(current_dir.clone());
|
||||
let mut config_file_name = current_dir.clone();
|
||||
config_file_name.push("grin-wallet.toml");
|
||||
if config_file_name.exists() {
|
||||
return Err(grin_wallet::ErrorKind::ArgumentError(
|
||||
"grin-wallet.toml already exists in the target directory. Please remove it first"
|
||||
.to_owned(),
|
||||
))?;
|
||||
}
|
||||
default_config.update_paths(¤t_dir);
|
||||
default_config
|
||||
.write_to_file(config_file_name.to_str().unwrap())
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
|
||||
println!(
|
||||
"File {} configured and created",
|
||||
config_file_name.to_str().unwrap(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles setup and detection of paths for wallet
|
||||
pub fn initial_setup_wallet(dir_name: &str, wallet_name: &str) -> WalletConfig {
|
||||
let mut current_dir;
|
||||
current_dir = env::current_dir().unwrap_or_else(|e| {
|
||||
panic!("Error creating config file: {}", e);
|
||||
});
|
||||
current_dir.push(dir_name);
|
||||
current_dir.push(wallet_name);
|
||||
let _ = fs::create_dir_all(current_dir.clone());
|
||||
let mut config_file_name = current_dir.clone();
|
||||
config_file_name.push("grin-wallet.toml");
|
||||
GlobalWalletConfig::new(config_file_name.to_str().unwrap())
|
||||
.unwrap()
|
||||
.members
|
||||
.unwrap()
|
||||
.wallet
|
||||
}
|
||||
|
||||
fn get_wallet_subcommand<'a>(
|
||||
wallet_dir: &str,
|
||||
wallet_name: &str,
|
||||
args: ArgMatches<'a>,
|
||||
) -> ArgMatches<'a> {
|
||||
match args.subcommand() {
|
||||
("wallet", Some(wallet_args)) => {
|
||||
// wallet init command should spit out its config file then continue
|
||||
// (if desired)
|
||||
if let ("init", Some(init_args)) = wallet_args.subcommand() {
|
||||
if init_args.is_present("here") {
|
||||
let _ = config_command_wallet(wallet_dir, wallet_name);
|
||||
}
|
||||
}
|
||||
wallet_args.to_owned()
|
||||
}
|
||||
_ => ArgMatches::new(),
|
||||
}
|
||||
}
|
||||
//
|
||||
// Helper to create an instance of the LMDB wallet
|
||||
fn instantiate_wallet(
|
||||
mut wallet_config: WalletConfig,
|
||||
node_client: LocalWalletClient,
|
||||
passphrase: &str,
|
||||
account: &str,
|
||||
) -> Result<Arc<Mutex<WalletInst<LocalWalletClient, ExtKeychain>>>, grin_wallet::Error> {
|
||||
wallet_config.chain_type = None;
|
||||
// First test decryption, so we can abort early if we have the wrong password
|
||||
let _ = WalletSeed::from_file(&wallet_config, passphrase)?;
|
||||
let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, node_client)?;
|
||||
db_wallet.set_parent_key_id_by_name(account)?;
|
||||
info!("Using LMDB Backend for wallet");
|
||||
Ok(Arc::new(Mutex::new(db_wallet)))
|
||||
}
|
||||
|
||||
fn execute_command(
|
||||
app: &App,
|
||||
test_dir: &str,
|
||||
wallet_name: &str,
|
||||
client: &LocalWalletClient,
|
||||
arg_vec: Vec<&str>,
|
||||
) -> Result<String, grin_wallet::Error> {
|
||||
let args = app.clone().get_matches_from(arg_vec);
|
||||
let args = get_wallet_subcommand(test_dir, wallet_name, args.clone());
|
||||
let mut config = initial_setup_wallet(test_dir, wallet_name);
|
||||
//unset chain type so it doesn't get reset
|
||||
config.chain_type = None;
|
||||
wallet_args::wallet_command(&args, config.clone(), client.clone())
|
||||
}
|
||||
|
||||
/// command line tests
|
||||
fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> =
|
||||
WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// load app yaml. If it don't exist, just say so and exit
|
||||
let yml = load_yaml!("../grin.yml");
|
||||
let app = App::from_yaml(yml);
|
||||
|
||||
// wallet init
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "init", "-h"];
|
||||
// should create new wallet file
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?;
|
||||
|
||||
// trying to init twice - should fail
|
||||
assert!(execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone()).is_err());
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
|
||||
// add wallet to proxy
|
||||
//let wallet1 = test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone());
|
||||
let config1 = initial_setup_wallet(test_dir, "wallet1");
|
||||
let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?;
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
// Create wallet 2
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?;
|
||||
|
||||
let config2 = initial_setup_wallet(test_dir, "wallet2");
|
||||
let wallet2 = instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?;
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Create some accounts in wallet 1
|
||||
let arg_vec = vec![
|
||||
"grin", "wallet", "-p", "password", "account", "-c", "mining",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"account",
|
||||
"-c",
|
||||
"account_1",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// Create some accounts in wallet 2
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"account",
|
||||
"-c",
|
||||
"account_1",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?;
|
||||
// already exists
|
||||
assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err());
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"account",
|
||||
"-c",
|
||||
"account_2",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
// let's see those accounts
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "account"];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
// let's see those accounts
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "account"];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
// Mine a bit into wallet 1 so we have something to send
|
||||
// (TODO: Be able to stop listeners so we can test this better)
|
||||
let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?;
|
||||
grin_wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.set_active_account("mining")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let mut bh = 10u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize);
|
||||
|
||||
let very_long_message = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef\
|
||||
This part should all be truncated";
|
||||
|
||||
// Update info and check
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "info"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// try a file exchange
|
||||
let file_name = format!("{}/tx1.part_tx", test_dir);
|
||||
let response_file_name = format!("{}/tx1.part_tx.response", test_dir);
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"send",
|
||||
"-m",
|
||||
"file",
|
||||
"-d",
|
||||
&file_name,
|
||||
"-g",
|
||||
very_long_message,
|
||||
"10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"account_1",
|
||||
"receive",
|
||||
"-i",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Thanks, Yeast!",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?;
|
||||
|
||||
// shouldn't be allowed to receive twice
|
||||
assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err());
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"finalize",
|
||||
"-i",
|
||||
&response_file_name,
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
bh += 1;
|
||||
|
||||
let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?;
|
||||
|
||||
// Check our transaction log, should have 10 entries
|
||||
grin_wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.set_active_account("mining")?;
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(txs.len(), bh as usize);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10);
|
||||
bh += 10;
|
||||
|
||||
// update info for each
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "info"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"account_1",
|
||||
"info",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client1, arg_vec)?;
|
||||
|
||||
// check results in wallet 2
|
||||
let wallet2 = instantiate_wallet(config2.clone(), client2.clone(), "password", "default")?;
|
||||
grin_wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
api.set_active_account("account_1")?;
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, 10_000_000_000);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Self-send to same account, using smallest strategy
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"send",
|
||||
"-m",
|
||||
"file",
|
||||
"-d",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Love, Yeast, Smallest",
|
||||
"-s",
|
||||
"smallest",
|
||||
"10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"receive",
|
||||
"-i",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Thanks, Yeast!",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec.clone())?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"finalize",
|
||||
"-i",
|
||||
&response_file_name,
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
bh += 1;
|
||||
|
||||
// Check our transaction log, should have bh entries + one for the self receive
|
||||
let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?;
|
||||
|
||||
grin_wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.set_active_account("mining")?;
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(txs.len(), bh as usize + 1);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Try using the self-send method, splitting up outputs for the fun of it
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"send",
|
||||
"-m",
|
||||
"self",
|
||||
"-d",
|
||||
"mining",
|
||||
"-g",
|
||||
"Self love",
|
||||
"-o",
|
||||
"3",
|
||||
"-s",
|
||||
"smallest",
|
||||
"10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
bh += 1;
|
||||
|
||||
// Check our transaction log, should have bh entries + 2 for the self receives
|
||||
let wallet1 = instantiate_wallet(config1.clone(), client1.clone(), "password", "default")?;
|
||||
|
||||
grin_wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.set_active_account("mining")?;
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(txs.len(), bh as usize + 2);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Another file exchange, don't send, but unlock with repair command
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"send",
|
||||
"-m",
|
||||
"file",
|
||||
"-d",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Ain't sending",
|
||||
"10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "check"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// Another file exchange, cancel this time
|
||||
let arg_vec = vec![
|
||||
"grin",
|
||||
"wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"-a",
|
||||
"mining",
|
||||
"send",
|
||||
"-m",
|
||||
"file",
|
||||
"-d",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Ain't sending 2",
|
||||
"10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec![
|
||||
"grin", "wallet", "-p", "password", "-a", "mining", "cancel", "-i", "26",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// txs and outputs (mostly spit out for a visual in test logs)
|
||||
let arg_vec = vec!["grin", "wallet", "-p", "password", "-a", "mining", "txs"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// message output (mostly spit out for a visual in test logs)
|
||||
let arg_vec = vec![
|
||||
"grin", "wallet", "-p", "password", "-a", "mining", "txs", "-i", "10",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// txs and outputs (mostly spit out for a visual in test logs)
|
||||
let arg_vec = vec![
|
||||
"grin", "wallet", "-p", "password", "-a", "mining", "outputs",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_command_line() {
|
||||
let test_dir = "target/test_output/command_line";
|
||||
if let Err(e) = command_line_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Main for building the binary of a Grin peer-to-peer node.
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use crate::config::config::{SERVER_CONFIG_FILE_NAME, WALLET_CONFIG_FILE_NAME};
|
||||
use crate::core::global;
|
||||
use crate::util::init_logger;
|
||||
use clap::App;
|
||||
use grin_api as api;
|
||||
use grin_config as config;
|
||||
use grin_core as core;
|
||||
use grin_p2p as p2p;
|
||||
use grin_servers as servers;
|
||||
use grin_util as util;
|
||||
use std::process::exit;
|
||||
|
||||
mod cmd;
|
||||
|
||||
// include build information
|
||||
pub mod built_info {
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
pub fn info_strings() -> (String, String) {
|
||||
(
|
||||
format!(
|
||||
"This is Grin version {}{}, built for {} by {}.",
|
||||
built_info::PKG_VERSION,
|
||||
built_info::GIT_VERSION.map_or_else(|| "".to_owned(), |v| format!(" (git {})", v)),
|
||||
built_info::TARGET,
|
||||
built_info::RUSTC_VERSION,
|
||||
)
|
||||
.to_string(),
|
||||
format!(
|
||||
"Built with profile \"{}\", features \"{}\".",
|
||||
built_info::PROFILE,
|
||||
built_info::FEATURES_STR,
|
||||
)
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn log_build_info() {
|
||||
let (basic_info, detailed_info) = info_strings();
|
||||
info!("{}", basic_info);
|
||||
debug!("{}", detailed_info);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let exit_code = real_main();
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn real_main() -> i32 {
|
||||
let yml = load_yaml!("grin-wallet.yml");
|
||||
let args = App::from_yaml(yml).get_matches();
|
||||
let mut wallet_config = None;
|
||||
let mut node_config = None;
|
||||
|
||||
let chain_type = if args.is_present("floonet") {
|
||||
global::ChainTypes::Floonet
|
||||
} else if args.is_present("usernet") {
|
||||
global::ChainTypes::UserTesting
|
||||
} else {
|
||||
global::ChainTypes::Mainnet
|
||||
};
|
||||
|
||||
// Deal with configuration file creation
|
||||
match args.subcommand() {
|
||||
("wallet", Some(wallet_args)) => {
|
||||
// wallet init command should spit out its config file then continue
|
||||
// (if desired)
|
||||
if let ("init", Some(init_args)) = wallet_args.subcommand() {
|
||||
if init_args.is_present("here") {
|
||||
cmd::config_command_wallet(&chain_type, WALLET_CONFIG_FILE_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Load relevant config
|
||||
match args.subcommand() {
|
||||
// If it's a wallet command, try and load a wallet config file
|
||||
("wallet", Some(wallet_args)) => {
|
||||
let mut w = config::initial_setup_wallet(&chain_type).unwrap_or_else(|e| {
|
||||
panic!("Error loading wallet configuration: {}", e);
|
||||
});
|
||||
if !cmd::seed_exists(w.members.as_ref().unwrap().wallet.clone()) {
|
||||
if "init" == wallet_args.subcommand().0 || "recover" == wallet_args.subcommand().0 {
|
||||
} else {
|
||||
println!("Wallet seed file doesn't exist. Run `grin wallet init` first");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
let mut l = w.members.as_mut().unwrap().logging.clone().unwrap();
|
||||
l.tui_running = Some(false);
|
||||
init_logger(Some(l));
|
||||
info!(
|
||||
"Using wallet configuration file at {}",
|
||||
w.config_file_path.as_ref().unwrap().to_str().unwrap()
|
||||
);
|
||||
wallet_config = Some(w);
|
||||
}
|
||||
// When the subscommand is 'server' take into account the 'config_file' flag
|
||||
("server", Some(server_args)) => {
|
||||
if let Some(_path) = server_args.value_of("config_file") {
|
||||
node_config = Some(config::GlobalConfig::new(_path).unwrap_or_else(|e| {
|
||||
panic!("Error loading server configuration: {}", e);
|
||||
}));
|
||||
} else {
|
||||
node_config = Some(
|
||||
config::initial_setup_server(&chain_type).unwrap_or_else(|e| {
|
||||
panic!("Error loading server configuration: {}", e);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Otherwise load up the node config as usual
|
||||
_ => {
|
||||
node_config = Some(
|
||||
config::initial_setup_server(&chain_type).unwrap_or_else(|e| {
|
||||
panic!("Error loading server configuration: {}", e);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut config) = node_config.clone() {
|
||||
let mut l = config.members.as_mut().unwrap().logging.clone().unwrap();
|
||||
let run_tui = config.members.as_mut().unwrap().server.run_tui;
|
||||
if let Some(true) = run_tui {
|
||||
l.log_to_stdout = false;
|
||||
l.tui_running = Some(true);
|
||||
}
|
||||
init_logger(Some(l));
|
||||
|
||||
global::set_mining_mode(config.members.unwrap().server.clone().chain_type);
|
||||
|
||||
if let Some(file_path) = &config.config_file_path {
|
||||
info!(
|
||||
"Using configuration file at {}",
|
||||
file_path.to_str().unwrap()
|
||||
);
|
||||
} else {
|
||||
info!("Node configuration file not found, using default");
|
||||
}
|
||||
}
|
||||
|
||||
log_build_info();
|
||||
|
||||
// Execute subcommand
|
||||
match args.subcommand() {
|
||||
// server commands and options
|
||||
("server", Some(server_args)) => {
|
||||
cmd::server_command(Some(server_args), node_config.unwrap())
|
||||
}
|
||||
|
||||
// client commands and options
|
||||
("client", Some(client_args)) => cmd::client_command(client_args, node_config.unwrap()),
|
||||
|
||||
// client commands and options
|
||||
("wallet", Some(wallet_args)) => cmd::wallet_command(wallet_args, wallet_config.unwrap()),
|
||||
|
||||
// If nothing is specified, try to just use the config file instead
|
||||
// this could possibly become the way to configure most things
|
||||
// with most command line options being phased out
|
||||
_ => cmd::server_command(None, node_config.unwrap()),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
name: grin
|
||||
version: "1.0.1"
|
||||
about: Lightweight implementation of the MimbleWimble protocol.
|
||||
author: The Grin Team
|
||||
|
||||
args:
|
||||
- floonet:
|
||||
help: Run grin against the Floonet (as opposed to mainnet)
|
||||
long: floonet
|
||||
takes_value: false
|
||||
- usernet:
|
||||
help: Run grin as a local-only network. Doesn't block peer connections but will not connect to any peer or seed
|
||||
long: usernet
|
||||
takes_value: false
|
||||
subcommands:
|
||||
- server:
|
||||
about: Control the Grin server
|
||||
args:
|
||||
- config_file:
|
||||
help: Path to a grin-server.toml configuration file
|
||||
short: c
|
||||
long: config_file
|
||||
takes_value: true
|
||||
- port:
|
||||
help: Port to start the P2P server on
|
||||
short: p
|
||||
long: port
|
||||
takes_value: true
|
||||
- api_port:
|
||||
help: Port on which to start the api server (e.g. transaction pool api)
|
||||
short: api
|
||||
long: api_port
|
||||
takes_value: true
|
||||
- seed:
|
||||
help: Override seed node(s) to connect to
|
||||
short: s
|
||||
long: seed
|
||||
takes_value: true
|
||||
- wallet_url:
|
||||
help: The wallet listener to which mining rewards will be sent
|
||||
short: w
|
||||
long: wallet_url
|
||||
takes_value: true
|
||||
subcommands:
|
||||
- config:
|
||||
about: Generate a configuration grin-server.toml file in the current directory
|
||||
- run:
|
||||
about: Run the Grin server in this console
|
||||
- client:
|
||||
about: Communicates with the Grin server
|
||||
subcommands:
|
||||
- status:
|
||||
about: Current status of the Grin chain
|
||||
- listconnectedpeers:
|
||||
about: Print a list of currently connected peers
|
||||
- ban:
|
||||
about: Ban peer
|
||||
args:
|
||||
- peer:
|
||||
help: Peer ip and port (e.g. 10.12.12.13:13414)
|
||||
short: p
|
||||
long: peer
|
||||
required: true
|
||||
takes_value: true
|
||||
- unban:
|
||||
about: Unban peer
|
||||
args:
|
||||
- peer:
|
||||
help: Peer ip and port (e.g. 10.12.12.13:13414)
|
||||
short: p
|
||||
long: peer
|
||||
required: true
|
||||
takes_value: true
|
||||
- wallet:
|
||||
about: Wallet software for Grin
|
||||
args:
|
||||
- pass:
|
||||
help: Wallet passphrase used to encrypt wallet seed
|
||||
short: p
|
||||
long: pass
|
||||
takes_value: true
|
||||
- account:
|
||||
help: Wallet account to use for this operation
|
||||
short: a
|
||||
long: account
|
||||
takes_value: true
|
||||
default_value: default
|
||||
- data_dir:
|
||||
help: Directory in which to store wallet files
|
||||
short: dd
|
||||
long: data_dir
|
||||
takes_value: true
|
||||
- external:
|
||||
help: Listen on 0.0.0.0 interface to allow external connections (default is 127.0.0.1)
|
||||
short: e
|
||||
long: external
|
||||
takes_value: false
|
||||
- show_spent:
|
||||
help: Show spent outputs on wallet output commands
|
||||
short: s
|
||||
long: show_spent
|
||||
takes_value: false
|
||||
- api_server_address:
|
||||
help: Api address of running node on which to check inputs and post transactions
|
||||
short: r
|
||||
long: api_server_address
|
||||
takes_value: true
|
||||
subcommands:
|
||||
- account:
|
||||
about: List wallet accounts or create a new account
|
||||
args:
|
||||
- create:
|
||||
help: Create a new wallet account with provided name
|
||||
short: c
|
||||
long: create
|
||||
takes_value: true
|
||||
- listen:
|
||||
about: Runs the wallet in listening mode waiting for transactions
|
||||
args:
|
||||
- port:
|
||||
help: Port on which to run the wallet listener
|
||||
short: l
|
||||
long: port
|
||||
takes_value: true
|
||||
- method:
|
||||
help: Which method to use for communication
|
||||
short: m
|
||||
long: method
|
||||
possible_values:
|
||||
- http
|
||||
- keybase
|
||||
default_value: http
|
||||
takes_value: true
|
||||
- owner_api:
|
||||
about: Runs the wallet's local web API
|
||||
# Turned off, for now
|
||||
# - web:
|
||||
# about: Runs the local web wallet which can be accessed through a browser
|
||||
- send:
|
||||
about: Builds a transaction to send coins and sends to the specified listener directly
|
||||
args:
|
||||
- amount:
|
||||
help: Number of coins to send with optional fraction, e.g. 12.423
|
||||
index: 1
|
||||
- minimum_confirmations:
|
||||
help: Minimum number of confirmations required for an output to be spendable
|
||||
short: c
|
||||
long: min_conf
|
||||
default_value: "10"
|
||||
takes_value: true
|
||||
- selection_strategy:
|
||||
help: Coin/Output selection strategy.
|
||||
short: s
|
||||
long: selection
|
||||
possible_values:
|
||||
- all
|
||||
- smallest
|
||||
default_value: all
|
||||
takes_value: true
|
||||
- estimate_selection_strategies:
|
||||
help: Estimates all possible Coin/Output selection strategies.
|
||||
short: e
|
||||
long: estimate-selection
|
||||
- change_outputs:
|
||||
help: Number of change outputs to generate (mainly for testing)
|
||||
short: o
|
||||
long: change_outputs
|
||||
default_value: "1"
|
||||
takes_value: true
|
||||
- method:
|
||||
help: Method for sending this transaction
|
||||
short: m
|
||||
long: method
|
||||
possible_values:
|
||||
- http
|
||||
- file
|
||||
- self
|
||||
- keybase
|
||||
default_value: http
|
||||
takes_value: true
|
||||
- dest:
|
||||
help: Send the transaction to the provided server (start with http://) or save as file.
|
||||
short: d
|
||||
long: dest
|
||||
takes_value: true
|
||||
- fluff:
|
||||
help: Fluff the transaction (ignore Dandelion relay protocol)
|
||||
short: f
|
||||
long: fluff
|
||||
- message:
|
||||
help: Optional participant message to include
|
||||
short: g
|
||||
long: message
|
||||
takes_value: true
|
||||
- stored_tx:
|
||||
help: If present, use the previously stored Unconfirmed transaction with given id
|
||||
short: t
|
||||
long: stored_tx
|
||||
takes_value: true
|
||||
- receive:
|
||||
about: Processes a transaction file to accept a transfer from a sender
|
||||
args:
|
||||
- message:
|
||||
help: Optional participant message to include
|
||||
short: g
|
||||
long: message
|
||||
takes_value: true
|
||||
- input:
|
||||
help: Partial transaction to process, expects the sender's transaction file.
|
||||
short: i
|
||||
long: input
|
||||
takes_value: true
|
||||
- finalize:
|
||||
about: Processes a receiver's transaction file to finalize a transfer.
|
||||
args:
|
||||
- input:
|
||||
help: Partial transaction to process, expects the receiver's transaction file.
|
||||
short: i
|
||||
long: input
|
||||
takes_value: true
|
||||
- fluff:
|
||||
help: Fluff the transaction (ignore Dandelion relay protocol)
|
||||
short: f
|
||||
long: fluff
|
||||
- outputs:
|
||||
about: Raw wallet output info (list of outputs)
|
||||
- txs:
|
||||
about: Display transaction information
|
||||
args:
|
||||
- id:
|
||||
help: If specified, display transaction with given Id and all associated Inputs/Outputs
|
||||
short: i
|
||||
long: id
|
||||
takes_value: true
|
||||
- repost:
|
||||
about: Reposts a stored, completed but unconfirmed transaction to the chain, or dumps it to a file
|
||||
args:
|
||||
- id:
|
||||
help: Transaction ID containing the stored completed transaction
|
||||
short: i
|
||||
long: id
|
||||
takes_value: true
|
||||
- dumpfile:
|
||||
help: File name to duMp the transaction to instead of posting
|
||||
short: m
|
||||
long: dumpfile
|
||||
takes_value: true
|
||||
- fluff:
|
||||
help: Fluff the transaction (ignore Dandelion relay protocol)
|
||||
short: f
|
||||
long: fluff
|
||||
- cancel:
|
||||
about: Cancels an previously created transaction, freeing previously locked outputs for use again
|
||||
args:
|
||||
- id:
|
||||
help: The ID of the transaction to cancel
|
||||
short: i
|
||||
long: id
|
||||
takes_value: true
|
||||
- txid:
|
||||
help: The TxID UUID of the transaction to cancel
|
||||
short: t
|
||||
long: txid
|
||||
takes_value: true
|
||||
- info:
|
||||
about: Basic wallet contents summary
|
||||
args:
|
||||
- minimum_confirmations:
|
||||
help: Minimum number of confirmations required for an output to be spendable
|
||||
short: c
|
||||
long: min_conf
|
||||
default_value: "10"
|
||||
takes_value: true
|
||||
- init:
|
||||
about: Initialize a new wallet seed file and database
|
||||
args:
|
||||
- here:
|
||||
help: Create wallet files in the current directory instead of the default ~/.grin directory
|
||||
short: h
|
||||
long: here
|
||||
takes_value: false
|
||||
- short_wordlist:
|
||||
help: Generate a 12-word recovery phrase/seed instead of default 24
|
||||
short: s
|
||||
long: short_wordlist
|
||||
takes_value: false
|
||||
- recover:
|
||||
help: Initialize new wallet using a recovery phrase
|
||||
short: r
|
||||
long: recover
|
||||
takes_value: false
|
||||
- recover:
|
||||
about: Recover a wallet.seed file from a recovery phrase (default) or displays a recovery phrase for an existing seed file
|
||||
args:
|
||||
- display:
|
||||
help: Display wallet recovery phrase
|
||||
short: d
|
||||
long: display
|
||||
takes_value: false
|
||||
- restore:
|
||||
about: Restores a wallet contents from a seed file
|
||||
- check:
|
||||
about: Checks a wallet's outputs against a live node, repairing and restoring missing outputs if required
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// File Output 'plugin' implementation
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::adapters::util::{deserialize_slate, serialize_slate};
|
||||
use crate::libwallet::slate::Slate;
|
||||
use crate::libwallet::Error;
|
||||
use crate::{WalletCommAdapter, WalletConfig};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FileWalletCommAdapter {}
|
||||
|
||||
impl FileWalletCommAdapter {
|
||||
/// Create
|
||||
pub fn new() -> Box<dyn WalletCommAdapter> {
|
||||
Box::new(FileWalletCommAdapter {})
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletCommAdapter for FileWalletCommAdapter {
|
||||
fn supports_sync(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn send_tx_sync(&self, _dest: &str, _slate: &Slate) -> Result<Slate, Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn send_tx_async(&self, dest: &str, slate: &Slate) -> Result<(), Error> {
|
||||
let mut pub_tx = File::create(dest)?;
|
||||
let slate_string = serialize_slate(slate);
|
||||
pub_tx.write_all(slate_string.as_bytes())?;
|
||||
pub_tx.sync_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_tx_async(&self, params: &str) -> Result<Slate, Error> {
|
||||
let mut pub_tx_f = File::open(params)?;
|
||||
let mut content = String::new();
|
||||
pub_tx_f.read_to_string(&mut content)?;
|
||||
Ok(deserialize_slate(&content))
|
||||
}
|
||||
|
||||
fn listen(
|
||||
&self,
|
||||
_params: HashMap<String, String>,
|
||||
_config: WalletConfig,
|
||||
_passphrase: &str,
|
||||
_account: &str,
|
||||
_node_api_secret: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::adapters::util::get_versioned_slate;
|
||||
use crate::api;
|
||||
use crate::controller;
|
||||
use crate::libwallet::slate::{Slate, VersionedSlate};
|
||||
use crate::libwallet::{Error, ErrorKind};
|
||||
use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig};
|
||||
/// HTTP Wallet 'plugin' implementation
|
||||
use failure::ResultExt;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HTTPWalletCommAdapter {}
|
||||
|
||||
impl HTTPWalletCommAdapter {
|
||||
/// Create
|
||||
pub fn new() -> Box<dyn WalletCommAdapter> {
|
||||
Box::new(HTTPWalletCommAdapter {})
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletCommAdapter for HTTPWalletCommAdapter {
|
||||
fn supports_sync(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result<Slate, Error> {
|
||||
if &dest[..4] != "http" {
|
||||
let err_str = format!(
|
||||
"dest formatted as {} but send -d expected stdout or http://IP:port",
|
||||
dest
|
||||
);
|
||||
error!("{}", err_str,);
|
||||
Err(ErrorKind::Uri)?
|
||||
}
|
||||
let url = format!("{}/v1/wallet/foreign/receive_tx", dest);
|
||||
debug!("Posting transaction slate to {}", url);
|
||||
let slate = get_versioned_slate(slate);
|
||||
let res: Result<VersionedSlate, _> = api::client::post(url.as_str(), None, &slate);
|
||||
match res {
|
||||
Err(e) => {
|
||||
let report = format!("Posting transaction slate (is recipient listening?): {}", e);
|
||||
error!("{}", report);
|
||||
Err(ErrorKind::ClientCallback(report).into())
|
||||
}
|
||||
Ok(r) => Ok(r.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn receive_tx_async(&self, _params: &str) -> Result<Slate, Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn listen(
|
||||
&self,
|
||||
params: HashMap<String, String>,
|
||||
config: WalletConfig,
|
||||
passphrase: &str,
|
||||
account: &str,
|
||||
node_api_secret: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret);
|
||||
let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account)
|
||||
.context(ErrorKind::WalletSeedDecryption)?;
|
||||
let listen_addr = params.get("api_listen_addr").unwrap();
|
||||
let tls_conf = match params.get("certificate") {
|
||||
Some(s) => Some(api::TLSConfig::new(
|
||||
s.to_owned(),
|
||||
params.get("private_key").unwrap().to_owned(),
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
controller::foreign_listener(wallet.clone(), &listen_addr, tls_conf)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Keybase Wallet Plugin
|
||||
|
||||
use crate::controller;
|
||||
use crate::libwallet::slate::{Slate, VersionedSlate};
|
||||
use crate::libwallet::slate_versions::v0::SlateV0;
|
||||
use crate::libwallet::{Error, ErrorKind};
|
||||
use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig};
|
||||
use failure::ResultExt;
|
||||
use serde::Serialize;
|
||||
use serde_json::{from_str, json, to_string, Value};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::str::from_utf8;
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const TTL: u16 = 60; // TODO: Pass this as a parameter
|
||||
const LISTEN_SLEEP_DURATION: Duration = Duration::from_millis(5000);
|
||||
const POLL_SLEEP_DURATION: Duration = Duration::from_millis(1000);
|
||||
|
||||
// Which topic names to use for communication
|
||||
const SLATE_NEW: &str = "grin_slate_new";
|
||||
const SLATE_SIGNED: &str = "grin_slate_signed";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeybaseWalletCommAdapter {}
|
||||
|
||||
impl KeybaseWalletCommAdapter {
|
||||
/// Check if keybase is installed and return an adapter object.
|
||||
pub fn new() -> Box<WalletCommAdapter> {
|
||||
let mut proc = if cfg!(target_os = "windows") {
|
||||
Command::new("where")
|
||||
} else {
|
||||
Command::new("which")
|
||||
};
|
||||
proc.arg("keybase")
|
||||
.stdout(Stdio::null())
|
||||
.status()
|
||||
.expect("Keybase executable not found, make sure it is installed and in your PATH");
|
||||
|
||||
Box::new(KeybaseWalletCommAdapter {})
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a json object to the keybase process. Type `keybase chat api --help` for a list of available methods.
|
||||
fn api_send(payload: &str) -> Result<Value, Error> {
|
||||
let mut proc = Command::new("keybase");
|
||||
proc.args(&["chat", "api", "-m", &payload]);
|
||||
let output = proc.output().expect("No output");
|
||||
if !output.status.success() {
|
||||
error!(
|
||||
"keybase api fail: {} {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
||||
} else {
|
||||
let response: Value =
|
||||
from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output");
|
||||
let err_msg = format!("{}", response["error"]["message"]);
|
||||
if err_msg.len() > 0 && err_msg != "null" {
|
||||
error!("api_send got error: {}", err_msg);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get keybase username
|
||||
fn whoami() -> Result<String, Error> {
|
||||
let mut proc = Command::new("keybase");
|
||||
proc.args(&["status", "-json"]);
|
||||
let output = proc.output().expect("No output");
|
||||
if !output.status.success() {
|
||||
error!(
|
||||
"keybase api fail: {} {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
||||
} else {
|
||||
let response: Value =
|
||||
from_str(from_utf8(&output.stdout).expect("Bad output")).expect("Bad output");
|
||||
let err_msg = format!("{}", response["error"]["message"]);
|
||||
if err_msg.len() > 0 && err_msg != "null" {
|
||||
error!("status query got error: {}", err_msg);
|
||||
}
|
||||
|
||||
let username = response["Username"].as_str();
|
||||
if let Some(s) = username {
|
||||
Ok(s.to_string())
|
||||
} else {
|
||||
error!("keybase username query fail");
|
||||
Err(ErrorKind::GenericError(
|
||||
"keybase username query fail".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all unread messages from a specific channel/topic and mark as read.
|
||||
fn read_from_channel(channel: &str, topic: &str) -> Result<Vec<String>, Error> {
|
||||
let payload = to_string(&json!({
|
||||
"method": "read",
|
||||
"params": {
|
||||
"options": {
|
||||
"channel": {
|
||||
"name": channel, "topic_type": "dev", "topic_name": topic
|
||||
},
|
||||
"unread_only": true, "peek": false
|
||||
},
|
||||
}
|
||||
}
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let response = api_send(&payload);
|
||||
if let Ok(res) = response {
|
||||
let mut unread: Vec<String> = Vec::new();
|
||||
for msg in res["result"]["messages"]
|
||||
.as_array()
|
||||
.unwrap_or(&vec![json!({})])
|
||||
.iter()
|
||||
{
|
||||
if (msg["msg"]["content"]["type"] == "text") && (msg["msg"]["unread"] == true) {
|
||||
let message = msg["msg"]["content"]["text"]["body"].as_str().unwrap_or("");
|
||||
unread.push(message.to_owned());
|
||||
}
|
||||
}
|
||||
Ok(unread)
|
||||
} else {
|
||||
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
||||
}
|
||||
}
|
||||
|
||||
/// Get unread messages from all channels and mark as read.
|
||||
fn get_unread(topic: &str) -> Result<HashMap<String, String>, Error> {
|
||||
let payload = to_string(&json!({
|
||||
"method": "list",
|
||||
"params": {
|
||||
"options": {
|
||||
"topic_type": "dev",
|
||||
},
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let response = api_send(&payload);
|
||||
|
||||
if let Ok(res) = response {
|
||||
let mut channels = HashSet::new();
|
||||
// Unfortunately the response does not contain the message body
|
||||
// and a separate call is needed for each channel
|
||||
for msg in res["result"]["conversations"]
|
||||
.as_array()
|
||||
.unwrap_or(&vec![json!({})])
|
||||
.iter()
|
||||
{
|
||||
if (msg["unread"] == true) && (msg["channel"]["topic_name"] == topic) {
|
||||
let channel = msg["channel"]["name"].as_str().unwrap();
|
||||
channels.insert(channel.to_string());
|
||||
}
|
||||
}
|
||||
let mut unread: HashMap<String, String> = HashMap::new();
|
||||
for channel in channels.iter() {
|
||||
let messages = read_from_channel(channel, topic);
|
||||
if messages.is_err() {
|
||||
break;
|
||||
}
|
||||
for msg in messages.unwrap() {
|
||||
unread.insert(msg, channel.to_string());
|
||||
}
|
||||
}
|
||||
Ok(unread)
|
||||
} else {
|
||||
Err(ErrorKind::GenericError("keybase api fail".to_owned()))?
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to a keybase channel that self-destructs after ttl seconds.
|
||||
fn send<T: Serialize>(message: T, channel: &str, topic: &str, ttl: u16) -> bool {
|
||||
let seconds = format!("{}s", ttl);
|
||||
let serialized = to_string(&message).unwrap();
|
||||
let payload = to_string(&json!({
|
||||
"method": "send",
|
||||
"params": {
|
||||
"options": {
|
||||
"channel": {
|
||||
"name": channel, "topic_name": topic, "topic_type": "dev"
|
||||
},
|
||||
"message": {
|
||||
"body": serialized
|
||||
},
|
||||
"exploding_lifetime": seconds
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let response = api_send(&payload);
|
||||
if let Ok(res) = response {
|
||||
match res["result"]["message"].as_str() {
|
||||
Some("message sent") => {
|
||||
debug!("Message sent to {}: {}", channel, serialized);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a notify to self that self-destructs after ttl minutes.
|
||||
fn notify(message: &str, channel: &str, ttl: u16) -> bool {
|
||||
let minutes = format!("{}m", ttl);
|
||||
let payload = to_string(&json!({
|
||||
"method": "send",
|
||||
"params": {
|
||||
"options": {
|
||||
"channel": {
|
||||
"name": channel
|
||||
},
|
||||
"message": {
|
||||
"body": message
|
||||
},
|
||||
"exploding_lifetime": minutes
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
let response = api_send(&payload);
|
||||
if let Ok(res) = response {
|
||||
match res["result"]["message"].as_str() {
|
||||
Some("message sent") => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen for a message from a specific channel with topic SLATE_SIGNED for nseconds and return the first valid slate.
|
||||
fn poll(nseconds: u64, channel: &str) -> Option<Slate> {
|
||||
let start = Instant::now();
|
||||
info!("Waiting for response message from @{}...", channel);
|
||||
while start.elapsed().as_secs() < nseconds {
|
||||
let unread = read_from_channel(channel, SLATE_SIGNED);
|
||||
for msg in unread.unwrap().iter() {
|
||||
let blob = from_str::<VersionedSlate>(msg);
|
||||
match blob {
|
||||
Ok(slate) => {
|
||||
let slate: Slate = slate.into();
|
||||
info!(
|
||||
"keybase response message received from @{}, tx uuid: {}",
|
||||
channel, slate.id,
|
||||
);
|
||||
return Some(slate);
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
sleep(POLL_SLEEP_DURATION);
|
||||
}
|
||||
error!(
|
||||
"No response from @{} in {} seconds. Grin send failed!",
|
||||
channel, nseconds
|
||||
);
|
||||
None
|
||||
}
|
||||
|
||||
impl WalletCommAdapter for KeybaseWalletCommAdapter {
|
||||
fn supports_sync(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// Send a slate to a keybase username then wait for a response for TTL seconds.
|
||||
fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result<Slate, Error> {
|
||||
// Limit only one recipient
|
||||
if addr.matches(",").count() > 0 {
|
||||
error!("Only one recipient is supported!");
|
||||
return Err(ErrorKind::GenericError("Tx rejected".to_owned()))?;
|
||||
}
|
||||
|
||||
let id = slate.id;
|
||||
|
||||
// Send original slate to recipient with the SLATE_NEW topic
|
||||
match send(&slate, addr, SLATE_NEW, TTL) {
|
||||
true => (),
|
||||
false => {
|
||||
return Err(ErrorKind::ClientCallback(
|
||||
"Posting transaction slate".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
info!("tx request has been sent to @{}, tx uuid: {}", addr, id);
|
||||
// Wait for response from recipient with SLATE_SIGNED topic
|
||||
match poll(TTL as u64, addr) {
|
||||
Some(slate) => return Ok(slate),
|
||||
None => {
|
||||
return Err(ErrorKind::ClientCallback(
|
||||
"Receiving reply from recipient".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a transaction asynchronously (result will be returned via the listener)
|
||||
fn send_tx_async(&self, _addr: &str, _slate: &Slate) -> Result<(), Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Receive a transaction async. (Actually just read it from wherever and return the slate)
|
||||
fn receive_tx_async(&self, _params: &str) -> Result<Slate, Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Start a listener, passing received messages to the wallet api directly
|
||||
#[allow(unreachable_code)]
|
||||
fn listen(
|
||||
&self,
|
||||
_params: HashMap<String, String>,
|
||||
config: WalletConfig,
|
||||
passphrase: &str,
|
||||
account: &str,
|
||||
node_api_secret: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
let node_client = HTTPNodeClient::new(&config.check_node_api_http_addr, node_api_secret);
|
||||
let wallet = instantiate_wallet(config.clone(), node_client, passphrase, account)
|
||||
.context(ErrorKind::WalletSeedDecryption)?;
|
||||
|
||||
info!("Listening for transactions on keybase ...");
|
||||
loop {
|
||||
// listen for messages from all channels with topic SLATE_NEW
|
||||
let unread = get_unread(SLATE_NEW);
|
||||
if unread.is_err() {
|
||||
error!("Listening exited for some keybase api failure");
|
||||
break;
|
||||
}
|
||||
for (msg, channel) in &unread.unwrap() {
|
||||
let blob = from_str::<VersionedSlate>(msg);
|
||||
match blob {
|
||||
Ok(message) => {
|
||||
let mut slate: Slate = message.clone().into();
|
||||
let tx_uuid = slate.id;
|
||||
|
||||
// Reject multiple recipients channel for safety
|
||||
{
|
||||
if channel.matches(",").count() > 1 {
|
||||
error!(
|
||||
"Incoming tx initiated on channel \"{}\" is rejected, multiple recipients channel! amount: {}(g), tx uuid: {}",
|
||||
channel,
|
||||
slate.amount as f64 / 1000000000.0,
|
||||
tx_uuid,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"tx initiated on channel \"{}\", to send you {}(g). tx uuid: {}",
|
||||
channel,
|
||||
slate.amount as f64 / 1000000000.0,
|
||||
tx_uuid,
|
||||
);
|
||||
match controller::foreign_single_use(wallet.clone(), |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
api.receive_tx(&mut slate, None, None)?;
|
||||
Ok(())
|
||||
}) {
|
||||
// Reply to the same channel with topic SLATE_SIGNED
|
||||
Ok(_) => {
|
||||
let success = match message {
|
||||
// Send the same version of slate that was sent to us
|
||||
VersionedSlate::V0(_) => {
|
||||
send(SlateV0::from(slate), channel, SLATE_SIGNED, TTL)
|
||||
}
|
||||
VersionedSlate::V1(_) => {
|
||||
send(slate, channel, SLATE_SIGNED, TTL)
|
||||
}
|
||||
};
|
||||
|
||||
if success {
|
||||
notify_on_receive(
|
||||
config.keybase_notify_ttl.unwrap_or(1440),
|
||||
channel.to_string(),
|
||||
tx_uuid.to_string(),
|
||||
);
|
||||
debug!("Returned slate to @{} via keybase", channel);
|
||||
} else {
|
||||
error!("Failed to return slate to @{} via keybase. Incoming tx failed", channel);
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error on receiving tx via keybase: {}. Incoming tx failed",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => debug!("Failed to deserialize keybase message: {}", msg),
|
||||
}
|
||||
}
|
||||
sleep(LISTEN_SLEEP_DURATION);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify in keybase on receiving a transaction
|
||||
fn notify_on_receive(keybase_notify_ttl: u16, channel: String, tx_uuid: String) {
|
||||
if keybase_notify_ttl > 0 {
|
||||
let my_username = whoami();
|
||||
if let Ok(username) = my_username {
|
||||
let split = channel.split(",");
|
||||
let vec: Vec<&str> = split.collect();
|
||||
if vec.len() > 1 {
|
||||
let receiver = username;
|
||||
let sender = if vec[0] == receiver {
|
||||
vec[1]
|
||||
} else {
|
||||
if vec[1] != receiver {
|
||||
error!("keybase - channel doesn't include my username! channel: {}, username: {}",
|
||||
channel, receiver
|
||||
);
|
||||
}
|
||||
vec[0]
|
||||
};
|
||||
|
||||
let msg = format!(
|
||||
"[grin wallet notice]: \
|
||||
you could have some coins received from @{}\n\
|
||||
Transaction Id: {}",
|
||||
sender, tx_uuid
|
||||
);
|
||||
notify(&msg, &receiver, keybase_notify_ttl);
|
||||
info!(
|
||||
"tx from @{} is done, please check on grin wallet. tx uuid: {}",
|
||||
sender, tx_uuid,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
error!("keybase notification fail on whoami query");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod file;
|
||||
mod http;
|
||||
mod keybase;
|
||||
mod null;
|
||||
pub mod util;
|
||||
|
||||
pub use self::file::FileWalletCommAdapter;
|
||||
pub use self::http::HTTPWalletCommAdapter;
|
||||
pub use self::keybase::KeybaseWalletCommAdapter;
|
||||
pub use self::null::NullWalletCommAdapter;
|
||||
|
||||
use crate::libwallet::slate::Slate;
|
||||
use crate::libwallet::Error;
|
||||
use crate::WalletConfig;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Encapsulate wallet to wallet communication functions
|
||||
pub trait WalletCommAdapter {
|
||||
/// Whether this adapter supports sync mode
|
||||
fn supports_sync(&self) -> bool;
|
||||
|
||||
/// Send a transaction slate to another listening wallet and return result
|
||||
/// TODO: Probably need a slate wrapper type
|
||||
fn send_tx_sync(&self, addr: &str, slate: &Slate) -> Result<Slate, Error>;
|
||||
|
||||
/// Send a transaction asynchronously (result will be returned via the listener)
|
||||
fn send_tx_async(&self, addr: &str, slate: &Slate) -> Result<(), Error>;
|
||||
|
||||
/// Receive a transaction async. (Actually just read it from wherever and return the slate)
|
||||
fn receive_tx_async(&self, params: &str) -> Result<Slate, Error>;
|
||||
|
||||
/// Start a listener, passing received messages to the wallet api directly
|
||||
/// Takes a wallet config for now to avoid needing all sorts of awkward
|
||||
/// type parameters on this trait
|
||||
fn listen(
|
||||
&self,
|
||||
params: HashMap<String, String>,
|
||||
config: WalletConfig,
|
||||
passphrase: &str,
|
||||
account: &str,
|
||||
node_api_secret: Option<String>,
|
||||
) -> Result<(), Error>;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Null Output 'plugin' implementation
|
||||
use crate::libwallet::slate::Slate;
|
||||
use crate::libwallet::Error;
|
||||
use crate::{WalletCommAdapter, WalletConfig};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NullWalletCommAdapter {}
|
||||
|
||||
impl NullWalletCommAdapter {
|
||||
/// Create
|
||||
pub fn new() -> Box<NullWalletCommAdapter> {
|
||||
Box::new(NullWalletCommAdapter {})
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletCommAdapter for NullWalletCommAdapter {
|
||||
fn supports_sync(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn send_tx_sync(&self, _dest: &str, slate: &Slate) -> Result<Slate, Error> {
|
||||
Ok(slate.clone())
|
||||
}
|
||||
|
||||
fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_tx_async(&self, _params: &str) -> Result<Slate, Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn listen(
|
||||
&self,
|
||||
_params: HashMap<String, String>,
|
||||
_config: WalletConfig,
|
||||
_passphrase: &str,
|
||||
_account: &str,
|
||||
_node_api_secret: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::libwallet::slate::{Slate, VersionedSlate};
|
||||
use crate::libwallet::slate_versions::v0::SlateV0;
|
||||
use crate::libwallet::ErrorKind;
|
||||
use serde_json as json;
|
||||
|
||||
pub fn get_versioned_slate(slate: &Slate) -> VersionedSlate {
|
||||
let slate = slate.clone();
|
||||
match slate.version {
|
||||
0 => VersionedSlate::V0(SlateV0::from(slate)),
|
||||
_ => VersionedSlate::V1(slate),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_slate(slate: &Slate) -> String {
|
||||
json::to_string(&get_versioned_slate(slate)).unwrap()
|
||||
}
|
||||
|
||||
pub fn deserialize_slate(raw_slate: &str) -> Slate {
|
||||
let versioned_slate: VersionedSlate = json::from_str(&raw_slate)
|
||||
.map_err(|err| ErrorKind::Format(err.to_string()))
|
||||
.unwrap();
|
||||
versioned_slate.into()
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::util::{Mutex, ZeroingString};
|
||||
use std::collections::HashMap;
|
||||
/// Grin wallet command-line function implementations
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json as json;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::TLSConfig;
|
||||
use crate::core::core;
|
||||
use crate::keychain;
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::{controller, display, HTTPNodeClient, WalletConfig, WalletInst, WalletSeed};
|
||||
use crate::{
|
||||
FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, LMDBBackend,
|
||||
NodeClient, NullWalletCommAdapter,
|
||||
};
|
||||
|
||||
/// Arguments common to all wallet commands
|
||||
#[derive(Clone)]
|
||||
pub struct GlobalArgs {
|
||||
pub account: String,
|
||||
pub node_api_secret: Option<String>,
|
||||
pub show_spent: bool,
|
||||
pub password: Option<ZeroingString>,
|
||||
pub tls_conf: Option<TLSConfig>,
|
||||
}
|
||||
|
||||
/// Arguments for init command
|
||||
pub struct InitArgs {
|
||||
/// BIP39 recovery phrase length
|
||||
pub list_length: usize,
|
||||
pub password: ZeroingString,
|
||||
pub config: WalletConfig,
|
||||
pub recovery_phrase: Option<ZeroingString>,
|
||||
pub restore: bool,
|
||||
}
|
||||
|
||||
pub fn init(g_args: &GlobalArgs, args: InitArgs) -> Result<(), Error> {
|
||||
WalletSeed::init_file(
|
||||
&args.config,
|
||||
args.list_length,
|
||||
args.recovery_phrase,
|
||||
&args.password,
|
||||
)?;
|
||||
info!("Wallet seed file created");
|
||||
let client_n = HTTPNodeClient::new(
|
||||
&args.config.check_node_api_http_addr,
|
||||
g_args.node_api_secret.clone(),
|
||||
);
|
||||
let _: LMDBBackend<HTTPNodeClient, keychain::ExtKeychain> =
|
||||
LMDBBackend::new(args.config.clone(), &args.password, client_n)?;
|
||||
info!("Wallet database backend created");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Argument for recover
|
||||
pub struct RecoverArgs {
|
||||
pub recovery_phrase: Option<ZeroingString>,
|
||||
pub passphrase: ZeroingString,
|
||||
}
|
||||
|
||||
/// Check whether seed file exists
|
||||
pub fn wallet_seed_exists(config: &WalletConfig) -> Result<(), Error> {
|
||||
let res = WalletSeed::seed_file_exists(&config)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub fn recover(config: &WalletConfig, args: RecoverArgs) -> Result<(), Error> {
|
||||
if args.recovery_phrase.is_none() {
|
||||
let res = WalletSeed::from_file(config, &args.passphrase);
|
||||
if let Err(e) = res {
|
||||
error!("Error loading wallet seed (check password): {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
let _ = res.unwrap().show_recovery_phrase();
|
||||
} else {
|
||||
let res = WalletSeed::recover_from_phrase(
|
||||
&config,
|
||||
&args.recovery_phrase.as_ref().unwrap(),
|
||||
&args.passphrase,
|
||||
);
|
||||
if let Err(e) = res {
|
||||
error!("Error recovering seed - {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Arguments for listen command
|
||||
pub struct ListenArgs {
|
||||
pub method: String,
|
||||
}
|
||||
|
||||
pub fn listen(config: &WalletConfig, args: &ListenArgs, g_args: &GlobalArgs) -> Result<(), Error> {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("api_listen_addr".to_owned(), config.api_listen_addr());
|
||||
if let Some(t) = g_args.tls_conf.as_ref() {
|
||||
params.insert("certificate".to_owned(), t.certificate.clone());
|
||||
params.insert("private_key".to_owned(), t.private_key.clone());
|
||||
}
|
||||
let adapter = match args.method.as_str() {
|
||||
"http" => HTTPWalletCommAdapter::new(),
|
||||
"keybase" => KeybaseWalletCommAdapter::new(),
|
||||
_ => NullWalletCommAdapter::new(),
|
||||
};
|
||||
|
||||
let res = adapter.listen(
|
||||
params,
|
||||
config.clone(),
|
||||
&g_args.password.clone().unwrap(),
|
||||
&g_args.account,
|
||||
g_args.node_api_secret.clone(),
|
||||
);
|
||||
if let Err(e) = res {
|
||||
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn owner_api(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
config: &WalletConfig,
|
||||
g_args: &GlobalArgs,
|
||||
) -> Result<(), Error> {
|
||||
let res = controller::owner_listener(
|
||||
wallet,
|
||||
config.owner_api_listen_addr().as_str(),
|
||||
g_args.node_api_secret.clone(),
|
||||
g_args.tls_conf.clone(),
|
||||
config.owner_api_include_foreign.clone(),
|
||||
);
|
||||
if let Err(e) = res {
|
||||
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Arguments for account command
|
||||
pub struct AccountArgs {
|
||||
pub create: Option<String>,
|
||||
}
|
||||
|
||||
pub fn account(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: AccountArgs,
|
||||
) -> Result<(), Error> {
|
||||
if args.create.is_none() {
|
||||
let res = controller::owner_single_use(wallet, |api| {
|
||||
let acct_mappings = api.accounts()?;
|
||||
// give logging thread a moment to catch up
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
display::accounts(acct_mappings);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = res {
|
||||
error!("Error listing accounts: {}", e);
|
||||
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
|
||||
}
|
||||
} else {
|
||||
let label = args.create.unwrap();
|
||||
let res = controller::owner_single_use(wallet, |api| {
|
||||
api.create_account_path(&label)?;
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
info!("Account: '{}' Created!", label);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = res {
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
error!("Error creating account '{}': {}", label, e);
|
||||
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Arguments for the send command
|
||||
pub struct SendArgs {
|
||||
pub amount: u64,
|
||||
pub message: Option<String>,
|
||||
pub minimum_confirmations: u64,
|
||||
pub selection_strategy: String,
|
||||
pub estimate_selection_strategies: bool,
|
||||
pub method: String,
|
||||
pub dest: String,
|
||||
pub change_outputs: usize,
|
||||
pub fluff: bool,
|
||||
pub max_outputs: usize,
|
||||
}
|
||||
|
||||
pub fn send(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: SendArgs,
|
||||
dark_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
if args.estimate_selection_strategies {
|
||||
let strategies = vec!["smallest", "all"]
|
||||
.into_iter()
|
||||
.map(|strategy| {
|
||||
let (total, fee) = api
|
||||
.estimate_initiate_tx(
|
||||
None,
|
||||
args.amount,
|
||||
args.minimum_confirmations,
|
||||
args.max_outputs,
|
||||
args.change_outputs,
|
||||
strategy == "all",
|
||||
)
|
||||
.unwrap();
|
||||
(strategy, total, fee)
|
||||
})
|
||||
.collect();
|
||||
display::estimate(args.amount, strategies, dark_scheme);
|
||||
} else {
|
||||
let result = api.initiate_tx(
|
||||
None,
|
||||
args.amount,
|
||||
args.minimum_confirmations,
|
||||
args.max_outputs,
|
||||
args.change_outputs,
|
||||
args.selection_strategy == "all",
|
||||
args.message.clone(),
|
||||
);
|
||||
let (mut slate, lock_fn) = match result {
|
||||
Ok(s) => {
|
||||
info!(
|
||||
"Tx created: {} grin to {} (strategy '{}')",
|
||||
core::amount_to_hr_string(args.amount, false),
|
||||
args.dest,
|
||||
args.selection_strategy,
|
||||
);
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Tx not created: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let adapter = match args.method.as_str() {
|
||||
"http" => HTTPWalletCommAdapter::new(),
|
||||
"file" => FileWalletCommAdapter::new(),
|
||||
"keybase" => KeybaseWalletCommAdapter::new(),
|
||||
"self" => NullWalletCommAdapter::new(),
|
||||
_ => NullWalletCommAdapter::new(),
|
||||
};
|
||||
if adapter.supports_sync() {
|
||||
slate = adapter.send_tx_sync(&args.dest, &slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
if args.method == "self" {
|
||||
controller::foreign_single_use(wallet, |api| {
|
||||
api.receive_tx(&mut slate, Some(&args.dest), None)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
api.finalize_tx(&mut slate)?;
|
||||
} else {
|
||||
adapter.send_tx_async(&args.dest, &slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
}
|
||||
if adapter.supports_sync() {
|
||||
let result = api.post_tx(&slate.tx, args.fluff);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Tx sent ok",);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tx sent fail: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive command argument
|
||||
pub struct ReceiveArgs {
|
||||
pub input: String,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn receive(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
g_args: &GlobalArgs,
|
||||
args: ReceiveArgs,
|
||||
) -> Result<(), Error> {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let mut slate = adapter.receive_tx_async(&args.input)?;
|
||||
controller::foreign_single_use(wallet, |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
api.receive_tx(&mut slate, Some(&g_args.account), args.message.clone())?;
|
||||
Ok(())
|
||||
})?;
|
||||
let send_tx = format!("{}.response", args.input);
|
||||
adapter.send_tx_async(&send_tx, &slate)?;
|
||||
info!(
|
||||
"Response file {}.response generated, sending it back to the transaction originator.",
|
||||
args.input
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finalize command args
|
||||
pub struct FinalizeArgs {
|
||||
pub input: String,
|
||||
pub fluff: bool,
|
||||
}
|
||||
|
||||
pub fn finalize(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: FinalizeArgs,
|
||||
) -> Result<(), Error> {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let mut slate = adapter.receive_tx_async(&args.input)?;
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
|
||||
|
||||
let result = api.post_tx(&slate.tx, args.fluff);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Transaction sent successfully, check the wallet again for confirmation.");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tx not sent: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Info command args
|
||||
pub struct InfoArgs {
|
||||
pub minimum_confirmations: u64,
|
||||
}
|
||||
|
||||
pub fn info(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
g_args: &GlobalArgs,
|
||||
args: InfoArgs,
|
||||
dark_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let (validated, wallet_info) =
|
||||
api.retrieve_summary_info(true, args.minimum_confirmations)?;
|
||||
display::info(&g_args.account, &wallet_info, validated, dark_scheme);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn outputs(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
g_args: &GlobalArgs,
|
||||
dark_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let (height, _) = api.node_height()?;
|
||||
let (validated, outputs) = api.retrieve_outputs(g_args.show_spent, true, None)?;
|
||||
display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Txs command args
|
||||
pub struct TxsArgs {
|
||||
pub id: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn txs(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
g_args: &GlobalArgs,
|
||||
args: TxsArgs,
|
||||
dark_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let (height, _) = api.node_height()?;
|
||||
let (validated, txs) = api.retrieve_txs(true, args.id, None)?;
|
||||
let include_status = !args.id.is_some();
|
||||
display::txs(
|
||||
&g_args.account,
|
||||
height,
|
||||
validated,
|
||||
&txs,
|
||||
include_status,
|
||||
dark_scheme,
|
||||
)?;
|
||||
// if given a particular transaction id, also get and display associated
|
||||
// inputs/outputs and messages
|
||||
if args.id.is_some() {
|
||||
let (_, outputs) = api.retrieve_outputs(true, false, args.id)?;
|
||||
display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?;
|
||||
// should only be one here, but just in case
|
||||
for tx in txs {
|
||||
display::tx_messages(&tx, dark_scheme)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repost
|
||||
pub struct RepostArgs {
|
||||
pub id: u32,
|
||||
pub dump_file: Option<String>,
|
||||
pub fluff: bool,
|
||||
}
|
||||
|
||||
pub fn repost(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: RepostArgs,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let (_, txs) = api.retrieve_txs(true, Some(args.id), None)?;
|
||||
let stored_tx = api.get_stored_tx(&txs[0])?;
|
||||
if stored_tx.is_none() {
|
||||
error!(
|
||||
"Transaction with id {} does not have transaction data. Not reposting.",
|
||||
args.id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
match args.dump_file {
|
||||
None => {
|
||||
if txs[0].confirmed {
|
||||
error!(
|
||||
"Transaction with id {} is confirmed. Not reposting.",
|
||||
args.id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
api.post_tx(&stored_tx.unwrap(), args.fluff)?;
|
||||
info!("Reposted transaction at {}", args.id);
|
||||
return Ok(());
|
||||
}
|
||||
Some(f) => {
|
||||
let mut tx_file = File::create(f.clone())?;
|
||||
tx_file.write_all(json::to_string(&stored_tx).unwrap().as_bytes())?;
|
||||
tx_file.sync_all()?;
|
||||
info!("Dumped transaction data for tx {} to {}", args.id, f);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel
|
||||
pub struct CancelArgs {
|
||||
pub tx_id: Option<u32>,
|
||||
pub tx_slate_id: Option<Uuid>,
|
||||
pub tx_id_string: String,
|
||||
}
|
||||
|
||||
pub fn cancel(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: CancelArgs,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let result = api.cancel_tx(args.tx_id, args.tx_slate_id);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Transaction {} Cancelled", args.tx_id_string);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("TX Cancellation failed: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let result = api.restore();
|
||||
match result {
|
||||
Ok(_) => {
|
||||
warn!("Wallet restore complete",);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Wallet restore failed: {}", e);
|
||||
error!("Backtrace: {}", e.backtrace().unwrap());
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_repair(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
warn!("Starting wallet check...",);
|
||||
warn!("Updating all wallet outputs, please wait ...",);
|
||||
let result = api.check_repair();
|
||||
match result {
|
||||
Ok(_) => {
|
||||
warn!("Wallet check complete",);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Wallet check failed: {}", e);
|
||||
error!("Backtrace: {}", e.backtrace().unwrap());
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,822 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Controller for wallet.. instantiates and handles listeners (or single-run
|
||||
//! invocations) as needed.
|
||||
//! Still experimental
|
||||
use crate::adapters::util::get_versioned_slate;
|
||||
use crate::adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter};
|
||||
use crate::api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
|
||||
use crate::core::core;
|
||||
use crate::core::core::Transaction;
|
||||
use crate::keychain::Keychain;
|
||||
use crate::libwallet::api::{APIForeign, APIOwner};
|
||||
use crate::libwallet::slate::{Slate, VersionedSlate};
|
||||
use crate::libwallet::types::{
|
||||
CbData, NodeClient, OutputData, SendTXArgs, TxLogEntry, WalletBackend, WalletInfo,
|
||||
};
|
||||
use crate::libwallet::{Error, ErrorKind};
|
||||
use crate::util::secp::pedersen;
|
||||
use crate::util::to_base64;
|
||||
use crate::util::Mutex;
|
||||
use failure::ResultExt;
|
||||
use futures::future::{err, ok};
|
||||
use futures::{Future, Stream};
|
||||
use hyper::{Body, Request, Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use url::form_urlencoded;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Instantiate wallet Owner API for a single-use (command line) call
|
||||
/// Return a function containing a loaded API context to call
|
||||
pub fn owner_single_use<F, T: ?Sized, C, K>(wallet: Arc<Mutex<T>>, f: F) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
F: FnOnce(&mut APIOwner<T, C, K>) -> Result<(), Error>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
f(&mut APIOwner::new(wallet.clone()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Instantiate wallet Foreign API for a single-use (command line) call
|
||||
/// Return a function containing a loaded API context to call
|
||||
pub fn foreign_single_use<F, T: ?Sized, C, K>(wallet: Arc<Mutex<T>>, f: F) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
F: FnOnce(&mut APIForeign<T, C, K>) -> Result<(), Error>,
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
f(&mut APIForeign::new(wallet.clone()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Listener version, providing same API but listening for requests on a
|
||||
/// port and wrapping the calls
|
||||
pub fn owner_listener<T: ?Sized, C, K>(
|
||||
wallet: Arc<Mutex<T>>,
|
||||
addr: &str,
|
||||
api_secret: Option<String>,
|
||||
tls_config: Option<TLSConfig>,
|
||||
owner_api_include_foreign: Option<bool>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
OwnerAPIHandler<T, C, K>: Handler,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
let api_handler = OwnerAPIHandler::new(wallet.clone());
|
||||
|
||||
let mut router = Router::new();
|
||||
if api_secret.is_some() {
|
||||
let api_basic_auth =
|
||||
"Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap()));
|
||||
let basic_realm = "Basic realm=GrinOwnerAPI".to_string();
|
||||
let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(api_basic_auth, basic_realm));
|
||||
router.add_middleware(basic_auth_middleware);
|
||||
}
|
||||
router
|
||||
.add_route("/v1/wallet/owner/**", Arc::new(api_handler))
|
||||
.map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?;
|
||||
|
||||
// If so configured, add the foreign API to the same port
|
||||
if owner_api_include_foreign.unwrap_or(false) {
|
||||
info!("Starting HTTP Foreign API on Owner server at {}.", addr);
|
||||
let foreign_api_handler = ForeignAPIHandler::new(wallet.clone());
|
||||
router
|
||||
.add_route("/v1/wallet/foreign/**", Arc::new(foreign_api_handler))
|
||||
.map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?;
|
||||
}
|
||||
|
||||
let mut apis = ApiServer::new();
|
||||
info!("Starting HTTP Owner API server at {}.", addr);
|
||||
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
|
||||
let api_thread =
|
||||
apis.start(socket_addr, router, tls_config)
|
||||
.context(ErrorKind::GenericError(
|
||||
"API thread failed to start".to_string(),
|
||||
))?;
|
||||
api_thread
|
||||
.join()
|
||||
.map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into())
|
||||
}
|
||||
|
||||
/// Listener version, providing same API but listening for requests on a
|
||||
/// port and wrapping the calls
|
||||
pub fn foreign_listener<T: ?Sized, C, K>(
|
||||
wallet: Arc<Mutex<T>>,
|
||||
addr: &str,
|
||||
tls_config: Option<TLSConfig>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
let api_handler = ForeignAPIHandler::new(wallet);
|
||||
|
||||
let mut router = Router::new();
|
||||
router
|
||||
.add_route("/v1/wallet/foreign/**", Arc::new(api_handler))
|
||||
.map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?;
|
||||
|
||||
let mut apis = ApiServer::new();
|
||||
warn!("Starting HTTP Foreign listener API server at {}.", addr);
|
||||
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
|
||||
let api_thread =
|
||||
apis.start(socket_addr, router, tls_config)
|
||||
.context(ErrorKind::GenericError(
|
||||
"API thread failed to start".to_string(),
|
||||
))?;
|
||||
warn!("HTTP Foreign listener started.");
|
||||
|
||||
api_thread
|
||||
.join()
|
||||
.map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into())
|
||||
}
|
||||
|
||||
type WalletResponseFuture = Box<dyn Future<Item = Response<Body>, Error = Error> + Send>;
|
||||
|
||||
/// API Handler/Wrapper for owner functions
|
||||
pub struct OwnerAPIHandler<T: ?Sized, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<T>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized, C, K> OwnerAPIHandler<T, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Create a new owner API handler for GET methods
|
||||
pub fn new(wallet: Arc<Mutex<T>>) -> OwnerAPIHandler<T, C, K> {
|
||||
OwnerAPIHandler {
|
||||
wallet,
|
||||
phantom: PhantomData,
|
||||
phantom_c: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieve_outputs(
|
||||
&self,
|
||||
req: &Request<Body>,
|
||||
api: APIOwner<T, C, K>,
|
||||
) -> Result<(bool, Vec<(OutputData, pedersen::Commitment)>), Error> {
|
||||
let mut update_from_node = false;
|
||||
let mut id = None;
|
||||
let mut show_spent = false;
|
||||
let params = parse_params(req);
|
||||
|
||||
if let Some(_) = params.get("refresh") {
|
||||
update_from_node = true;
|
||||
}
|
||||
if let Some(_) = params.get("show_spent") {
|
||||
show_spent = true;
|
||||
}
|
||||
if let Some(ids) = params.get("tx_id") {
|
||||
if let Some(x) = ids.first() {
|
||||
id = Some(x.parse().unwrap());
|
||||
}
|
||||
}
|
||||
api.retrieve_outputs(show_spent, update_from_node, id)
|
||||
}
|
||||
|
||||
pub fn retrieve_txs(
|
||||
&self,
|
||||
req: &Request<Body>,
|
||||
api: APIOwner<T, C, K>,
|
||||
) -> Result<(bool, Vec<TxLogEntry>), Error> {
|
||||
let mut tx_id = None;
|
||||
let mut tx_slate_id = None;
|
||||
let mut update_from_node = false;
|
||||
|
||||
let params = parse_params(req);
|
||||
|
||||
if let Some(_) = params.get("refresh") {
|
||||
update_from_node = true;
|
||||
}
|
||||
if let Some(ids) = params.get("id") {
|
||||
if let Some(x) = ids.first() {
|
||||
tx_id = Some(x.parse().unwrap());
|
||||
}
|
||||
}
|
||||
if let Some(tx_slate_ids) = params.get("tx_id") {
|
||||
if let Some(x) = tx_slate_ids.first() {
|
||||
tx_slate_id = Some(x.parse().unwrap());
|
||||
}
|
||||
}
|
||||
api.retrieve_txs(update_from_node, tx_id, tx_slate_id)
|
||||
}
|
||||
|
||||
pub fn retrieve_stored_tx(
|
||||
&self,
|
||||
req: &Request<Body>,
|
||||
api: APIOwner<T, C, K>,
|
||||
) -> Result<(bool, Option<Transaction>), Error> {
|
||||
let params = parse_params(req);
|
||||
if let Some(id_string) = params.get("id") {
|
||||
match id_string[0].parse() {
|
||||
Ok(id) => match api.retrieve_txs(true, Some(id), None) {
|
||||
Ok((_, txs)) => {
|
||||
let stored_tx = api.get_stored_tx(&txs[0])?;
|
||||
Ok((txs[0].confirmed, stored_tx))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("retrieve_stored_tx: failed with error: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("retrieve_stored_tx: could not parse id: {}", e);
|
||||
Err(ErrorKind::TransactionDumpError(
|
||||
"retrieve_stored_tx: cannot dump transaction. Could not parse id in request.",
|
||||
).into())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(ErrorKind::TransactionDumpError(
|
||||
"retrieve_stored_tx: Cannot retrieve transaction. Missing id param in request.",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn retrieve_summary_info(
|
||||
&self,
|
||||
req: &Request<Body>,
|
||||
mut api: APIOwner<T, C, K>,
|
||||
) -> Result<(bool, WalletInfo), Error> {
|
||||
let mut minimum_confirmations = 1; // TODO - default needed here
|
||||
let params = parse_params(req);
|
||||
let update_from_node = params.get("refresh").is_some();
|
||||
|
||||
if let Some(confs) = params.get("minimum_confirmations") {
|
||||
if let Some(x) = confs.first() {
|
||||
minimum_confirmations = x.parse().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
api.retrieve_summary_info(update_from_node, minimum_confirmations)
|
||||
}
|
||||
|
||||
pub fn node_height(
|
||||
&self,
|
||||
_req: &Request<Body>,
|
||||
mut api: APIOwner<T, C, K>,
|
||||
) -> Result<(u64, bool), Error> {
|
||||
api.node_height()
|
||||
}
|
||||
|
||||
fn handle_get_request(&self, req: &Request<Body>) -> Result<Response<Body>, Error> {
|
||||
let api = APIOwner::new(self.wallet.clone());
|
||||
|
||||
Ok(
|
||||
match req
|
||||
.uri()
|
||||
.path()
|
||||
.trim_right_matches("/")
|
||||
.rsplit("/")
|
||||
.next()
|
||||
.unwrap()
|
||||
{
|
||||
"retrieve_outputs" => json_response(&self.retrieve_outputs(req, api)?),
|
||||
"retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?),
|
||||
"node_height" => json_response(&self.node_height(req, api)?),
|
||||
"retrieve_txs" => json_response(&self.retrieve_txs(req, api)?),
|
||||
"retrieve_stored_tx" => json_response(&self.retrieve_stored_tx(req, api)?),
|
||||
_ => response(StatusCode::BAD_REQUEST, ""),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn issue_send_tx(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
mut api: APIOwner<T, C, K>,
|
||||
) -> Box<dyn Future<Item = Slate, Error = Error> + Send> {
|
||||
Box::new(parse_body(req).and_then(move |args: SendTXArgs| {
|
||||
let result = api.initiate_tx(
|
||||
None,
|
||||
args.amount,
|
||||
args.minimum_confirmations,
|
||||
args.max_outputs,
|
||||
args.num_change_outputs,
|
||||
args.selection_strategy_is_use_all,
|
||||
args.message,
|
||||
);
|
||||
let (mut slate, lock_fn) = match result {
|
||||
Ok(s) => {
|
||||
info!(
|
||||
"Tx created: {} grin to {} (strategy '{}')",
|
||||
core::amount_to_hr_string(args.amount, false),
|
||||
&args.dest,
|
||||
args.selection_strategy_is_use_all,
|
||||
);
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tx not created: {}", e);
|
||||
match e.kind() {
|
||||
// user errors, don't backtrace
|
||||
ErrorKind::NotEnoughFunds { .. } => {}
|
||||
ErrorKind::Fee { .. } => {}
|
||||
_ => {
|
||||
// otherwise give full dump
|
||||
error!("Backtrace: {}", e.backtrace().unwrap());
|
||||
}
|
||||
};
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
match args.method.as_ref() {
|
||||
"http" => slate = HTTPWalletCommAdapter::new().send_tx_sync(&args.dest, &slate)?,
|
||||
"file" => {
|
||||
FileWalletCommAdapter::new().send_tx_async(&args.dest, &slate)?;
|
||||
}
|
||||
"keybase" => {
|
||||
//TODO: in case of keybase, the response might take 60s and leave the service hanging
|
||||
slate = KeybaseWalletCommAdapter::new().send_tx_sync(&args.dest, &slate)?;
|
||||
}
|
||||
_ => {
|
||||
error!("unsupported payment method: {}", args.method);
|
||||
return Err(ErrorKind::ClientCallback(
|
||||
"unsupported payment method".to_owned(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
if args.method != "file" {
|
||||
api.finalize_tx(&mut slate)?;
|
||||
}
|
||||
Ok(slate)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn finalize_tx(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
mut api: APIOwner<T, C, K>,
|
||||
) -> Box<dyn Future<Item = Slate, Error = Error> + Send> {
|
||||
Box::new(
|
||||
parse_body(req).and_then(move |mut slate| match api.finalize_tx(&mut slate) {
|
||||
Ok(_) => ok(slate.clone()),
|
||||
Err(e) => {
|
||||
error!("finalize_tx: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn cancel_tx(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
mut api: APIOwner<T, C, K>,
|
||||
) -> Box<dyn Future<Item = (), Error = Error> + Send> {
|
||||
let params = parse_params(&req);
|
||||
if let Some(id_string) = params.get("id") {
|
||||
Box::new(match id_string[0].parse() {
|
||||
Ok(id) => match api.cancel_tx(Some(id), None) {
|
||||
Ok(_) => ok(()),
|
||||
Err(e) => {
|
||||
error!("cancel_tx: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("cancel_tx: could not parse id: {}", e);
|
||||
err(ErrorKind::TransactionCancellationError(
|
||||
"cancel_tx: cannot cancel transaction. Could not parse id in request.",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
} else if let Some(tx_id_string) = params.get("tx_id") {
|
||||
Box::new(match tx_id_string[0].parse() {
|
||||
Ok(tx_id) => match api.cancel_tx(None, Some(tx_id)) {
|
||||
Ok(_) => ok(()),
|
||||
Err(e) => {
|
||||
error!("cancel_tx: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("cancel_tx: could not parse tx_id: {}", e);
|
||||
err(ErrorKind::TransactionCancellationError(
|
||||
"cancel_tx: cannot cancel transaction. Could not parse tx_id in request.",
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Box::new(err(ErrorKind::TransactionCancellationError(
|
||||
"cancel_tx: Cannot cancel transaction. Missing id or tx_id param in request.",
|
||||
)
|
||||
.into()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn post_tx(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
api: APIOwner<T, C, K>,
|
||||
) -> Box<dyn Future<Item = (), Error = Error> + Send> {
|
||||
let params = match req.uri().query() {
|
||||
Some(query_string) => form_urlencoded::parse(query_string.as_bytes())
|
||||
.into_owned()
|
||||
.fold(HashMap::new(), |mut hm, (k, v)| {
|
||||
hm.entry(k).or_insert(vec![]).push(v);
|
||||
hm
|
||||
}),
|
||||
None => HashMap::new(),
|
||||
};
|
||||
let fluff = params.get("fluff").is_some();
|
||||
Box::new(parse_body(req).and_then(
|
||||
move |slate: Slate| match api.post_tx(&slate.tx, fluff) {
|
||||
Ok(_) => ok(()),
|
||||
Err(e) => {
|
||||
error!("post_tx: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn repost(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
api: APIOwner<T, C, K>,
|
||||
) -> Box<dyn Future<Item = (), Error = Error> + Send> {
|
||||
let params = parse_params(&req);
|
||||
let mut id_int: Option<u32> = None;
|
||||
let mut tx_uuid: Option<Uuid> = None;
|
||||
|
||||
if let Some(id_string) = params.get("id") {
|
||||
match id_string[0].parse() {
|
||||
Ok(id) => id_int = Some(id),
|
||||
Err(e) => {
|
||||
error!("repost: could not parse id: {}", e);
|
||||
return Box::new(err(ErrorKind::GenericError(
|
||||
"repost: cannot repost transaction. Could not parse id in request."
|
||||
.to_owned(),
|
||||
)
|
||||
.into()));
|
||||
}
|
||||
}
|
||||
} else if let Some(tx_id_string) = params.get("tx_id") {
|
||||
match tx_id_string[0].parse() {
|
||||
Ok(tx_id) => tx_uuid = Some(tx_id),
|
||||
Err(e) => {
|
||||
error!("repost: could not parse tx_id: {}", e);
|
||||
return Box::new(err(ErrorKind::GenericError(
|
||||
"repost: cannot repost transaction. Could not parse tx_id in request."
|
||||
.to_owned(),
|
||||
)
|
||||
.into()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Box::new(err(ErrorKind::GenericError(
|
||||
"repost: Cannot repost transaction. Missing id or tx_id param in request."
|
||||
.to_owned(),
|
||||
)
|
||||
.into()));
|
||||
}
|
||||
|
||||
let res = api.retrieve_txs(true, id_int, tx_uuid);
|
||||
if let Err(e) = res {
|
||||
return Box::new(err(ErrorKind::GenericError(format!(
|
||||
"repost: cannot repost transaction. retrieve_txs failed, err: {:?}",
|
||||
e
|
||||
))
|
||||
.into()));
|
||||
}
|
||||
let (_, txs) = res.unwrap();
|
||||
let res = api.get_stored_tx(&txs[0]);
|
||||
if let Err(e) = res {
|
||||
return Box::new(err(ErrorKind::GenericError(format!(
|
||||
"repost: cannot repost transaction. get_stored_tx failed, err: {:?}",
|
||||
e
|
||||
))
|
||||
.into()));
|
||||
}
|
||||
let stored_tx = res.unwrap();
|
||||
if stored_tx.is_none() {
|
||||
error!(
|
||||
"Transaction with id {:?}/{:?} does not have transaction data. Not reposting.",
|
||||
id_int, tx_uuid,
|
||||
);
|
||||
return Box::new(err(ErrorKind::GenericError(
|
||||
"repost: Cannot repost transaction. Missing id or tx_id param in request."
|
||||
.to_owned(),
|
||||
)
|
||||
.into()));
|
||||
}
|
||||
|
||||
let fluff = params.get("fluff").is_some();
|
||||
Box::new(match api.post_tx(&stored_tx.unwrap(), fluff) {
|
||||
Ok(_) => ok(()),
|
||||
Err(e) => {
|
||||
error!("repost: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_post_request(&self, req: Request<Body>) -> WalletResponseFuture {
|
||||
let api = APIOwner::new(self.wallet.clone());
|
||||
match req
|
||||
.uri()
|
||||
.path()
|
||||
.trim_right_matches("/")
|
||||
.rsplit("/")
|
||||
.next()
|
||||
.unwrap()
|
||||
{
|
||||
"issue_send_tx" => Box::new(
|
||||
self.issue_send_tx(req, api)
|
||||
.and_then(|slate| ok(json_response_pretty(&slate))),
|
||||
),
|
||||
"finalize_tx" => Box::new(
|
||||
self.finalize_tx(req, api)
|
||||
.and_then(|slate| ok(json_response_pretty(&slate))),
|
||||
),
|
||||
"cancel_tx" => Box::new(
|
||||
self.cancel_tx(req, api)
|
||||
.and_then(|_| ok(response(StatusCode::OK, "{}"))),
|
||||
),
|
||||
"post_tx" => Box::new(
|
||||
self.post_tx(req, api)
|
||||
.and_then(|_| ok(response(StatusCode::OK, "{}"))),
|
||||
),
|
||||
"repost" => Box::new(
|
||||
self.repost(req, api)
|
||||
.and_then(|_| ok(response(StatusCode::OK, ""))),
|
||||
),
|
||||
_ => Box::new(err(ErrorKind::GenericError(
|
||||
"Unknown error handling post request".to_owned(),
|
||||
)
|
||||
.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized, C, K> Handler for OwnerAPIHandler<T, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn get(&self, req: Request<Body>) -> ResponseFuture {
|
||||
match self.handle_get_request(&req) {
|
||||
Ok(r) => Box::new(ok(r)),
|
||||
Err(e) => {
|
||||
error!("Request Error: {:?}", e);
|
||||
Box::new(ok(create_error_response(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn post(&self, req: Request<Body>) -> ResponseFuture {
|
||||
Box::new(
|
||||
self.handle_post_request(req)
|
||||
.and_then(|r| ok(r))
|
||||
.or_else(|e| {
|
||||
error!("Request Error: {:?}", e);
|
||||
ok(create_error_response(e))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn options(&self, _req: Request<Body>) -> ResponseFuture {
|
||||
Box::new(ok(create_ok_response("{}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// API Handler/Wrapper for foreign functions
|
||||
|
||||
pub struct ForeignAPIHandler<T: ?Sized, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// Wallet instance
|
||||
pub wallet: Arc<Mutex<T>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized, C, K> ForeignAPIHandler<T, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
/// create a new api handler
|
||||
pub fn new(wallet: Arc<Mutex<T>>) -> ForeignAPIHandler<T, C, K> {
|
||||
ForeignAPIHandler {
|
||||
wallet,
|
||||
phantom: PhantomData,
|
||||
phantom_c: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_coinbase(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
mut api: APIForeign<T, C, K>,
|
||||
) -> Box<dyn Future<Item = CbData, Error = Error> + Send> {
|
||||
Box::new(parse_body(req).and_then(move |block_fees| api.build_coinbase(&block_fees)))
|
||||
}
|
||||
|
||||
fn receive_tx(
|
||||
&self,
|
||||
req: Request<Body>,
|
||||
mut api: APIForeign<T, C, K>,
|
||||
) -> Box<dyn Future<Item = VersionedSlate, Error = Error> + Send> {
|
||||
Box::new(parse_body(req).and_then(
|
||||
//TODO: No way to insert a message from the params
|
||||
move |slate: VersionedSlate| {
|
||||
let mut slate: Slate = slate.into();
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
err(e)
|
||||
} else {
|
||||
match api.receive_tx(&mut slate, None, None) {
|
||||
Ok(_) => ok(get_versioned_slate(&slate.clone())),
|
||||
Err(e) => {
|
||||
error!("receive_tx: failed with error: {}", e);
|
||||
err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_request(&self, req: Request<Body>) -> WalletResponseFuture {
|
||||
let api = *APIForeign::new(self.wallet.clone());
|
||||
match req
|
||||
.uri()
|
||||
.path()
|
||||
.trim_right_matches("/")
|
||||
.rsplit("/")
|
||||
.next()
|
||||
.unwrap()
|
||||
{
|
||||
"build_coinbase" => Box::new(
|
||||
self.build_coinbase(req, api)
|
||||
.and_then(|res| ok(json_response(&res))),
|
||||
),
|
||||
"receive_tx" => Box::new(
|
||||
self.receive_tx(req, api)
|
||||
.and_then(|res| ok(json_response(&res))),
|
||||
),
|
||||
_ => Box::new(ok(response(StatusCode::BAD_REQUEST, "unknown action"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: ?Sized, C, K> Handler for ForeignAPIHandler<T, C, K>
|
||||
where
|
||||
T: WalletBackend<C, K> + Send + Sync + 'static,
|
||||
C: NodeClient + Send + Sync + 'static,
|
||||
K: Keychain + 'static,
|
||||
{
|
||||
fn post(&self, req: Request<Body>) -> ResponseFuture {
|
||||
Box::new(self.handle_request(req).and_then(|r| ok(r)).or_else(|e| {
|
||||
error!("Request Error: {:?}", e);
|
||||
ok(create_error_response(e))
|
||||
}))
|
||||
}
|
||||
|
||||
fn options(&self, _req: Request<Body>) -> ResponseFuture {
|
||||
Box::new(ok(create_ok_response("{}")))
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to serialize a struct into JSON and produce a sensible Response
|
||||
// out of it.
|
||||
fn json_response<T>(s: &T) -> Response<Body>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
match serde_json::to_string(s) {
|
||||
Ok(json) => response(StatusCode::OK, json),
|
||||
Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""),
|
||||
}
|
||||
}
|
||||
|
||||
// pretty-printed version of above
|
||||
fn json_response_pretty<T>(s: &T) -> Response<Body>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
match serde_json::to_string_pretty(s) {
|
||||
Ok(json) => response(StatusCode::OK, json),
|
||||
Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_error_response(e: Error) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
)
|
||||
.body(format!("{}", e).into())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn create_ok_response(json: &str) -> Response<Body> {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
)
|
||||
.header(hyper::header::CONTENT_TYPE, "application/json")
|
||||
.body(json.to_string().into())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Build a new hyper Response with the status code and body provided.
|
||||
///
|
||||
/// Whenever the status code is `StatusCode::OK` the text parameter should be
|
||||
/// valid JSON as the content type header will be set to `application/json'
|
||||
fn response<T: Into<Body>>(status: StatusCode, text: T) -> Response<Body> {
|
||||
let mut builder = &mut Response::builder();
|
||||
|
||||
builder = builder
|
||||
.status(status)
|
||||
.header("access-control-allow-origin", "*")
|
||||
.header(
|
||||
"access-control-allow-headers",
|
||||
"Content-Type, Authorization",
|
||||
);
|
||||
|
||||
if status == StatusCode::OK {
|
||||
builder = builder.header(hyper::header::CONTENT_TYPE, "application/json");
|
||||
}
|
||||
|
||||
builder.body(text.into()).unwrap()
|
||||
}
|
||||
|
||||
fn parse_params(req: &Request<Body>) -> HashMap<String, Vec<String>> {
|
||||
match req.uri().query() {
|
||||
Some(query_string) => form_urlencoded::parse(query_string.as_bytes())
|
||||
.into_owned()
|
||||
.fold(HashMap::new(), |mut hm, (k, v)| {
|
||||
hm.entry(k).or_insert(vec![]).push(v);
|
||||
hm
|
||||
}),
|
||||
None => HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_body<T>(req: Request<Body>) -> Box<dyn Future<Item = T, Error = Error> + Send>
|
||||
where
|
||||
for<'de> T: Deserialize<'de> + Send + 'static,
|
||||
{
|
||||
Box::new(
|
||||
req.into_body()
|
||||
.concat2()
|
||||
.map_err(|_| ErrorKind::GenericError("Failed to read request".to_owned()).into())
|
||||
.and_then(|body| match serde_json::from_reader(&body.to_vec()[..]) {
|
||||
Ok(obj) => ok(obj),
|
||||
Err(e) => {
|
||||
err(ErrorKind::GenericError(format!("Invalid request body: {}", e)).into())
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::core::core::{self, amount_to_hr_string};
|
||||
use crate::core::global;
|
||||
use crate::libwallet::types::{AcctPathMapping, OutputData, OutputStatus, TxLogEntry, WalletInfo};
|
||||
use crate::libwallet::Error;
|
||||
use crate::util;
|
||||
use crate::util::secp::pedersen;
|
||||
use prettytable;
|
||||
use std::io::prelude::Write;
|
||||
use term;
|
||||
|
||||
/// Display outputs in a pretty way
|
||||
pub fn outputs(
|
||||
account: &str,
|
||||
cur_height: u64,
|
||||
validated: bool,
|
||||
outputs: Vec<(OutputData, pedersen::Commitment)>,
|
||||
dark_background_color_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
let title = format!(
|
||||
"Wallet Outputs - Account '{}' - Block Height: {}",
|
||||
account, cur_height
|
||||
);
|
||||
println!();
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Output Commitment",
|
||||
bMG->"MMR Index",
|
||||
bMG->"Block Height",
|
||||
bMG->"Locked Until",
|
||||
bMG->"Status",
|
||||
bMG->"Coinbase?",
|
||||
bMG->"# Confirms",
|
||||
bMG->"Value",
|
||||
bMG->"Tx"
|
||||
]);
|
||||
|
||||
for (out, commit) in outputs {
|
||||
let commit = format!("{}", util::to_hex(commit.as_ref().to_vec()));
|
||||
let index = match out.mmr_index {
|
||||
None => "None".to_owned(),
|
||||
Some(t) => t.to_string(),
|
||||
};
|
||||
let height = format!("{}", out.height);
|
||||
let lock_height = format!("{}", out.lock_height);
|
||||
let is_coinbase = format!("{}", out.is_coinbase);
|
||||
|
||||
// Mark unconfirmed coinbase outputs as "Mining" instead of "Unconfirmed"
|
||||
let status = match out.status {
|
||||
OutputStatus::Unconfirmed if out.is_coinbase => "Mining".to_string(),
|
||||
_ => format!("{}", out.status),
|
||||
};
|
||||
|
||||
let num_confirmations = format!("{}", out.num_confirmations(cur_height));
|
||||
let value = format!("{}", core::amount_to_hr_string(out.value, false));
|
||||
let tx = match out.tx_log_entry {
|
||||
None => "".to_owned(),
|
||||
Some(t) => t.to_string(),
|
||||
};
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFR->status,
|
||||
bFY->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
bFC->tx,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->commit,
|
||||
bFB->index,
|
||||
bFB->height,
|
||||
bFB->lock_height,
|
||||
bFR->status,
|
||||
bFD->is_coinbase,
|
||||
bFB->num_confirmations,
|
||||
bFG->value,
|
||||
bFD->tx,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
|
||||
if !validated {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data. \
|
||||
The above is from local cache and possibly invalid! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display transaction log in a pretty way
|
||||
pub fn txs(
|
||||
account: &str,
|
||||
cur_height: u64,
|
||||
validated: bool,
|
||||
txs: &Vec<TxLogEntry>,
|
||||
include_status: bool,
|
||||
dark_background_color_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
let title = format!(
|
||||
"Transaction Log - Account '{}' - Block Height: {}",
|
||||
account, cur_height
|
||||
);
|
||||
println!();
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Id",
|
||||
bMG->"Type",
|
||||
bMG->"Shared Transaction Id",
|
||||
bMG->"Creation Time",
|
||||
bMG->"Confirmed?",
|
||||
bMG->"Confirmation Time",
|
||||
bMG->"Num. \nInputs",
|
||||
bMG->"Num. \nOutputs",
|
||||
bMG->"Amount \nCredited",
|
||||
bMG->"Amount \nDebited",
|
||||
bMG->"Fee",
|
||||
bMG->"Net \nDifference",
|
||||
bMG->"Tx \nData",
|
||||
]);
|
||||
|
||||
for t in txs {
|
||||
let id = format!("{}", t.id);
|
||||
let slate_id = match t.tx_slate_id {
|
||||
Some(m) => format!("{}", m),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let entry_type = format!("{}", t.tx_type);
|
||||
let creation_ts = format!("{}", t.creation_ts.format("%Y-%m-%d %H:%M:%S"));
|
||||
let confirmation_ts = match t.confirmation_ts {
|
||||
Some(m) => format!("{}", m.format("%Y-%m-%d %H:%M:%S")),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let confirmed = format!("{}", t.confirmed);
|
||||
let num_inputs = format!("{}", t.num_inputs);
|
||||
let num_outputs = format!("{}", t.num_outputs);
|
||||
let amount_debited_str = core::amount_to_hr_string(t.amount_debited, true);
|
||||
let amount_credited_str = core::amount_to_hr_string(t.amount_credited, true);
|
||||
let fee = match t.fee {
|
||||
Some(f) => format!("{}", core::amount_to_hr_string(f, true)),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let net_diff = if t.amount_credited >= t.amount_debited {
|
||||
core::amount_to_hr_string(t.amount_credited - t.amount_debited, true)
|
||||
} else {
|
||||
format!(
|
||||
"-{}",
|
||||
core::amount_to_hr_string(t.amount_debited - t.amount_credited, true)
|
||||
)
|
||||
};
|
||||
let tx_data = match t.stored_tx {
|
||||
Some(_) => "Yes".to_owned(),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->id,
|
||||
bFC->entry_type,
|
||||
bFC->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFC->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFC->num_inputs,
|
||||
bFC->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFR->amount_debited_str,
|
||||
bFR->fee,
|
||||
bFY->net_diff,
|
||||
bFb->tx_data,
|
||||
]);
|
||||
} else {
|
||||
if t.confirmed {
|
||||
table.add_row(row![
|
||||
bFD->id,
|
||||
bFb->entry_type,
|
||||
bFD->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFg->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFD->num_inputs,
|
||||
bFD->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFD->amount_debited_str,
|
||||
bFD->fee,
|
||||
bFG->net_diff,
|
||||
bFB->tx_data,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->id,
|
||||
bFb->entry_type,
|
||||
bFD->slate_id,
|
||||
bFB->creation_ts,
|
||||
bFR->confirmed,
|
||||
bFB->confirmation_ts,
|
||||
bFD->num_inputs,
|
||||
bFD->num_outputs,
|
||||
bFG->amount_credited_str,
|
||||
bFD->amount_debited_str,
|
||||
bFD->fee,
|
||||
bFG->net_diff,
|
||||
bFB->tx_data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
|
||||
if !validated && include_status {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data. \
|
||||
The above is from local cache and possibly invalid! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Display summary info in a pretty way
|
||||
pub fn info(
|
||||
account: &str,
|
||||
wallet_info: &WalletInfo,
|
||||
validated: bool,
|
||||
dark_background_color_scheme: bool,
|
||||
) {
|
||||
println!(
|
||||
"\n____ Wallet Summary Info - Account '{}' as of height {} ____\n",
|
||||
account, wallet_info.last_confirmed_height,
|
||||
);
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFG->"Total",
|
||||
FG->amount_to_hr_string(wallet_info.total, false)
|
||||
]);
|
||||
// Only dispay "Immature Coinbase" if we have related outputs in the wallet.
|
||||
// This row just introduces confusion if the wallet does not receive coinbase rewards.
|
||||
if wallet_info.amount_immature > 0 {
|
||||
table.add_row(row![
|
||||
bFY->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
|
||||
FY->amount_to_hr_string(wallet_info.amount_immature, false)
|
||||
]);
|
||||
}
|
||||
table.add_row(row![
|
||||
bFY->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
|
||||
FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fr->"Locked by previous transaction",
|
||||
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fw->"--------------------------------",
|
||||
Fw->"-------------"
|
||||
]);
|
||||
table.add_row(row![
|
||||
bFG->"Currently Spendable",
|
||||
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFG->"Total",
|
||||
FG->amount_to_hr_string(wallet_info.total, false)
|
||||
]);
|
||||
// Only dispay "Immature Coinbase" if we have related outputs in the wallet.
|
||||
// This row just introduces confusion if the wallet does not receive coinbase rewards.
|
||||
if wallet_info.amount_immature > 0 {
|
||||
table.add_row(row![
|
||||
bFB->format!("Immature Coinbase (< {})", global::coinbase_maturity()),
|
||||
FB->amount_to_hr_string(wallet_info.amount_immature, false)
|
||||
]);
|
||||
}
|
||||
table.add_row(row![
|
||||
bFB->format!("Awaiting Confirmation (< {})", wallet_info.minimum_confirmations),
|
||||
FB->amount_to_hr_string(wallet_info.amount_awaiting_confirmation, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fr->"Locked by previous transaction",
|
||||
Fr->amount_to_hr_string(wallet_info.amount_locked, false)
|
||||
]);
|
||||
table.add_row(row![
|
||||
Fw->"--------------------------------",
|
||||
Fw->"-------------"
|
||||
]);
|
||||
table.add_row(row![
|
||||
bFG->"Currently Spendable",
|
||||
FG->amount_to_hr_string(wallet_info.amount_currently_spendable, false)
|
||||
]);
|
||||
};
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.printstd();
|
||||
println!();
|
||||
if !validated {
|
||||
println!(
|
||||
"\nWARNING: Wallet failed to verify data against a live chain. \
|
||||
The above is from local cache and only valid up to the given height! \
|
||||
(is your `grin server` offline or broken?)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display summary info in a pretty way
|
||||
pub fn estimate(
|
||||
amount: u64,
|
||||
strategies: Vec<(
|
||||
&str, // strategy
|
||||
u64, // total amount to be locked
|
||||
u64, // fee
|
||||
)>,
|
||||
dark_background_color_scheme: bool,
|
||||
) {
|
||||
println!(
|
||||
"\nEstimation for sending {}:\n",
|
||||
amount_to_hr_string(amount, false)
|
||||
);
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Selection strategy",
|
||||
bMG->"Fee",
|
||||
bMG->"Will be locked",
|
||||
]);
|
||||
|
||||
for (strategy, total, fee) in strategies {
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->strategy,
|
||||
FR->amount_to_hr_string(fee, false),
|
||||
FY->amount_to_hr_string(total, false),
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->strategy,
|
||||
FR->amount_to_hr_string(fee, false),
|
||||
FY->amount_to_hr_string(total, false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
table.printstd();
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Display list of wallet accounts in a pretty way
|
||||
pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
|
||||
println!("\n____ Wallet Accounts ____\n",);
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
mMG->"Name",
|
||||
bMG->"Parent BIP-32 Derivation Path",
|
||||
]);
|
||||
for m in acct_mappings {
|
||||
table.add_row(row![
|
||||
bFC->m.label,
|
||||
bGC->m.path.to_bip_32_string(),
|
||||
]);
|
||||
}
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
|
||||
table.printstd();
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Display transaction log messages
|
||||
pub fn tx_messages(tx: &TxLogEntry, dark_background_color_scheme: bool) -> Result<(), Error> {
|
||||
let title = format!("Transaction Messages - Transaction '{}'", tx.id,);
|
||||
println!();
|
||||
let mut t = term::stdout().unwrap();
|
||||
t.fg(term::color::MAGENTA).unwrap();
|
||||
writeln!(t, "{}", title).unwrap();
|
||||
t.reset().unwrap();
|
||||
|
||||
let msgs = match tx.messages.clone() {
|
||||
None => {
|
||||
writeln!(t, "{}", "None").unwrap();
|
||||
t.reset().unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
Some(m) => m.clone(),
|
||||
};
|
||||
|
||||
if msgs.messages.is_empty() {
|
||||
writeln!(t, "{}", "None").unwrap();
|
||||
t.reset().unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut table = table!();
|
||||
|
||||
table.set_titles(row![
|
||||
bMG->"Participant Id",
|
||||
bMG->"Message",
|
||||
bMG->"Public Key",
|
||||
bMG->"Signature",
|
||||
]);
|
||||
|
||||
let secp = util::static_secp_instance();
|
||||
let secp_lock = secp.lock();
|
||||
|
||||
for m in msgs.messages {
|
||||
let id = format!("{}", m.id);
|
||||
let public_key = format!(
|
||||
"{}",
|
||||
util::to_hex(m.public_key.serialize_vec(&secp_lock, true).to_vec())
|
||||
);
|
||||
let message = match m.message {
|
||||
Some(m) => format!("{}", m),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
let message_sig = match m.message_sig {
|
||||
Some(s) => format!("{}", util::to_hex(s.serialize_der(&secp_lock))),
|
||||
None => "None".to_owned(),
|
||||
};
|
||||
if dark_background_color_scheme {
|
||||
table.add_row(row![
|
||||
bFC->id,
|
||||
bFC->message,
|
||||
bFC->public_key,
|
||||
bFB->message_sig,
|
||||
]);
|
||||
} else {
|
||||
table.add_row(row![
|
||||
bFD->id,
|
||||
bFb->message,
|
||||
bFD->public_key,
|
||||
bFB->message_sig,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
|
||||
table.printstd();
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementation specific error types
|
||||
use crate::api;
|
||||
use crate::core::core::transaction;
|
||||
use crate::core::libtx;
|
||||
use crate::keychain;
|
||||
use crate::libwallet;
|
||||
use failure::{Backtrace, Context, Fail};
|
||||
use std::env;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
/// Error definition
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
pub inner: Context<ErrorKind>,
|
||||
}
|
||||
|
||||
/// Wallet errors, mostly wrappers around underlying crypto or I/O errors.
|
||||
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
|
||||
pub enum ErrorKind {
|
||||
/// LibTX Error
|
||||
#[fail(display = "LibTx Error")]
|
||||
LibTX(libtx::ErrorKind),
|
||||
|
||||
/// LibWallet Error
|
||||
#[fail(display = "LibWallet Error: {}", _1)]
|
||||
LibWallet(libwallet::ErrorKind, String),
|
||||
|
||||
/// Keychain error
|
||||
#[fail(display = "Keychain error")]
|
||||
Keychain(keychain::Error),
|
||||
|
||||
/// Transaction Error
|
||||
#[fail(display = "Transaction error")]
|
||||
Transaction(transaction::Error),
|
||||
|
||||
/// Secp Error
|
||||
#[fail(display = "Secp error")]
|
||||
Secp,
|
||||
|
||||
/// Filewallet error
|
||||
#[fail(display = "Wallet data error: {}", _0)]
|
||||
FileWallet(&'static str),
|
||||
|
||||
/// Error when formatting json
|
||||
#[fail(display = "IO error")]
|
||||
IO,
|
||||
|
||||
/// Error when formatting json
|
||||
#[fail(display = "Serde JSON error")]
|
||||
Format,
|
||||
|
||||
/// Error when contacting a node through its API
|
||||
#[fail(display = "Node API error")]
|
||||
Node(api::ErrorKind),
|
||||
|
||||
/// Error originating from hyper.
|
||||
#[fail(display = "Hyper error")]
|
||||
Hyper,
|
||||
|
||||
/// Error originating from hyper uri parsing.
|
||||
#[fail(display = "Uri parsing error")]
|
||||
Uri,
|
||||
|
||||
/// Attempt to use duplicate transaction id in separate transactions
|
||||
#[fail(display = "Duplicate transaction ID error")]
|
||||
DuplicateTransactionId,
|
||||
|
||||
/// Wallet seed already exists
|
||||
#[fail(display = "Wallet seed file exists: {}", _0)]
|
||||
WalletSeedExists(String),
|
||||
|
||||
/// Wallet seed doesn't exist
|
||||
#[fail(display = "Wallet seed doesn't exist error")]
|
||||
WalletSeedDoesntExist,
|
||||
|
||||
/// Enc/Decryption Error
|
||||
#[fail(display = "Enc/Decryption error (check password?)")]
|
||||
Encryption,
|
||||
|
||||
/// BIP 39 word list
|
||||
#[fail(display = "BIP39 Mnemonic (word list) Error")]
|
||||
Mnemonic,
|
||||
|
||||
/// Command line argument error
|
||||
#[fail(display = "{}", _0)]
|
||||
ArgumentError(String),
|
||||
|
||||
/// Other
|
||||
#[fail(display = "Generic error: {}", _0)]
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Fail for Error {
|
||||
fn cause(&self) -> Option<&dyn Fail> {
|
||||
self.inner.cause()
|
||||
}
|
||||
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.inner.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let show_bt = match env::var("RUST_BACKTRACE") {
|
||||
Ok(r) => {
|
||||
if r == "1" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
let backtrace = match self.backtrace() {
|
||||
Some(b) => format!("{}", b),
|
||||
None => String::from("Unknown"),
|
||||
};
|
||||
let inner_output = format!("{}", self.inner,);
|
||||
let backtrace_output = format!("\nBacktrace: {}", backtrace);
|
||||
let mut output = inner_output.clone();
|
||||
if show_bt {
|
||||
output.push_str(&backtrace_output);
|
||||
}
|
||||
Display::fmt(&output, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// get kind
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
self.inner.get_context().clone()
|
||||
}
|
||||
/// get cause
|
||||
pub fn cause(&self) -> Option<&dyn Fail> {
|
||||
self.inner.cause()
|
||||
}
|
||||
/// get backtrace
|
||||
pub fn backtrace(&self) -> Option<&Backtrace> {
|
||||
self.inner.backtrace()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Error {
|
||||
Error {
|
||||
inner: Context::new(kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(inner: Context<ErrorKind>) -> Error {
|
||||
Error { inner: inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::Error> for Error {
|
||||
fn from(error: api::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Node(error.kind().clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<keychain::Error> for Error {
|
||||
fn from(error: keychain::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Keychain(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<transaction::Error> for Error {
|
||||
fn from(error: transaction::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::Transaction(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libwallet::Error> for Error {
|
||||
fn from(error: libwallet::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::LibWallet(error.kind(), format!("{}", error))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libtx::Error> for Error {
|
||||
fn from(error: libtx::Error) -> Error {
|
||||
Error {
|
||||
inner: Context::new(ErrorKind::LibTX(error.kind())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Library module for the main wallet functionalities provided by Grin.
|
||||
|
||||
use blake2_rfc as blake2;
|
||||
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use failure;
|
||||
use grin_api as api;
|
||||
#[macro_use]
|
||||
extern crate grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_store as store;
|
||||
use grin_util as util;
|
||||
|
||||
mod adapters;
|
||||
pub mod command;
|
||||
pub mod controller;
|
||||
pub mod display;
|
||||
mod error;
|
||||
pub mod libwallet;
|
||||
pub mod lmdb_wallet;
|
||||
mod node_clients;
|
||||
pub mod test_framework;
|
||||
mod types;
|
||||
|
||||
pub use crate::adapters::{
|
||||
FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter, NullWalletCommAdapter,
|
||||
WalletCommAdapter,
|
||||
};
|
||||
pub use crate::error::{Error, ErrorKind};
|
||||
pub use crate::libwallet::slate::Slate;
|
||||
pub use crate::libwallet::types::{
|
||||
BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst,
|
||||
};
|
||||
pub use crate::lmdb_wallet::{wallet_db_exists, LMDBBackend};
|
||||
pub use crate::node_clients::{create_coinbase, HTTPNodeClient};
|
||||
pub use crate::types::{EncryptedWalletSeed, WalletConfig, WalletSeed, SEED_FILE};
|
||||
|
||||
use crate::util::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Helper to create an instance of the LMDB wallet
|
||||
pub fn instantiate_wallet(
|
||||
wallet_config: WalletConfig,
|
||||
node_client: impl NodeClient + 'static,
|
||||
passphrase: &str,
|
||||
account: &str,
|
||||
) -> Result<Arc<Mutex<WalletInst<impl NodeClient, keychain::ExtKeychain>>>, Error> {
|
||||
// First test decryption, so we can abort early if we have the wrong password
|
||||
let _ = WalletSeed::from_file(&wallet_config, passphrase)?;
|
||||
let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, node_client)?;
|
||||
db_wallet.set_parent_key_id_by_name(account)?;
|
||||
info!("Using LMDB Backend for wallet");
|
||||
Ok(Arc::new(Mutex::new(db_wallet)))
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, path};
|
||||
|
||||
// for writing storedtransaction files
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use failure::ResultExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::blake2::blake2b::Blake2b;
|
||||
|
||||
use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain};
|
||||
use crate::store::{self, option_to_not_found, to_key, to_key_u64};
|
||||
|
||||
use crate::core::core::Transaction;
|
||||
use crate::core::{global, ser};
|
||||
use crate::libwallet::types::*;
|
||||
use crate::libwallet::{internal, Error, ErrorKind};
|
||||
use crate::types::{WalletConfig, WalletSeed};
|
||||
use crate::util;
|
||||
use crate::util::secp::constants::SECRET_KEY_SIZE;
|
||||
use crate::util::ZeroingString;
|
||||
|
||||
pub const DB_DIR: &'static str = "db";
|
||||
pub const TX_SAVE_DIR: &'static str = "saved_txs";
|
||||
|
||||
const OUTPUT_PREFIX: u8 = 'o' as u8;
|
||||
const DERIV_PREFIX: u8 = 'd' as u8;
|
||||
const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8;
|
||||
const PRIVATE_TX_CONTEXT_PREFIX: u8 = 'p' as u8;
|
||||
const TX_LOG_ENTRY_PREFIX: u8 = 't' as u8;
|
||||
const TX_LOG_ID_PREFIX: u8 = 'i' as u8;
|
||||
const ACCOUNT_PATH_MAPPING_PREFIX: u8 = 'a' as u8;
|
||||
|
||||
impl From<store::Error> for Error {
|
||||
fn from(error: store::Error) -> Error {
|
||||
Error::from(ErrorKind::Backend(format!("{}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
/// test to see if database files exist in the current directory. If so,
|
||||
/// use a DB backend for all operations
|
||||
pub fn wallet_db_exists(config: WalletConfig) -> bool {
|
||||
let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR);
|
||||
db_path.exists()
|
||||
}
|
||||
|
||||
/// Helper to derive XOR keys for storing private transaction keys in the DB
|
||||
/// (blind_xor_key, nonce_xor_key)
|
||||
fn private_ctx_xor_keys<K>(
|
||||
keychain: &K,
|
||||
slate_id: &[u8],
|
||||
) -> Result<([u8; SECRET_KEY_SIZE], [u8; SECRET_KEY_SIZE]), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
{
|
||||
let root_key = keychain.derive_key(0, &K::root_key_id())?;
|
||||
|
||||
// derive XOR values for storing secret values in DB
|
||||
// h(root_key|slate_id|"blind")
|
||||
let mut hasher = Blake2b::new(SECRET_KEY_SIZE);
|
||||
hasher.update(&root_key.0[..]);
|
||||
hasher.update(&slate_id[..]);
|
||||
hasher.update(&"blind".as_bytes()[..]);
|
||||
let blind_xor_key = hasher.finalize();
|
||||
let mut ret_blind = [0; SECRET_KEY_SIZE];
|
||||
ret_blind.copy_from_slice(&blind_xor_key.as_bytes()[0..SECRET_KEY_SIZE]);
|
||||
|
||||
// h(root_key|slate_id|"nonce")
|
||||
let mut hasher = Blake2b::new(SECRET_KEY_SIZE);
|
||||
hasher.update(&root_key.0[..]);
|
||||
hasher.update(&slate_id[..]);
|
||||
hasher.update(&"nonce".as_bytes()[..]);
|
||||
let nonce_xor_key = hasher.finalize();
|
||||
let mut ret_nonce = [0; SECRET_KEY_SIZE];
|
||||
ret_nonce.copy_from_slice(&nonce_xor_key.as_bytes()[0..SECRET_KEY_SIZE]);
|
||||
|
||||
Ok((ret_blind, ret_nonce))
|
||||
}
|
||||
|
||||
pub struct LMDBBackend<C, K> {
|
||||
db: store::Store,
|
||||
config: WalletConfig,
|
||||
/// passphrase: TODO better ways of dealing with this other than storing
|
||||
passphrase: ZeroingString,
|
||||
/// Keychain
|
||||
pub keychain: Option<K>,
|
||||
/// Parent path to use by default for output operations
|
||||
parent_key_id: Identifier,
|
||||
/// wallet to node client
|
||||
w2n_client: C,
|
||||
}
|
||||
|
||||
impl<C, K> LMDBBackend<C, K> {
|
||||
pub fn new(config: WalletConfig, passphrase: &str, n_client: C) -> Result<Self, Error> {
|
||||
let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR);
|
||||
fs::create_dir_all(&db_path).expect("Couldn't create wallet backend directory!");
|
||||
|
||||
let stored_tx_path = path::Path::new(&config.data_file_dir).join(TX_SAVE_DIR);
|
||||
fs::create_dir_all(&stored_tx_path)
|
||||
.expect("Couldn't create wallet backend tx storage directory!");
|
||||
|
||||
let lmdb_env = Arc::new(store::new_env(db_path.to_str().unwrap().to_string()));
|
||||
let store = store::Store::open(lmdb_env, DB_DIR);
|
||||
|
||||
// Make sure default wallet derivation path always exists
|
||||
// as well as path (so it can be retrieved by batches to know where to store
|
||||
// completed transactions, for reference
|
||||
let default_account = AcctPathMapping {
|
||||
label: "default".to_owned(),
|
||||
path: LMDBBackend::<C, K>::default_path(),
|
||||
};
|
||||
let acct_key = to_key(
|
||||
ACCOUNT_PATH_MAPPING_PREFIX,
|
||||
&mut default_account.label.as_bytes().to_vec(),
|
||||
);
|
||||
|
||||
{
|
||||
let batch = store.batch()?;
|
||||
batch.put_ser(&acct_key, &default_account)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
|
||||
let res = LMDBBackend {
|
||||
db: store,
|
||||
config: config.clone(),
|
||||
passphrase: ZeroingString::from(passphrase),
|
||||
keychain: None,
|
||||
parent_key_id: LMDBBackend::<C, K>::default_path(),
|
||||
w2n_client: n_client,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn default_path() -> Identifier {
|
||||
// return the default parent wallet path, corresponding to the default account
|
||||
// in the BIP32 spec. Parent is account 0 at level 2, child output identifiers
|
||||
// are all at level 3
|
||||
ExtKeychain::derive_key_id(2, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
/// Just test to see if database files exist in the current directory. If
|
||||
/// so, use a DB backend for all operations
|
||||
pub fn exists(config: WalletConfig) -> bool {
|
||||
let db_path = path::Path::new(&config.data_file_dir).join(DB_DIR);
|
||||
db_path.exists()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, K> WalletBackend<C, K> for LMDBBackend<C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Initialise with whatever stored credentials we have
|
||||
fn open_with_credentials(&mut self) -> Result<(), Error> {
|
||||
let wallet_seed = WalletSeed::from_file(&self.config, &self.passphrase)
|
||||
.context(ErrorKind::CallbackImpl("Error opening wallet"))?;
|
||||
self.keychain = Some(
|
||||
wallet_seed
|
||||
.derive_keychain(global::is_floonet())
|
||||
.context(ErrorKind::CallbackImpl("Error deriving keychain"))?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close wallet and remove any stored credentials (TBD)
|
||||
fn close(&mut self) -> Result<(), Error> {
|
||||
self.keychain = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the keychain being used
|
||||
fn keychain(&mut self) -> &mut K {
|
||||
self.keychain.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Return the node client being used
|
||||
fn w2n_client(&mut self) -> &mut C {
|
||||
&mut self.w2n_client
|
||||
}
|
||||
|
||||
/// return the version of the commit for caching
|
||||
fn calc_commit_for_cache(
|
||||
&mut self,
|
||||
amount: u64,
|
||||
id: &Identifier,
|
||||
) -> Result<Option<String>, Error> {
|
||||
if self.config.no_commit_cache == Some(true) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(util::to_hex(
|
||||
self.keychain().commit(amount, &id)?.0.to_vec(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set parent path by account name
|
||||
fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> {
|
||||
let label = label.to_owned();
|
||||
let res = self.acct_path_iter().find(|l| l.label == label);
|
||||
if let Some(a) = res {
|
||||
self.set_parent_key_id(a.path);
|
||||
Ok(())
|
||||
} else {
|
||||
return Err(ErrorKind::UnknownAccountLabel(label.clone()).into());
|
||||
}
|
||||
}
|
||||
|
||||
/// set parent path
|
||||
fn set_parent_key_id(&mut self, id: Identifier) {
|
||||
self.parent_key_id = id;
|
||||
}
|
||||
|
||||
fn parent_key_id(&mut self) -> Identifier {
|
||||
self.parent_key_id.clone()
|
||||
}
|
||||
|
||||
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error> {
|
||||
let key = match mmr_index {
|
||||
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i),
|
||||
None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()),
|
||||
};
|
||||
option_to_not_found(self.db.get_ser(&key), &format!("Key Id: {}", id)).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = OutputData> + 'a> {
|
||||
Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap())
|
||||
}
|
||||
|
||||
fn get_tx_log_entry(&self, u: &Uuid) -> Result<Option<TxLogEntry>, Error> {
|
||||
let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec());
|
||||
self.db.get_ser(&key).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn tx_log_iter<'a>(&'a self) -> Box<dyn Iterator<Item = TxLogEntry> + 'a> {
|
||||
Box::new(self.db.iter(&[TX_LOG_ENTRY_PREFIX]).unwrap())
|
||||
}
|
||||
|
||||
fn get_private_context(&mut self, slate_id: &[u8]) -> Result<Context, Error> {
|
||||
let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec());
|
||||
let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?;
|
||||
|
||||
let mut ctx: Context = option_to_not_found(
|
||||
self.db.get_ser(&ctx_key),
|
||||
&format!("Slate id: {:x?}", slate_id.to_vec()),
|
||||
)?;
|
||||
|
||||
for i in 0..SECRET_KEY_SIZE {
|
||||
ctx.sec_key.0[i] = ctx.sec_key.0[i] ^ blind_xor_key[i];
|
||||
ctx.sec_nonce.0[i] = ctx.sec_nonce.0[i] ^ nonce_xor_key[i];
|
||||
}
|
||||
|
||||
Ok(ctx)
|
||||
}
|
||||
|
||||
fn acct_path_iter<'a>(&'a self) -> Box<dyn Iterator<Item = AcctPathMapping> + 'a> {
|
||||
Box::new(self.db.iter(&[ACCOUNT_PATH_MAPPING_PREFIX]).unwrap())
|
||||
}
|
||||
|
||||
fn get_acct_path(&self, label: String) -> Result<Option<AcctPathMapping>, Error> {
|
||||
let acct_key = to_key(ACCOUNT_PATH_MAPPING_PREFIX, &mut label.as_bytes().to_vec());
|
||||
self.db.get_ser(&acct_key).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn store_tx(&self, uuid: &str, tx: &Transaction) -> Result<(), Error> {
|
||||
let filename = format!("{}.grintx", uuid);
|
||||
let path = path::Path::new(&self.config.data_file_dir)
|
||||
.join(TX_SAVE_DIR)
|
||||
.join(filename);
|
||||
let path_buf = Path::new(&path).to_path_buf();
|
||||
let mut stored_tx = File::create(path_buf)?;
|
||||
let tx_hex = util::to_hex(ser::ser_vec(tx).unwrap());;
|
||||
stored_tx.write_all(&tx_hex.as_bytes())?;
|
||||
stored_tx.sync_all()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_stored_tx(&self, entry: &TxLogEntry) -> Result<Option<Transaction>, Error> {
|
||||
let filename = match entry.stored_tx.clone() {
|
||||
Some(f) => f,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let path = path::Path::new(&self.config.data_file_dir)
|
||||
.join(TX_SAVE_DIR)
|
||||
.join(filename);
|
||||
let tx_file = Path::new(&path).to_path_buf();
|
||||
let mut tx_f = File::open(tx_file)?;
|
||||
let mut content = String::new();
|
||||
tx_f.read_to_string(&mut content)?;
|
||||
let tx_bin = util::from_hex(content).unwrap();
|
||||
Ok(Some(
|
||||
ser::deserialize::<Transaction>(&mut &tx_bin[..]).unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
fn batch<'a>(&'a mut self) -> Result<Box<dyn WalletOutputBatch<K> + 'a>, Error> {
|
||||
Ok(Box::new(Batch {
|
||||
_store: self,
|
||||
db: RefCell::new(Some(self.db.batch()?)),
|
||||
keychain: self.keychain.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn next_child<'a>(&mut self) -> Result<Identifier, Error> {
|
||||
let parent_key_id = self.parent_key_id.clone();
|
||||
let mut deriv_idx = {
|
||||
let batch = self.db.batch()?;
|
||||
let deriv_key = to_key(DERIV_PREFIX, &mut self.parent_key_id.to_bytes().to_vec());
|
||||
match batch.get_ser(&deriv_key)? {
|
||||
Some(idx) => idx,
|
||||
None => 0,
|
||||
}
|
||||
};
|
||||
let mut return_path = self.parent_key_id.to_path();
|
||||
return_path.depth = return_path.depth + 1;
|
||||
return_path.path[return_path.depth as usize - 1] = ChildNumber::from(deriv_idx);
|
||||
deriv_idx = deriv_idx + 1;
|
||||
let mut batch = self.batch()?;
|
||||
batch.save_child_index(&parent_key_id, deriv_idx)?;
|
||||
batch.commit()?;
|
||||
Ok(Identifier::from_path(&return_path))
|
||||
}
|
||||
|
||||
fn last_confirmed_height<'a>(&mut self) -> Result<u64, Error> {
|
||||
let batch = self.db.batch()?;
|
||||
let height_key = to_key(
|
||||
CONFIRMED_HEIGHT_PREFIX,
|
||||
&mut self.parent_key_id.to_bytes().to_vec(),
|
||||
);
|
||||
let last_confirmed_height = match batch.get_ser(&height_key)? {
|
||||
Some(h) => h,
|
||||
None => 0,
|
||||
};
|
||||
Ok(last_confirmed_height)
|
||||
}
|
||||
|
||||
fn restore(&mut self) -> Result<(), Error> {
|
||||
internal::restore::restore(self).context(ErrorKind::Restore)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_repair(&mut self) -> Result<(), Error> {
|
||||
internal::restore::check_repair(self).context(ErrorKind::Restore)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An atomic batch in which all changes can be committed all at once or
|
||||
/// discarded on error.
|
||||
pub struct Batch<'a, C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
_store: &'a LMDBBackend<C, K>,
|
||||
db: RefCell<Option<store::Batch<'a>>>,
|
||||
/// Keychain
|
||||
keychain: Option<K>,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl<'a, C, K> WalletOutputBatch<K> for Batch<'a, C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
fn keychain(&mut self) -> &mut K {
|
||||
self.keychain.as_mut().unwrap()
|
||||
}
|
||||
|
||||
fn save(&mut self, out: OutputData) -> Result<(), Error> {
|
||||
// Save the output data to the db.
|
||||
{
|
||||
let key = match out.mmr_index {
|
||||
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec(), i),
|
||||
None => to_key(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec()),
|
||||
};
|
||||
self.db.borrow().as_ref().unwrap().put_ser(&key, &out)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&self, id: &Identifier, mmr_index: &Option<u64>) -> Result<OutputData, Error> {
|
||||
let key = match mmr_index {
|
||||
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i),
|
||||
None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()),
|
||||
};
|
||||
option_to_not_found(
|
||||
self.db.borrow().as_ref().unwrap().get_ser(&key),
|
||||
&format!("Key ID: {}", id),
|
||||
)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn iter(&self) -> Box<dyn Iterator<Item = OutputData>> {
|
||||
Box::new(
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter(&[OUTPUT_PREFIX])
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn delete(&mut self, id: &Identifier, mmr_index: &Option<u64>) -> Result<(), Error> {
|
||||
// Delete the output data.
|
||||
{
|
||||
let key = match mmr_index {
|
||||
Some(i) => to_key_u64(OUTPUT_PREFIX, &mut id.to_bytes().to_vec(), *i),
|
||||
None => to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()),
|
||||
};
|
||||
let _ = self.db.borrow().as_ref().unwrap().delete(&key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_tx_log_id(&mut self, parent_key_id: &Identifier) -> Result<u32, Error> {
|
||||
let tx_id_key = to_key(TX_LOG_ID_PREFIX, &mut parent_key_id.to_bytes().to_vec());
|
||||
let last_tx_log_id = match self.db.borrow().as_ref().unwrap().get_ser(&tx_id_key)? {
|
||||
Some(t) => t,
|
||||
None => 0,
|
||||
};
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&tx_id_key, &(last_tx_log_id + 1))?;
|
||||
Ok(last_tx_log_id)
|
||||
}
|
||||
|
||||
fn tx_log_iter(&self) -> Box<dyn Iterator<Item = TxLogEntry>> {
|
||||
Box::new(
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter(&[TX_LOG_ENTRY_PREFIX])
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn save_last_confirmed_height(
|
||||
&mut self,
|
||||
parent_key_id: &Identifier,
|
||||
height: u64,
|
||||
) -> Result<(), Error> {
|
||||
let height_key = to_key(
|
||||
CONFIRMED_HEIGHT_PREFIX,
|
||||
&mut parent_key_id.to_bytes().to_vec(),
|
||||
);
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&height_key, &height)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_child_index(&mut self, parent_id: &Identifier, child_n: u32) -> Result<(), Error> {
|
||||
let deriv_key = to_key(DERIV_PREFIX, &mut parent_id.to_bytes().to_vec());
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&deriv_key, &child_n)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_tx_log_entry(
|
||||
&mut self,
|
||||
tx_in: TxLogEntry,
|
||||
parent_id: &Identifier,
|
||||
) -> Result<(), Error> {
|
||||
let tx_log_key = to_key_u64(
|
||||
TX_LOG_ENTRY_PREFIX,
|
||||
&mut parent_id.to_bytes().to_vec(),
|
||||
tx_in.id as u64,
|
||||
);
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&tx_log_key, &tx_in)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> {
|
||||
let acct_key = to_key(
|
||||
ACCOUNT_PATH_MAPPING_PREFIX,
|
||||
&mut mapping.label.as_bytes().to_vec(),
|
||||
);
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&acct_key, &mapping)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn acct_path_iter(&self) -> Box<dyn Iterator<Item = AcctPathMapping>> {
|
||||
Box::new(
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter(&[ACCOUNT_PATH_MAPPING_PREFIX])
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> {
|
||||
out.lock();
|
||||
self.save(out.clone())
|
||||
}
|
||||
|
||||
fn save_private_context(&mut self, slate_id: &[u8], ctx: &Context) -> Result<(), Error> {
|
||||
let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec());
|
||||
let (blind_xor_key, nonce_xor_key) = private_ctx_xor_keys(self.keychain(), slate_id)?;
|
||||
|
||||
let mut s_ctx = ctx.clone();
|
||||
for i in 0..SECRET_KEY_SIZE {
|
||||
s_ctx.sec_key.0[i] = s_ctx.sec_key.0[i] ^ blind_xor_key[i];
|
||||
s_ctx.sec_nonce.0[i] = s_ctx.sec_nonce.0[i] ^ nonce_xor_key[i];
|
||||
}
|
||||
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.put_ser(&ctx_key, &s_ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_private_context(&mut self, slate_id: &[u8]) -> Result<(), Error> {
|
||||
let ctx_key = to_key(PRIVATE_TX_CONTEXT_PREFIX, &mut slate_id.to_vec());
|
||||
self.db
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.delete(&ctx_key)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn commit(&self) -> Result<(), Error> {
|
||||
let db = self.db.replace(None);
|
||||
db.unwrap().commit()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Client functions, implementations of the NodeClient trait
|
||||
//! specific to the FileWallet
|
||||
|
||||
use failure::ResultExt;
|
||||
use futures::{stream, Stream};
|
||||
|
||||
use crate::libwallet::types::*;
|
||||
use std::collections::HashMap;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::api;
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::libwallet;
|
||||
use crate::util;
|
||||
use crate::util::secp::pedersen;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HTTPNodeClient {
|
||||
node_url: String,
|
||||
node_api_secret: Option<String>,
|
||||
}
|
||||
|
||||
impl HTTPNodeClient {
|
||||
/// Create a new client that will communicate with the given grin node
|
||||
pub fn new(node_url: &str, node_api_secret: Option<String>) -> HTTPNodeClient {
|
||||
HTTPNodeClient {
|
||||
node_url: node_url.to_owned(),
|
||||
node_api_secret: node_api_secret,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeClient for HTTPNodeClient {
|
||||
fn node_url(&self) -> &str {
|
||||
&self.node_url
|
||||
}
|
||||
fn node_api_secret(&self) -> Option<String> {
|
||||
self.node_api_secret.clone()
|
||||
}
|
||||
|
||||
fn set_node_url(&mut self, node_url: &str) {
|
||||
self.node_url = node_url.to_owned();
|
||||
}
|
||||
|
||||
fn set_node_api_secret(&mut self, node_api_secret: Option<String>) {
|
||||
self.node_api_secret = node_api_secret;
|
||||
}
|
||||
|
||||
/// Posts a transaction to a grin node
|
||||
fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> {
|
||||
let url;
|
||||
let dest = self.node_url();
|
||||
if fluff {
|
||||
url = format!("{}/v1/pool/push?fluff", dest);
|
||||
} else {
|
||||
url = format!("{}/v1/pool/push", dest);
|
||||
}
|
||||
let res = api::client::post_no_ret(url.as_str(), self.node_api_secret(), tx);
|
||||
if let Err(e) = res {
|
||||
let report = format!("Posting transaction to node: {}", e);
|
||||
error!("Post TX Error: {}", e);
|
||||
return Err(libwallet::ErrorKind::ClientCallback(report).into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the chain tip from a given node
|
||||
fn get_chain_height(&self) -> Result<u64, libwallet::Error> {
|
||||
let addr = self.node_url();
|
||||
let url = format!("{}/v1/chain", addr);
|
||||
let res = api::client::get::<api::Tip>(url.as_str(), self.node_api_secret());
|
||||
match res {
|
||||
Err(e) => {
|
||||
let report = format!("Getting chain height from node: {}", e);
|
||||
error!("Get chain height error: {}", e);
|
||||
Err(libwallet::ErrorKind::ClientCallback(report).into())
|
||||
}
|
||||
Ok(r) => Ok(r.height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve outputs from node
|
||||
fn get_outputs_from_node(
|
||||
&self,
|
||||
wallet_outputs: Vec<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, (String, u64, u64)>, libwallet::Error> {
|
||||
let addr = self.node_url();
|
||||
// build the necessary query params -
|
||||
// ?id=xxx&id=yyy&id=zzz
|
||||
let query_params: Vec<String> = wallet_outputs
|
||||
.iter()
|
||||
.map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec())))
|
||||
.collect();
|
||||
|
||||
// build a map of api outputs by commit so we can look them up efficiently
|
||||
let mut api_outputs: HashMap<pedersen::Commitment, (String, u64, u64)> = HashMap::new();
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for query_chunk in query_params.chunks(500) {
|
||||
let url = format!("{}/v1/chain/outputs/byids?{}", addr, query_chunk.join("&"),);
|
||||
tasks.push(api::client::get_async::<Vec<api::Output>>(
|
||||
url.as_str(),
|
||||
self.node_api_secret(),
|
||||
));
|
||||
}
|
||||
|
||||
let task = stream::futures_unordered(tasks).collect();
|
||||
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let results = match rt.block_on(task) {
|
||||
Ok(outputs) => outputs,
|
||||
Err(e) => {
|
||||
let report = format!("Getting outputs by id: {}", e);
|
||||
error!("Outputs by id failed: {}", e);
|
||||
return Err(libwallet::ErrorKind::ClientCallback(report).into());
|
||||
}
|
||||
};
|
||||
|
||||
for res in results {
|
||||
for out in res {
|
||||
api_outputs.insert(
|
||||
out.commit.commit(),
|
||||
(util::to_hex(out.commit.to_vec()), out.height, out.mmr_index),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(api_outputs)
|
||||
}
|
||||
|
||||
fn get_outputs_by_pmmr_index(
|
||||
&self,
|
||||
start_height: u64,
|
||||
max_outputs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
u64,
|
||||
u64,
|
||||
Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>,
|
||||
),
|
||||
libwallet::Error,
|
||||
> {
|
||||
let addr = self.node_url();
|
||||
let query_param = format!("start_index={}&max={}", start_height, max_outputs);
|
||||
|
||||
let url = format!("{}/v1/txhashset/outputs?{}", addr, query_param,);
|
||||
|
||||
let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> =
|
||||
Vec::new();
|
||||
|
||||
match api::client::get::<api::OutputListing>(url.as_str(), self.node_api_secret()) {
|
||||
Ok(o) => {
|
||||
for out in o.outputs {
|
||||
let is_coinbase = match out.output_type {
|
||||
api::OutputType::Coinbase => true,
|
||||
api::OutputType::Transaction => false,
|
||||
};
|
||||
api_outputs.push((
|
||||
out.commit,
|
||||
out.range_proof().unwrap(),
|
||||
is_coinbase,
|
||||
out.block_height.unwrap(),
|
||||
out.mmr_index,
|
||||
));
|
||||
}
|
||||
|
||||
Ok((o.highest_index, o.last_retrieved_index, api_outputs))
|
||||
}
|
||||
Err(e) => {
|
||||
// if we got anything other than 200 back from server, bye
|
||||
error!(
|
||||
"get_outputs_by_pmmr_index: error contacting {}. Error: {}",
|
||||
addr, e
|
||||
);
|
||||
let report = format!("outputs by pmmr index: {}", e);
|
||||
Err(libwallet::ErrorKind::ClientCallback(report))?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the wallet API to create a coinbase output for the given block_fees.
|
||||
/// Will retry based on default "retry forever with backoff" behavior.
|
||||
pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result<CbData, Error> {
|
||||
let url = format!("{}/v1/wallet/foreign/build_coinbase", dest);
|
||||
match single_create_coinbase(&url, &block_fees) {
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get coinbase from {}. Run grin wallet listen?",
|
||||
url
|
||||
);
|
||||
error!("Underlying Error: {}", e.cause().unwrap());
|
||||
error!("Backtrace: {}", e.backtrace().unwrap());
|
||||
Err(e)?
|
||||
}
|
||||
Ok(res) => Ok(res),
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a single request to the wallet API to create a new coinbase output.
|
||||
fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result<CbData, Error> {
|
||||
let res = api::client::post(url, None, block_fees).context(ErrorKind::GenericError(
|
||||
"Posting create coinbase".to_string(),
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod http;
|
||||
|
||||
pub use self::http::{create_coinbase, HTTPNodeClient};
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use self::chain::Chain;
|
||||
use self::core::core::{OutputFeatures, OutputIdentifier, Transaction};
|
||||
use self::core::{consensus, global, pow, ser};
|
||||
use self::util::secp::pedersen;
|
||||
use self::util::Mutex;
|
||||
use crate::libwallet::api::APIOwner;
|
||||
use crate::libwallet::types::{BlockFees, CbData, NodeClient, WalletInfo, WalletInst};
|
||||
use crate::lmdb_wallet::LMDBBackend;
|
||||
use crate::{controller, libwallet, WalletSeed};
|
||||
use crate::{WalletBackend, WalletConfig};
|
||||
use chrono::Duration;
|
||||
use grin_api as api;
|
||||
use grin_chain as chain;
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod testclient;
|
||||
|
||||
pub use self::{testclient::LocalWalletClient, testclient::WalletProxy};
|
||||
|
||||
/// types of backends tests should iterate through
|
||||
//#[derive(Clone)]
|
||||
//pub enum BackendType {
|
||||
// /// File
|
||||
// FileBackend,
|
||||
// /// LMDB
|
||||
// LMDBBackend,
|
||||
//}
|
||||
|
||||
/// Get an output from the chain locally and present it back as an API output
|
||||
fn get_output_local(chain: &chain::Chain, commit: &pedersen::Commitment) -> Option<api::Output> {
|
||||
let outputs = [
|
||||
OutputIdentifier::new(OutputFeatures::Plain, commit),
|
||||
OutputIdentifier::new(OutputFeatures::Coinbase, commit),
|
||||
];
|
||||
|
||||
for x in outputs.iter() {
|
||||
if let Ok(_) = chain.is_unspent(&x) {
|
||||
let block_height = chain.get_header_for_output(&x).unwrap().height;
|
||||
let output_pos = chain.get_output_pos(&x.commit).unwrap_or(0);
|
||||
return Some(api::Output::new(&commit, block_height, output_pos));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// get output listing traversing pmmr from local
|
||||
fn get_outputs_by_pmmr_index_local(
|
||||
chain: Arc<chain::Chain>,
|
||||
start_index: u64,
|
||||
max: u64,
|
||||
) -> api::OutputListing {
|
||||
let outputs = chain
|
||||
.unspent_outputs_by_insertion_index(start_index, max)
|
||||
.unwrap();
|
||||
api::OutputListing {
|
||||
last_retrieved_index: outputs.0,
|
||||
highest_index: outputs.1,
|
||||
outputs: outputs
|
||||
.2
|
||||
.iter()
|
||||
.map(|x| api::OutputPrintable::from_output(x, chain.clone(), None, true))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a block with a given reward to the chain and mines it
|
||||
pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: CbData) {
|
||||
let prev = chain.head_header().unwrap();
|
||||
let next_header_info = consensus::next_difficulty(1, chain.difficulty_iter());
|
||||
let out_bin = util::from_hex(reward.output).unwrap();
|
||||
let kern_bin = util::from_hex(reward.kernel).unwrap();
|
||||
let output = ser::deserialize(&mut &out_bin[..]).unwrap();
|
||||
let kernel = ser::deserialize(&mut &kern_bin[..]).unwrap();
|
||||
let mut b = core::core::Block::new(
|
||||
&prev,
|
||||
txs.into_iter().cloned().collect(),
|
||||
next_header_info.clone().difficulty,
|
||||
(output, kernel),
|
||||
)
|
||||
.unwrap();
|
||||
b.header.timestamp = prev.timestamp + Duration::seconds(60);
|
||||
b.header.pow.secondary_scaling = next_header_info.secondary_scaling;
|
||||
chain.set_txhashset_roots(&mut b).unwrap();
|
||||
pow::pow_size(
|
||||
&mut b.header,
|
||||
next_header_info.difficulty,
|
||||
global::proofsize(),
|
||||
global::min_edge_bits(),
|
||||
)
|
||||
.unwrap();
|
||||
chain.process_block(b, chain::Options::MINE).unwrap();
|
||||
chain.validate(false).unwrap();
|
||||
}
|
||||
|
||||
/// adds a reward output to a wallet, includes that reward in a block, mines
|
||||
/// the block and adds it to the chain, with option transactions included.
|
||||
/// Helpful for building up precise wallet balances for testing.
|
||||
pub fn award_block_to_wallet<C, K>(
|
||||
chain: &Chain,
|
||||
txs: Vec<&Transaction>,
|
||||
wallet: Arc<Mutex<dyn WalletInst<C, K>>>,
|
||||
) -> Result<(), libwallet::Error>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
// build block fees
|
||||
let prev = chain.head_header().unwrap();
|
||||
let fee_amt = txs.iter().map(|tx| tx.fee()).sum();
|
||||
let block_fees = BlockFees {
|
||||
fees: fee_amt,
|
||||
key_id: None,
|
||||
height: prev.height + 1,
|
||||
};
|
||||
// build coinbase (via api) and add block
|
||||
controller::foreign_single_use(wallet.clone(), |api| {
|
||||
let coinbase_tx = api.build_coinbase(&block_fees)?;
|
||||
add_block_with_reward(chain, txs, coinbase_tx.clone());
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Award a blocks to a wallet directly
|
||||
pub fn award_blocks_to_wallet<C, K>(
|
||||
chain: &Chain,
|
||||
wallet: Arc<Mutex<dyn WalletInst<C, K>>>,
|
||||
number: usize,
|
||||
) -> Result<(), libwallet::Error>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
for _ in 0..number {
|
||||
award_block_to_wallet(chain, vec![], wallet.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// dispatch a db wallet
|
||||
pub fn create_wallet<C, K>(
|
||||
dir: &str,
|
||||
n_client: C,
|
||||
rec_phrase: Option<&str>,
|
||||
) -> Arc<Mutex<dyn WalletInst<C, K>>>
|
||||
where
|
||||
C: NodeClient + 'static,
|
||||
K: keychain::Keychain + 'static,
|
||||
{
|
||||
let z_string = match rec_phrase {
|
||||
Some(s) => Some(util::ZeroingString::from(s)),
|
||||
None => None,
|
||||
};
|
||||
let mut wallet_config = WalletConfig::default();
|
||||
wallet_config.data_file_dir = String::from(dir);
|
||||
let _ = WalletSeed::init_file(&wallet_config, 32, z_string, "");
|
||||
let mut wallet = LMDBBackend::new(wallet_config.clone(), "", n_client)
|
||||
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config));
|
||||
wallet.open_with_credentials().unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Error initializing wallet: {:?} Config: {:?}",
|
||||
e, wallet_config
|
||||
)
|
||||
});
|
||||
Arc::new(Mutex::new(wallet))
|
||||
}
|
||||
|
||||
/// send an amount to a destination
|
||||
pub fn send_to_dest<T: ?Sized, C, K>(
|
||||
client: LocalWalletClient,
|
||||
api: &mut APIOwner<T, C, K>,
|
||||
dest: &str,
|
||||
amount: u64,
|
||||
) -> Result<(), libwallet::Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
let (slate_i, lock_fn) = api.initiate_tx(
|
||||
None, // account
|
||||
amount, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
let mut slate = client.send_tx_slate_direct(dest, &slate_i)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
api.finalize_tx(&mut slate)?;
|
||||
api.post_tx(&slate.tx, false)?; // mines a block
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get wallet info totals
|
||||
pub fn wallet_info<T: ?Sized, C, K>(
|
||||
api: &mut APIOwner<T, C, K>,
|
||||
) -> Result<WalletInfo, libwallet::Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: NodeClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
let (wallet_refreshed, wallet_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet_refreshed);
|
||||
Ok(wallet_info)
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test client that acts against a local instance of a node
|
||||
//! so that wallet API can be fully exercised
|
||||
//! Operates directly on a chain instance
|
||||
|
||||
use self::chain::types::NoopAdapter;
|
||||
use self::chain::Chain;
|
||||
use self::core::core::verifier_cache::LruVerifierCache;
|
||||
use self::core::core::Transaction;
|
||||
use self::core::global::{set_mining_mode, ChainTypes};
|
||||
use self::core::{pow, ser};
|
||||
use self::keychain::Keychain;
|
||||
use self::util::secp::pedersen;
|
||||
use self::util::secp::pedersen::Commitment;
|
||||
use self::util::{Mutex, RwLock, StopState};
|
||||
use crate::libwallet::slate::Slate;
|
||||
use crate::libwallet::types::*;
|
||||
use crate::{controller, libwallet, WalletCommAdapter, WalletConfig};
|
||||
use failure::ResultExt;
|
||||
use grin_api as api;
|
||||
use grin_chain as chain;
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_store as store;
|
||||
use grin_util as util;
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Messages to simulate wallet requests/responses
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WalletProxyMessage {
|
||||
/// sender ID
|
||||
pub sender_id: String,
|
||||
/// destination wallet (or server)
|
||||
pub dest: String,
|
||||
/// method (like a GET url)
|
||||
pub method: String,
|
||||
/// payload (json body)
|
||||
pub body: String,
|
||||
}
|
||||
|
||||
/// communicates with a chain instance or other wallet
|
||||
/// listener APIs via message queues
|
||||
pub struct WalletProxy<C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// directory to create the chain in
|
||||
pub chain_dir: String,
|
||||
/// handle to chain itself
|
||||
pub chain: Arc<Chain>,
|
||||
/// list of interested wallets
|
||||
pub wallets: HashMap<
|
||||
String,
|
||||
(
|
||||
Sender<WalletProxyMessage>,
|
||||
Arc<Mutex<dyn WalletInst<LocalWalletClient, K>>>,
|
||||
),
|
||||
>,
|
||||
/// simulate json send to another client
|
||||
/// address, method, payload (simulate HTTP request)
|
||||
pub tx: Sender<WalletProxyMessage>,
|
||||
/// simulate json receiving
|
||||
pub rx: Receiver<WalletProxyMessage>,
|
||||
/// queue control
|
||||
pub running: Arc<AtomicBool>,
|
||||
/// Phantom
|
||||
phantom_c: PhantomData<C>,
|
||||
/// Phantom
|
||||
phantom_k: PhantomData<K>,
|
||||
}
|
||||
|
||||
impl<C, K> WalletProxy<C, K>
|
||||
where
|
||||
C: NodeClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Create a new client that will communicate with the given grin node
|
||||
pub fn new(chain_dir: &str) -> Self {
|
||||
set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
let genesis_block = pow::mine_genesis_block().unwrap();
|
||||
let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new()));
|
||||
let dir_name = format!("{}/.grin", chain_dir);
|
||||
let db_env = Arc::new(store::new_env(dir_name.to_string()));
|
||||
let c = Chain::init(
|
||||
dir_name.to_string(),
|
||||
db_env,
|
||||
Arc::new(NoopAdapter {}),
|
||||
genesis_block,
|
||||
pow::verify_size,
|
||||
verifier_cache,
|
||||
false,
|
||||
Arc::new(Mutex::new(StopState::new())),
|
||||
)
|
||||
.unwrap();
|
||||
let (tx, rx) = channel();
|
||||
let retval = WalletProxy {
|
||||
chain_dir: chain_dir.to_owned(),
|
||||
chain: Arc::new(c),
|
||||
tx: tx,
|
||||
rx: rx,
|
||||
wallets: HashMap::new(),
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
phantom_c: PhantomData,
|
||||
phantom_k: PhantomData,
|
||||
};
|
||||
retval
|
||||
}
|
||||
|
||||
/// Add wallet with a given "address"
|
||||
pub fn add_wallet(
|
||||
&mut self,
|
||||
addr: &str,
|
||||
tx: Sender<WalletProxyMessage>,
|
||||
wallet: Arc<Mutex<dyn WalletInst<LocalWalletClient, K>>>,
|
||||
) {
|
||||
self.wallets.insert(addr.to_owned(), (tx, wallet));
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.running.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Run the incoming message queue and respond more or less
|
||||
/// synchronously
|
||||
pub fn run(&mut self) -> Result<(), libwallet::Error> {
|
||||
self.running.store(true, Ordering::Relaxed);
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
// read queue
|
||||
let m = self.rx.recv().unwrap();
|
||||
trace!("Wallet Client Proxy Received: {:?}", m);
|
||||
let resp = match m.method.as_ref() {
|
||||
"get_chain_height" => self.get_chain_height(m)?,
|
||||
"get_outputs_from_node" => self.get_outputs_from_node(m)?,
|
||||
"get_outputs_by_pmmr_index" => self.get_outputs_by_pmmr_index(m)?,
|
||||
"send_tx_slate" => self.send_tx_slate(m)?,
|
||||
"post_tx" => self.post_tx(m)?,
|
||||
_ => panic!("Unknown Wallet Proxy Message"),
|
||||
};
|
||||
|
||||
self.respond(resp);
|
||||
if !self.running.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a message to a given wallet client
|
||||
fn respond(&mut self, m: WalletProxyMessage) {
|
||||
if let Some(s) = self.wallets.get_mut(&m.dest) {
|
||||
if let Err(e) = s.0.send(m.clone()) {
|
||||
panic!("Error sending response from proxy: {:?}, {}", m, e);
|
||||
}
|
||||
} else {
|
||||
panic!("Unknown wallet recipient for response message: {:?}", m);
|
||||
}
|
||||
}
|
||||
|
||||
/// post transaction to the chain (and mine it, taking the reward)
|
||||
fn post_tx(&mut self, m: WalletProxyMessage) -> Result<WalletProxyMessage, libwallet::Error> {
|
||||
let dest_wallet = self.wallets.get_mut(&m.sender_id).unwrap().1.clone();
|
||||
let wrapper: TxWrapper = serde_json::from_str(&m.body).context(
|
||||
libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper".to_owned()),
|
||||
)?;
|
||||
|
||||
let tx_bin = util::from_hex(wrapper.tx_hex).context(
|
||||
libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx_bin".to_owned()),
|
||||
)?;
|
||||
|
||||
let tx: Transaction = ser::deserialize(&mut &tx_bin[..]).context(
|
||||
libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx".to_owned()),
|
||||
)?;
|
||||
|
||||
super::award_block_to_wallet(&self.chain, vec![&tx], dest_wallet)?;
|
||||
|
||||
Ok(WalletProxyMessage {
|
||||
sender_id: "node".to_owned(),
|
||||
dest: m.sender_id,
|
||||
method: m.method,
|
||||
body: "".to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// send tx slate
|
||||
fn send_tx_slate(
|
||||
&mut self,
|
||||
m: WalletProxyMessage,
|
||||
) -> Result<WalletProxyMessage, libwallet::Error> {
|
||||
let dest_wallet = self.wallets.get_mut(&m.dest);
|
||||
if let None = dest_wallet {
|
||||
panic!("Unknown wallet destination for send_tx_slate: {:?}", m);
|
||||
}
|
||||
let w = dest_wallet.unwrap().1.clone();
|
||||
let mut slate = serde_json::from_str(&m.body).unwrap();
|
||||
controller::foreign_single_use(w.clone(), |listener_api| {
|
||||
listener_api.receive_tx(&mut slate, None, None)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(WalletProxyMessage {
|
||||
sender_id: m.dest,
|
||||
dest: m.sender_id,
|
||||
method: m.method,
|
||||
body: serde_json::to_string(&slate).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// get chain height
|
||||
fn get_chain_height(
|
||||
&mut self,
|
||||
m: WalletProxyMessage,
|
||||
) -> Result<WalletProxyMessage, libwallet::Error> {
|
||||
Ok(WalletProxyMessage {
|
||||
sender_id: "node".to_owned(),
|
||||
dest: m.sender_id,
|
||||
method: m.method,
|
||||
body: format!("{}", self.chain.head().unwrap().height).to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// get api outputs
|
||||
fn get_outputs_from_node(
|
||||
&mut self,
|
||||
m: WalletProxyMessage,
|
||||
) -> Result<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(",");
|
||||
//let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new();
|
||||
let mut outputs: Vec<api::Output> = vec![];
|
||||
for o in split {
|
||||
let o_str = String::from(o);
|
||||
if o_str.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
let c = util::from_hex(o_str).unwrap();
|
||||
let commit = Commitment::from_vec(c);
|
||||
let out = super::get_output_local(&self.chain.clone(), &commit);
|
||||
if let Some(o) = out {
|
||||
outputs.push(o);
|
||||
}
|
||||
}
|
||||
Ok(WalletProxyMessage {
|
||||
sender_id: "node".to_owned(),
|
||||
dest: m.sender_id,
|
||||
method: m.method,
|
||||
body: serde_json::to_string(&outputs).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// get api outputs
|
||||
fn get_outputs_by_pmmr_index(
|
||||
&mut self,
|
||||
m: WalletProxyMessage,
|
||||
) -> Result<WalletProxyMessage, libwallet::Error> {
|
||||
let split = m.body.split(",").collect::<Vec<&str>>();
|
||||
let start_index = split[0].parse::<u64>().unwrap();
|
||||
let max = split[1].parse::<u64>().unwrap();
|
||||
let ol = super::get_outputs_by_pmmr_index_local(self.chain.clone(), start_index, max);
|
||||
Ok(WalletProxyMessage {
|
||||
sender_id: "node".to_owned(),
|
||||
dest: m.sender_id,
|
||||
method: m.method,
|
||||
body: serde_json::to_string(&ol).unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalWalletClient {
|
||||
/// wallet identifier for the proxy queue
|
||||
pub id: String,
|
||||
/// proxy's tx queue (receive messages from other wallets or node
|
||||
pub proxy_tx: Arc<Mutex<Sender<WalletProxyMessage>>>,
|
||||
/// my rx queue
|
||||
pub rx: Arc<Mutex<Receiver<WalletProxyMessage>>>,
|
||||
/// my tx queue
|
||||
pub tx: Arc<Mutex<Sender<WalletProxyMessage>>>,
|
||||
}
|
||||
|
||||
impl LocalWalletClient {
|
||||
/// new
|
||||
pub fn new(id: &str, proxy_rx: Sender<WalletProxyMessage>) -> Self {
|
||||
let (tx, rx) = channel();
|
||||
LocalWalletClient {
|
||||
id: id.to_owned(),
|
||||
proxy_tx: Arc::new(Mutex::new(proxy_rx)),
|
||||
rx: Arc::new(Mutex::new(rx)),
|
||||
tx: Arc::new(Mutex::new(tx)),
|
||||
}
|
||||
}
|
||||
|
||||
/// get an instance of the send queue for other senders
|
||||
pub fn get_send_instance(&self) -> Sender<WalletProxyMessage> {
|
||||
self.tx.lock().clone()
|
||||
}
|
||||
|
||||
/// Send the slate to a listening wallet instance
|
||||
pub fn send_tx_slate_direct(
|
||||
&self,
|
||||
dest: &str,
|
||||
slate: &Slate,
|
||||
) -> Result<Slate, libwallet::Error> {
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: dest.to_owned(),
|
||||
method: "send_tx_slate".to_owned(),
|
||||
body: serde_json::to_string(slate).unwrap(),
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Send TX Slate".to_owned(),
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
trace!("Received send_tx_slate response: {:?}", m.clone());
|
||||
Ok(
|
||||
serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Parsing send_tx_slate response".to_owned(),
|
||||
))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletCommAdapter for LocalWalletClient {
|
||||
fn supports_sync(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Send the slate to a listening wallet instance
|
||||
fn send_tx_sync(&self, dest: &str, slate: &Slate) -> Result<Slate, libwallet::Error> {
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: dest.to_owned(),
|
||||
method: "send_tx_slate".to_owned(),
|
||||
body: serde_json::to_string(slate).unwrap(),
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Send TX Slate".to_owned(),
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
trace!("Received send_tx_slate response: {:?}", m.clone());
|
||||
Ok(
|
||||
serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Parsing send_tx_slate response".to_owned(),
|
||||
))?,
|
||||
)
|
||||
}
|
||||
|
||||
fn send_tx_async(&self, _dest: &str, _slate: &Slate) -> Result<(), libwallet::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn receive_tx_async(&self, _params: &str) -> Result<Slate, libwallet::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn listen(
|
||||
&self,
|
||||
_params: HashMap<String, String>,
|
||||
_config: WalletConfig,
|
||||
_passphrase: &str,
|
||||
_account: &str,
|
||||
_node_api_secret: Option<String>,
|
||||
) -> Result<(), libwallet::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeClient for LocalWalletClient {
|
||||
fn node_url(&self) -> &str {
|
||||
"node"
|
||||
}
|
||||
fn node_api_secret(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn set_node_url(&mut self, _node_url: &str) {}
|
||||
fn set_node_api_secret(&mut self, _node_api_secret: Option<String>) {}
|
||||
/// Posts a transaction to a grin node
|
||||
/// In this case it will create a new block with award rewarded to
|
||||
fn post_tx(&self, tx: &TxWrapper, _fluff: bool) -> Result<(), libwallet::Error> {
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: self.node_url().to_owned(),
|
||||
method: "post_tx".to_owned(),
|
||||
body: serde_json::to_string(tx).unwrap(),
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"post_tx send".to_owned(),
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
trace!("Received post_tx response: {:?}", m.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the chain tip from a given node
|
||||
fn get_chain_height(&self) -> Result<u64, libwallet::Error> {
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: self.node_url().to_owned(),
|
||||
method: "get_chain_height".to_owned(),
|
||||
body: "".to_owned(),
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Get chain height send".to_owned(),
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
trace!("Received get_chain_height response: {:?}", m.clone());
|
||||
Ok(m.body
|
||||
.parse::<u64>()
|
||||
.context(libwallet::ErrorKind::ClientCallback(
|
||||
"Parsing get_height response".to_owned(),
|
||||
))?)
|
||||
}
|
||||
|
||||
/// Retrieve outputs from node
|
||||
fn get_outputs_from_node(
|
||||
&self,
|
||||
wallet_outputs: Vec<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, (String, u64, u64)>, libwallet::Error> {
|
||||
let query_params: Vec<String> = wallet_outputs
|
||||
.iter()
|
||||
.map(|commit| format!("{}", util::to_hex(commit.as_ref().to_vec())))
|
||||
.collect();
|
||||
let query_str = query_params.join(",");
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: self.node_url().to_owned(),
|
||||
method: "get_outputs_from_node".to_owned(),
|
||||
body: query_str,
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Get outputs from node send".to_owned(),
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
let outputs: Vec<api::Output> = serde_json::from_str(&m.body).unwrap();
|
||||
let mut api_outputs: HashMap<pedersen::Commitment, (String, u64, u64)> = HashMap::new();
|
||||
for out in outputs {
|
||||
api_outputs.insert(
|
||||
out.commit.commit(),
|
||||
(util::to_hex(out.commit.to_vec()), out.height, out.mmr_index),
|
||||
);
|
||||
}
|
||||
Ok(api_outputs)
|
||||
}
|
||||
|
||||
fn get_outputs_by_pmmr_index(
|
||||
&self,
|
||||
start_height: u64,
|
||||
max_outputs: u64,
|
||||
) -> Result<
|
||||
(
|
||||
u64,
|
||||
u64,
|
||||
Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>,
|
||||
),
|
||||
libwallet::Error,
|
||||
> {
|
||||
// start index, max
|
||||
let query_str = format!("{},{}", start_height, max_outputs);
|
||||
let m = WalletProxyMessage {
|
||||
sender_id: self.id.clone(),
|
||||
dest: self.node_url().to_owned(),
|
||||
method: "get_outputs_by_pmmr_index".to_owned(),
|
||||
body: query_str,
|
||||
};
|
||||
{
|
||||
let p = self.proxy_tx.lock();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Get outputs from node by PMMR index send".to_owned(),
|
||||
))?;
|
||||
}
|
||||
|
||||
let r = self.rx.lock();
|
||||
let m = r.recv().unwrap();
|
||||
let o: api::OutputListing = serde_json::from_str(&m.body).unwrap();
|
||||
|
||||
let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)> =
|
||||
Vec::new();
|
||||
|
||||
for out in o.outputs {
|
||||
let is_coinbase = match out.output_type {
|
||||
api::OutputType::Coinbase => true,
|
||||
api::OutputType::Transaction => false,
|
||||
};
|
||||
api_outputs.push((
|
||||
out.commit,
|
||||
out.range_proof().unwrap(),
|
||||
is_coinbase,
|
||||
out.block_height.unwrap(),
|
||||
out.mmr_index,
|
||||
));
|
||||
}
|
||||
Ok((o.highest_index, o.last_retrieved_index, api_outputs))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
|
||||
use crate::blake2;
|
||||
use rand::{thread_rng, Rng};
|
||||
use serde_json;
|
||||
|
||||
use ring::aead;
|
||||
use ring::{digest, pbkdf2};
|
||||
|
||||
use crate::core::global::ChainTypes;
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::keychain::{mnemonic, Keychain};
|
||||
use crate::util;
|
||||
use failure::ResultExt;
|
||||
|
||||
pub const SEED_FILE: &'static str = "wallet.seed";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct WalletConfig {
|
||||
// Chain parameters (default to Testnet3 if none at the moment)
|
||||
pub chain_type: Option<ChainTypes>,
|
||||
// The api interface/ip_address that this api server (i.e. this wallet) will run
|
||||
// by default this is 127.0.0.1 (and will not accept connections from external clients)
|
||||
pub api_listen_interface: String,
|
||||
// The port this wallet will run on
|
||||
pub api_listen_port: u16,
|
||||
// The port this wallet's owner API will run on
|
||||
pub owner_api_listen_port: Option<u16>,
|
||||
/// Location of the secret for basic auth on the Owner API
|
||||
pub api_secret_path: Option<String>,
|
||||
/// Location of the node api secret for basic auth on the Grin API
|
||||
pub node_api_secret_path: Option<String>,
|
||||
// The api address of a running server node against which transaction inputs
|
||||
// will be checked during send
|
||||
pub check_node_api_http_addr: String,
|
||||
// Whether to include foreign API endpoints on the Owner API
|
||||
pub owner_api_include_foreign: Option<bool>,
|
||||
// The directory in which wallet files are stored
|
||||
pub data_file_dir: String,
|
||||
/// If Some(true), don't cache commits alongside output data
|
||||
/// speed improvement, but your commits are in the database
|
||||
pub no_commit_cache: Option<bool>,
|
||||
/// TLS certificate file
|
||||
pub tls_certificate_file: Option<String>,
|
||||
/// TLS certificate private key file
|
||||
pub tls_certificate_key: Option<String>,
|
||||
/// Whether to use the black background color scheme for command line
|
||||
/// if enabled, wallet command output color will be suitable for black background terminal
|
||||
pub dark_background_color_scheme: Option<bool>,
|
||||
// The exploding lifetime (minutes) for keybase notification on coins received
|
||||
pub keybase_notify_ttl: Option<u16>,
|
||||
}
|
||||
|
||||
impl Default for WalletConfig {
|
||||
fn default() -> WalletConfig {
|
||||
WalletConfig {
|
||||
chain_type: Some(ChainTypes::Mainnet),
|
||||
api_listen_interface: "127.0.0.1".to_string(),
|
||||
api_listen_port: 3415,
|
||||
owner_api_listen_port: Some(WalletConfig::default_owner_api_listen_port()),
|
||||
api_secret_path: Some(".api_secret".to_string()),
|
||||
node_api_secret_path: Some(".api_secret".to_string()),
|
||||
check_node_api_http_addr: "http://127.0.0.1:3413".to_string(),
|
||||
owner_api_include_foreign: Some(false),
|
||||
data_file_dir: ".".to_string(),
|
||||
no_commit_cache: Some(false),
|
||||
tls_certificate_file: None,
|
||||
tls_certificate_key: None,
|
||||
dark_background_color_scheme: Some(true),
|
||||
keybase_notify_ttl: Some(1440),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletConfig {
|
||||
pub fn api_listen_addr(&self) -> String {
|
||||
format!("{}:{}", self.api_listen_interface, self.api_listen_port)
|
||||
}
|
||||
|
||||
pub fn default_owner_api_listen_port() -> u16 {
|
||||
3420
|
||||
}
|
||||
|
||||
/// Use value from config file, defaulting to sensible value if missing.
|
||||
pub fn owner_api_listen_port(&self) -> u16 {
|
||||
self.owner_api_listen_port
|
||||
.unwrap_or(WalletConfig::default_owner_api_listen_port())
|
||||
}
|
||||
|
||||
pub fn owner_api_listen_addr(&self) -> String {
|
||||
format!("127.0.0.1:{}", self.owner_api_listen_port())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct WalletSeed(Vec<u8>);
|
||||
|
||||
impl WalletSeed {
|
||||
pub fn from_bytes(bytes: &[u8]) -> WalletSeed {
|
||||
WalletSeed(bytes.to_vec())
|
||||
}
|
||||
|
||||
pub fn from_mnemonic(word_list: &str) -> Result<WalletSeed, Error> {
|
||||
let res = mnemonic::to_entropy(word_list);
|
||||
match res {
|
||||
Ok(s) => Ok(WalletSeed::from_bytes(&s)),
|
||||
Err(_) => Err(ErrorKind::Mnemonic.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
|
||||
let bytes = util::from_hex(hex.to_string())
|
||||
.context(ErrorKind::GenericError("Invalid hex".to_owned()))?;
|
||||
Ok(WalletSeed::from_bytes(&bytes))
|
||||
}
|
||||
|
||||
pub fn to_hex(&self) -> String {
|
||||
util::to_hex(self.0.to_vec())
|
||||
}
|
||||
|
||||
pub fn to_mnemonic(&self) -> Result<String, Error> {
|
||||
let result = mnemonic::from_entropy(&self.0);
|
||||
match result {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => Err(ErrorKind::Mnemonic.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_keychain_old(old_wallet_seed: [u8; 32], password: &str) -> Vec<u8> {
|
||||
let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed);
|
||||
seed.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
pub fn derive_keychain<K: Keychain>(&self, is_floonet: bool) -> Result<K, Error> {
|
||||
let result = K::from_seed(&self.0, is_floonet)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn init_new(seed_length: usize) -> WalletSeed {
|
||||
let mut seed: Vec<u8> = vec![];
|
||||
let mut rng = thread_rng();
|
||||
for _ in 0..seed_length {
|
||||
seed.push(rng.gen());
|
||||
}
|
||||
WalletSeed(seed)
|
||||
}
|
||||
|
||||
pub fn seed_file_exists(wallet_config: &WalletConfig) -> Result<(), Error> {
|
||||
let seed_file_path = &format!(
|
||||
"{}{}{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
|
||||
);
|
||||
if Path::new(seed_file_path).exists() {
|
||||
return Err(ErrorKind::WalletSeedExists(seed_file_path.to_owned()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn backup_seed(wallet_config: &WalletConfig) -> Result<(), Error> {
|
||||
let seed_file_name = &format!(
|
||||
"{}{}{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
|
||||
);
|
||||
|
||||
let mut path = Path::new(seed_file_name).to_path_buf();
|
||||
path.pop();
|
||||
let mut backup_seed_file_name = format!(
|
||||
"{}{}{}.bak",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE
|
||||
);
|
||||
let mut i = 1;
|
||||
while Path::new(&backup_seed_file_name).exists() {
|
||||
backup_seed_file_name = format!(
|
||||
"{}{}{}.bak.{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, i
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
path.push(backup_seed_file_name.clone());
|
||||
if let Err(_) = fs::rename(seed_file_name, backup_seed_file_name.as_str()) {
|
||||
return Err(ErrorKind::GenericError(
|
||||
"Can't rename wallet seed file".to_owned(),
|
||||
))?;
|
||||
}
|
||||
warn!("{} backed up as {}", seed_file_name, backup_seed_file_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recover_from_phrase(
|
||||
wallet_config: &WalletConfig,
|
||||
word_list: &str,
|
||||
password: &str,
|
||||
) -> Result<(), Error> {
|
||||
let seed_file_path = &format!(
|
||||
"{}{}{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
|
||||
);
|
||||
if WalletSeed::seed_file_exists(wallet_config).is_err() {
|
||||
WalletSeed::backup_seed(wallet_config)?;
|
||||
}
|
||||
let seed = WalletSeed::from_mnemonic(word_list)?;
|
||||
let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?;
|
||||
let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?;
|
||||
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
|
||||
file.write_all(&enc_seed_json.as_bytes())
|
||||
.context(ErrorKind::IO)?;
|
||||
warn!("Seed created from word list");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_recovery_phrase(&self) -> Result<(), Error> {
|
||||
println!("Your recovery phrase is:");
|
||||
println!();
|
||||
println!("{}", self.to_mnemonic()?);
|
||||
println!();
|
||||
println!("Please back-up these words in a non-digital format.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_file(
|
||||
wallet_config: &WalletConfig,
|
||||
seed_length: usize,
|
||||
recovery_phrase: Option<util::ZeroingString>,
|
||||
password: &str,
|
||||
) -> Result<WalletSeed, Error> {
|
||||
// create directory if it doesn't exist
|
||||
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
|
||||
|
||||
let seed_file_path = &format!(
|
||||
"{}{}{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
|
||||
);
|
||||
|
||||
warn!("Generating wallet seed file at: {}", seed_file_path);
|
||||
let _ = WalletSeed::seed_file_exists(wallet_config)?;
|
||||
|
||||
let seed = match recovery_phrase {
|
||||
Some(p) => WalletSeed::from_mnemonic(&p)?,
|
||||
None => WalletSeed::init_new(seed_length),
|
||||
};
|
||||
|
||||
let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?;
|
||||
let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?;
|
||||
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
|
||||
file.write_all(&enc_seed_json.as_bytes())
|
||||
.context(ErrorKind::IO)?;
|
||||
seed.show_recovery_phrase()?;
|
||||
Ok(seed)
|
||||
}
|
||||
|
||||
pub fn from_file(wallet_config: &WalletConfig, password: &str) -> Result<WalletSeed, Error> {
|
||||
// create directory if it doesn't exist
|
||||
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
|
||||
|
||||
let seed_file_path = &format!(
|
||||
"{}{}{}",
|
||||
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
|
||||
);
|
||||
|
||||
debug!("Using wallet seed file at: {}", seed_file_path);
|
||||
|
||||
if Path::new(seed_file_path).exists() {
|
||||
let mut file = File::open(seed_file_path).context(ErrorKind::IO)?;
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer).context(ErrorKind::IO)?;
|
||||
let enc_seed: EncryptedWalletSeed =
|
||||
serde_json::from_str(&buffer).context(ErrorKind::Format)?;
|
||||
let wallet_seed = enc_seed.decrypt(password)?;
|
||||
Ok(wallet_seed)
|
||||
} else {
|
||||
error!(
|
||||
"wallet seed file {} could not be opened (grin wallet init). \
|
||||
Run \"grin wallet init\" to initialize a new wallet.",
|
||||
seed_file_path
|
||||
);
|
||||
Err(ErrorKind::WalletSeedDoesntExist)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypted wallet seed, for storing on disk and decrypting
|
||||
/// with provided password
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct EncryptedWalletSeed {
|
||||
encrypted_seed: String,
|
||||
/// Salt, not so useful in single case but include anyhow for situations
|
||||
/// where someone wants to store many of these
|
||||
pub salt: String,
|
||||
/// Nonce
|
||||
pub nonce: String,
|
||||
}
|
||||
|
||||
impl EncryptedWalletSeed {
|
||||
/// Create a new encrypted seed from the given seed + password
|
||||
pub fn from_seed(seed: &WalletSeed, password: &str) -> Result<EncryptedWalletSeed, Error> {
|
||||
let salt: [u8; 8] = thread_rng().gen();
|
||||
let nonce: [u8; 12] = thread_rng().gen();
|
||||
let password = password.as_bytes();
|
||||
let mut key = [0; 32];
|
||||
pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key);
|
||||
let content = seed.0.to_vec();
|
||||
let mut enc_bytes = content.clone();
|
||||
let suffix_len = aead::CHACHA20_POLY1305.tag_len();
|
||||
for _ in 0..suffix_len {
|
||||
enc_bytes.push(0);
|
||||
}
|
||||
let sealing_key =
|
||||
aead::SealingKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?;
|
||||
aead::seal_in_place(&sealing_key, &nonce, &[], &mut enc_bytes, suffix_len)
|
||||
.context(ErrorKind::Encryption)?;
|
||||
Ok(EncryptedWalletSeed {
|
||||
encrypted_seed: util::to_hex(enc_bytes.to_vec()),
|
||||
salt: util::to_hex(salt.to_vec()),
|
||||
nonce: util::to_hex(nonce.to_vec()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrypt seed
|
||||
pub fn decrypt(&self, password: &str) -> Result<WalletSeed, Error> {
|
||||
let mut encrypted_seed = match util::from_hex(self.encrypted_seed.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(ErrorKind::Encryption)?,
|
||||
};
|
||||
let salt = match util::from_hex(self.salt.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(ErrorKind::Encryption)?,
|
||||
};
|
||||
let nonce = match util::from_hex(self.nonce.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(ErrorKind::Encryption)?,
|
||||
};
|
||||
let password = password.as_bytes();
|
||||
let mut key = [0; 32];
|
||||
pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key);
|
||||
|
||||
let opening_key =
|
||||
aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?;
|
||||
let decrypted_data = aead::open_in_place(&opening_key, &nonce, &[], 0, &mut encrypted_seed)
|
||||
.context(ErrorKind::Encryption)?;
|
||||
|
||||
Ok(WalletSeed::from_bytes(&decrypted_data))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Build hooks to spit out version+build time info
|
||||
|
||||
use built;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// Setting up git hooks in the project: rustfmt and so on.
|
||||
let git_hooks = format!(
|
||||
"git config core.hooksPath {}",
|
||||
PathBuf::from("./.hooks").to_str().unwrap()
|
||||
);
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(&["/C", &git_hooks])
|
||||
.output()
|
||||
.expect("failed to execute git config for hooks");
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.args(&["-c", &git_hooks])
|
||||
.output()
|
||||
.expect("failed to execute git config for hooks");
|
||||
}
|
||||
|
||||
// build and versioning information
|
||||
let mut opts = built::Options::default();
|
||||
opts.set_dependencies(true);
|
||||
// don't fail the build if something's missing, may just be cargo release
|
||||
let _ = built::write_built_file_with_opts(
|
||||
&opts,
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
format!("{}{}", env::var("OUT_DIR").unwrap(), "/built.rs"),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! tests differing accounts in the same wallet
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::{ExtKeychain, Keychain};
|
||||
use self::wallet::libwallet;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// Various tests on accounts within the same wallet
|
||||
fn accounts_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
// define recipient wallet, add to proxy
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height
|
||||
|
||||
// test default accounts exist
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let accounts = api.accounts()?;
|
||||
assert_eq!(accounts[0].label, "default");
|
||||
assert_eq!(accounts[0].path, ExtKeychain::derive_key_id(2, 0, 0, 0, 0));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let new_path = api.create_account_path("account1").unwrap();
|
||||
assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0));
|
||||
let new_path = api.create_account_path("account2").unwrap();
|
||||
assert_eq!(new_path, ExtKeychain::derive_key_id(2, 2, 0, 0, 0));
|
||||
let new_path = api.create_account_path("account3").unwrap();
|
||||
assert_eq!(new_path, ExtKeychain::derive_key_id(2, 3, 0, 0, 0));
|
||||
// trying to add same label again should fail
|
||||
let res = api.create_account_path("account1");
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// add account to wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let new_path = api.create_account_path("listener_account").unwrap();
|
||||
assert_eq!(new_path, ExtKeychain::derive_key_id(2, 1, 0, 0, 0));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Default wallet 2 to listen on that account
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("listener_account")?;
|
||||
}
|
||||
|
||||
// Mine into two different accounts in the same wallet
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0));
|
||||
}
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 7);
|
||||
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("account2")?;
|
||||
assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0));
|
||||
}
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5);
|
||||
|
||||
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 12);
|
||||
assert_eq!(wallet1_info.total, 5 * reward);
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, (5 - cm) * reward);
|
||||
// check tx log as well
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 5);
|
||||
Ok(())
|
||||
})?;
|
||||
// now check second account
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
// check last confirmed height on this account is different from above (should be 0)
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?;
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 0);
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 12);
|
||||
assert_eq!(wallet1_info.total, 7 * reward);
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, 7 * reward);
|
||||
// check tx log as well
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 7);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// should be nothing in default account
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("default")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?;
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 0);
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 12);
|
||||
assert_eq!(wallet1_info.total, 0,);
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, 0,);
|
||||
// check tx log as well
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 0);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Send a tx to another wallet
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
}
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (mut slate, lock_fn) = api.initiate_tx(
|
||||
None, reward, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
api.finalize_tx(&mut slate)?;
|
||||
api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 13);
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 9);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// other account should be untouched
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("account2")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(false, 1)?;
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 12);
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert_eq!(wallet1_info.last_confirmed_height, 13);
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
println!("{:?}", txs);
|
||||
assert_eq!(txs.len(), 5);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 2 should only have this tx on the listener account
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.last_confirmed_height, 13);
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 1);
|
||||
Ok(())
|
||||
})?;
|
||||
// Default account on wallet 2 should be untouched
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("default")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (_, wallet2_info) = api.retrieve_summary_info(false, 1)?;
|
||||
assert_eq!(wallet2_info.last_confirmed_height, 0);
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.last_confirmed_height, 13);
|
||||
assert_eq!(wallet2_info.total, 0,);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, 0,);
|
||||
// check tx log as well
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert_eq!(txs.len(), 0);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accounts() {
|
||||
let test_dir = "test_output/accounts";
|
||||
if let Err(e) = accounts_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! tests differing accounts in the same wallet
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use self::core::consensus;
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::ExtKeychain;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use self::wallet::{libwallet, FileWalletCommAdapter};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// Various tests on checking functionality
|
||||
fn check_repair_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
// define recipient wallet, add to proxy
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity() as u64; // assume all testing precedes soft fork height
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.create_account_path("account_1")?;
|
||||
api.create_account_path("account_2")?;
|
||||
api.create_account_path("account_3")?;
|
||||
api.set_active_account("account_1")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// add account to wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
api.create_account_path("account_1")?;
|
||||
api.set_active_account("account_1")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Do some mining
|
||||
let bh = 20u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize);
|
||||
|
||||
// Sanity check contents
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward);
|
||||
// check tx log as well
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
let (c, _) = libwallet::types::TxLogEntry::sum_confirmed(&txs);
|
||||
assert_eq!(wallet1_info.total, c);
|
||||
assert_eq!(txs.len(), bh as usize);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Accidentally delete some outputs
|
||||
let mut w1_outputs_commits = vec![];
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
w1_outputs_commits = api.retrieve_outputs(false, true, None)?.1;
|
||||
Ok(())
|
||||
})?;
|
||||
let w1_outputs: Vec<libwallet::types::OutputData> =
|
||||
w1_outputs_commits.into_iter().map(|o| o.0).collect();
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.open_with_credentials()?;
|
||||
{
|
||||
let mut batch = w.batch()?;
|
||||
batch.delete(&w1_outputs[4].key_id, &None)?;
|
||||
batch.delete(&w1_outputs[10].key_id, &None)?;
|
||||
let mut accidental_spent = w1_outputs[13].clone();
|
||||
accidental_spent.status = libwallet::types::OutputStatus::Spent;
|
||||
batch.save(accidental_spent)?;
|
||||
batch.commit()?;
|
||||
}
|
||||
w.close()?;
|
||||
}
|
||||
|
||||
// check we have a problem now
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
let (c, _) = libwallet::types::TxLogEntry::sum_confirmed(&txs);
|
||||
assert!(wallet1_info.total != c);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// this should restore our missing outputs
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// check our outputs match again
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// perform a transaction, but don't let it finish
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
// send to send
|
||||
let (mut slate, lock_fn) = api.initiate_tx(
|
||||
None,
|
||||
reward * 2, // amount
|
||||
cm, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None, // optional message
|
||||
)?;
|
||||
// output tx file
|
||||
let file_adapter = FileWalletCommAdapter::new();
|
||||
let send_file = format!("{}/part_tx_1.tx", test_dir);
|
||||
file_adapter.send_tx_async(&send_file, &mut slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// check we're all locked
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_info.amount_currently_spendable == 0);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// unlock/restore
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// check spendable amount again
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert_eq!(wallet1_info.amount_currently_spendable, (bh - cm) * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn two_wallets_one_seed_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
let seed_phrase =
|
||||
"affair pistol cancel crush garment candy ancient flag work \
|
||||
market crush dry stand focus mutual weapon offer ceiling rival turn team spring \
|
||||
where swift";
|
||||
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let m_client = LocalWalletClient::new("miner", wallet_proxy.tx.clone());
|
||||
let miner =
|
||||
test_framework::create_wallet(&format!("{}/miner", test_dir), m_client.clone(), None);
|
||||
wallet_proxy.add_wallet("miner", m_client.get_send_instance(), miner.clone());
|
||||
|
||||
// non-mining recipient wallets
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 = test_framework::create_wallet(
|
||||
&format!("{}/wallet1", test_dir),
|
||||
client1.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 = test_framework::create_wallet(
|
||||
&format!("{}/wallet2", test_dir),
|
||||
client2.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// we'll restore into here
|
||||
let client3 = LocalWalletClient::new("wallet3", wallet_proxy.tx.clone());
|
||||
let wallet3 = test_framework::create_wallet(
|
||||
&format!("{}/wallet3", test_dir),
|
||||
client3.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet3", client3.get_send_instance(), wallet3.clone());
|
||||
|
||||
// also restore into here
|
||||
let client4 = LocalWalletClient::new("wallet4", wallet_proxy.tx.clone());
|
||||
let wallet4 = test_framework::create_wallet(
|
||||
&format!("{}/wallet4", test_dir),
|
||||
client4.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet4", client4.get_send_instance(), wallet4.clone());
|
||||
|
||||
// Simulate a recover from seed without restore into here
|
||||
let client5 = LocalWalletClient::new("wallet5", wallet_proxy.tx.clone());
|
||||
let wallet5 = test_framework::create_wallet(
|
||||
&format!("{}/wallet5", test_dir),
|
||||
client5.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet5", client5.get_send_instance(), wallet5.clone());
|
||||
|
||||
//simulate a recover from seed without restore into here
|
||||
let client6 = LocalWalletClient::new("wallet6", wallet_proxy.tx.clone());
|
||||
let wallet6 = test_framework::create_wallet(
|
||||
&format!("{}/wallet6", test_dir),
|
||||
client6.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet6", client6.get_send_instance(), wallet6.clone());
|
||||
|
||||
let client7 = LocalWalletClient::new("wallet7", wallet_proxy.tx.clone());
|
||||
let wallet7 = test_framework::create_wallet(
|
||||
&format!("{}/wallet7", test_dir),
|
||||
client7.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet7", client7.get_send_instance(), wallet7.clone());
|
||||
|
||||
let client8 = LocalWalletClient::new("wallet8", wallet_proxy.tx.clone());
|
||||
let wallet8 = test_framework::create_wallet(
|
||||
&format!("{}/wallet8", test_dir),
|
||||
client8.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet8", client8.get_send_instance(), wallet8.clone());
|
||||
|
||||
let client9 = LocalWalletClient::new("wallet9", wallet_proxy.tx.clone());
|
||||
let wallet9 = test_framework::create_wallet(
|
||||
&format!("{}/wallet9", test_dir),
|
||||
client9.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet9", client9.get_send_instance(), wallet9.clone());
|
||||
|
||||
let client10 = LocalWalletClient::new("wallet10", wallet_proxy.tx.clone());
|
||||
let wallet10 = test_framework::create_wallet(
|
||||
&format!("{}/wallet10", test_dir),
|
||||
client10.clone(),
|
||||
Some(seed_phrase),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet10", client10.get_send_instance(), wallet10.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let _reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity() as usize; // assume all testing precedes soft fork height
|
||||
|
||||
// Do some mining
|
||||
let mut bh = 20u64;
|
||||
let base_amount = consensus::GRIN_BASE;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), bh as usize);
|
||||
|
||||
// send some funds to wallets 1
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 1)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 2)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet1", base_amount * 3)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 0) Check repair when all is okay should leave wallet contents alone
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 6);
|
||||
assert_eq!(info.total, base_amount * 6);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// send some funds to wallet 2
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 4)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 5)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet2", base_amount * 6)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm);
|
||||
bh += cm as u64;
|
||||
|
||||
// confirm balances
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 6);
|
||||
assert_eq!(info.total, base_amount * 6);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 15);
|
||||
assert_eq!(info.total, base_amount * 15);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Now there should be outputs on the chain using the same
|
||||
// seed + BIP32 path.
|
||||
|
||||
// 1) a full restore should recover all of them:
|
||||
wallet::controller::owner_single_use(wallet3.clone(), |api| {
|
||||
api.restore()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet3.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 6);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 21);
|
||||
assert_eq!(info.total, base_amount * 21);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 2) check_repair should recover them into a single wallet
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 6);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 21);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 3) If I recover from seed and start using the wallet without restoring,
|
||||
// check_repair should restore the older outputs
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 7)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 8)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet4", base_amount * 9)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm);
|
||||
bh += cm as u64;
|
||||
|
||||
wallet::controller::owner_single_use(wallet4.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 24);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet5.clone(), |api| {
|
||||
api.restore()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet5.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 9);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * (45));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 4) If I recover from seed and start using the wallet without restoring,
|
||||
// check_repair should restore the older outputs
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 10)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 11)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet6", base_amount * 12)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm as usize);
|
||||
bh += cm as u64;
|
||||
|
||||
wallet::controller::owner_single_use(wallet6.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 33);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet6.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet6.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 12);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * (78));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 5) Start using same seed with a different account, amounts should
|
||||
// be distinct and restore should return funds from other account
|
||||
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 13)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 14)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 15)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mix it up a bit
|
||||
wallet::controller::owner_single_use(wallet7.clone(), |api| {
|
||||
api.create_account_path("account_1")?;
|
||||
api.set_active_account("account_1")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 1)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 2)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet7", base_amount * 3)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// check balances
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm);
|
||||
bh += cm as u64;
|
||||
|
||||
wallet::controller::owner_single_use(wallet7.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 6);
|
||||
api.set_active_account("default")?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 42);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet8.clone(), |api| {
|
||||
api.restore()?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 15);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 120);
|
||||
api.set_active_account("account_1")?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 6);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// 6) Start using same seed with a different account, now overwriting
|
||||
// ids on account 2 as well, check_repair should get all outputs created
|
||||
// to now into 2 accounts
|
||||
|
||||
wallet::controller::owner_single_use(wallet9.clone(), |api| {
|
||||
api.create_account_path("account_1")?;
|
||||
api.set_active_account("account_1")?;
|
||||
Ok(())
|
||||
})?;
|
||||
wallet::controller::owner_single_use(miner.clone(), |api| {
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 4)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 5)?;
|
||||
test_framework::send_to_dest(m_client.clone(), api, "wallet9", base_amount * 6)?;
|
||||
bh += 3;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet9.clone(), |api| {
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 15);
|
||||
api.check_repair()?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 6);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 21);
|
||||
|
||||
api.set_active_account("default")?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 15);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 120);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, miner.clone(), cm);
|
||||
|
||||
// 7) Ensure check_repair creates missing accounts
|
||||
wallet::controller::owner_single_use(wallet10.clone(), |api| {
|
||||
api.check_repair()?;
|
||||
api.set_active_account("account_1")?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 6);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 21);
|
||||
|
||||
api.set_active_account("default")?;
|
||||
let info = test_framework::wallet_info(api)?;
|
||||
let outputs = api.retrieve_outputs(true, false, None)?.1;
|
||||
assert_eq!(outputs.len(), 15);
|
||||
assert_eq!(info.amount_currently_spendable, base_amount * 120);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn check_repair() {
|
||||
let test_dir = "test_output/check_repair";
|
||||
if let Err(e) = check_repair_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_wallets_one_seed() {
|
||||
let test_dir = "test_output/two_wallets_one_seed";
|
||||
if let Err(e) = two_wallets_one_seed_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test a wallet file send/recieve
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::ExtKeychain;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use self::wallet::{libwallet, FileWalletCommAdapter};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// self send impl
|
||||
fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.create_account_path("mining")?;
|
||||
api.create_account_path("listener")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
api.create_account_path("account1")?;
|
||||
api.create_account_path("account2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Get some mining done
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
let mut bh = 10u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize);
|
||||
|
||||
let send_file = format!("{}/part_tx_1.tx", test_dir);
|
||||
let receive_file = format!("{}/part_tx_2.tx", test_dir);
|
||||
|
||||
// test optional message
|
||||
let message = "sender test message, sender test message";
|
||||
|
||||
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
// send to send
|
||||
let (mut slate, lock_fn) = api.initiate_tx(
|
||||
Some("mining"),
|
||||
reward * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
Some(message.to_owned()), // optional message
|
||||
)?;
|
||||
// output tx file
|
||||
let file_adapter = FileWalletCommAdapter::new();
|
||||
file_adapter.send_tx_async(&send_file, &mut slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Get some mining done
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
}
|
||||
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let mut slate = adapter.receive_tx_async(&send_file)?;
|
||||
let mut naughty_slate = slate.clone();
|
||||
naughty_slate.participant_data[0].message = Some("I changed the message".to_owned());
|
||||
|
||||
// verify messages on slate match
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.verify_slate_messages(&slate)?;
|
||||
assert!(api.verify_slate_messages(&naughty_slate).is_err());
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let sender2_message = "And this is sender 2's message".to_owned();
|
||||
|
||||
// wallet 2 receives file, completes, sends file back
|
||||
wallet::controller::foreign_single_use(wallet2.clone(), |api| {
|
||||
api.receive_tx(&mut slate, None, Some(sender2_message.clone()))?;
|
||||
adapter.send_tx_async(&receive_file, &mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 finalises and posts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let mut slate = adapter.receive_tx_async(&receive_file)?;
|
||||
api.verify_slate_messages(&slate)?;
|
||||
api.finalize_tx(&mut slate)?;
|
||||
api.post_tx(&slate.tx, false)?;
|
||||
bh += 1;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
|
||||
// Check total in mining account
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward - reward * 2);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check total in 'wallet 2' account
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet2_info.total, 2 * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check messages, all participants should have both
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?;
|
||||
assert_eq!(
|
||||
tx[0].clone().messages.unwrap().messages[0].message,
|
||||
Some(message.to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
tx[0].clone().messages.unwrap().messages[1].message,
|
||||
Some(sender2_message.to_owned())
|
||||
);
|
||||
|
||||
let msg_json = serde_json::to_string_pretty(&tx[0].clone().messages.unwrap()).unwrap();
|
||||
println!("{}", msg_json);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (_, tx) = api.retrieve_txs(true, None, Some(slate.id))?;
|
||||
assert_eq!(
|
||||
tx[0].clone().messages.unwrap().messages[0].message,
|
||||
Some(message.to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
tx[0].clone().messages.unwrap().messages[1].message,
|
||||
Some(sender2_message)
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_file_exchange() {
|
||||
let test_dir = "test_output/file_exchange";
|
||||
if let Err(e) = file_exchange_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! core::libtx specific tests
|
||||
use self::core::core::transaction;
|
||||
use self::core::libtx::{aggsig, proof};
|
||||
use self::keychain::{BlindSum, BlindingFactor, ExtKeychain, Keychain};
|
||||
use self::util::secp;
|
||||
use self::util::secp::key::{PublicKey, SecretKey};
|
||||
use self::wallet::libwallet::types::Context;
|
||||
use self::wallet::{EncryptedWalletSeed, WalletSeed};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use rand::thread_rng;
|
||||
|
||||
fn kernel_sig_msg() -> secp::Message {
|
||||
transaction::kernel_sig_msg(0, 0, transaction::KernelFeatures::Plain).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggsig_sender_receiver_interaction() {
|
||||
let sender_keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
|
||||
// Calculate the kernel excess here for convenience.
|
||||
// Normally this would happen during transaction building.
|
||||
let kernel_excess = {
|
||||
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
let skey1 = sender_keychain.derive_key(0, &id1).unwrap();
|
||||
let skey2 = receiver_keychain.derive_key(0, &id1).unwrap();
|
||||
|
||||
let keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let blinding_factor = keychain
|
||||
.blind_sum(
|
||||
&BlindSum::new()
|
||||
.sub_blinding_factor(BlindingFactor::from_secret_key(skey1))
|
||||
.add_blinding_factor(BlindingFactor::from_secret_key(skey2)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
keychain
|
||||
.secp()
|
||||
.commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap())
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let s_cx;
|
||||
let mut rx_cx;
|
||||
// sender starts the tx interaction
|
||||
let (sender_pub_excess, _sender_pub_nonce) = {
|
||||
let keychain = sender_keychain.clone();
|
||||
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
let skey = keychain.derive_key(0, &id1).unwrap();
|
||||
|
||||
// dealing with an input here so we need to negate the blinding_factor
|
||||
// rather than use it as is
|
||||
let bs = BlindSum::new();
|
||||
let blinding_factor = keychain
|
||||
.blind_sum(&bs.sub_blinding_factor(BlindingFactor::from_secret_key(skey)))
|
||||
.unwrap();
|
||||
|
||||
let blind = blinding_factor.secret_key(&keychain.secp()).unwrap();
|
||||
|
||||
s_cx = Context::new(&keychain.secp(), blind);
|
||||
s_cx.get_public_keys(&keychain.secp())
|
||||
};
|
||||
|
||||
let pub_nonce_sum;
|
||||
let pub_key_sum;
|
||||
// receiver receives partial tx
|
||||
let (receiver_pub_excess, _receiver_pub_nonce, rx_sig_part) = {
|
||||
let keychain = receiver_keychain.clone();
|
||||
let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
|
||||
// let blind = blind_sum.secret_key(&keychain.secp())?;
|
||||
let blind = keychain.derive_key(0, &key_id).unwrap();
|
||||
|
||||
rx_cx = Context::new(&keychain.secp(), blind);
|
||||
let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp());
|
||||
rx_cx.add_output(&key_id, &None);
|
||||
|
||||
pub_nonce_sum = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).1,
|
||||
&rx_cx.get_public_keys(keychain.secp()).1,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
pub_key_sum = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).0,
|
||||
&rx_cx.get_public_keys(keychain.secp()).0,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&rx_cx.sec_key,
|
||||
&rx_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
(pub_excess, pub_nonce, sig_part)
|
||||
};
|
||||
|
||||
// check the sender can verify the partial signature
|
||||
// received in the response back from the receiver
|
||||
{
|
||||
let keychain = sender_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies = aggsig::verify_partial_sig(
|
||||
&keychain.secp(),
|
||||
&rx_sig_part,
|
||||
&pub_nonce_sum,
|
||||
&receiver_pub_excess,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// now sender signs with their key
|
||||
let sender_sig_part = {
|
||||
let keychain = sender_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&s_cx.sec_key,
|
||||
&s_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
sig_part
|
||||
};
|
||||
|
||||
// check the receiver can verify the partial signature
|
||||
// received by the sender
|
||||
{
|
||||
let keychain = receiver_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies = aggsig::verify_partial_sig(
|
||||
&keychain.secp(),
|
||||
&sender_sig_part,
|
||||
&pub_nonce_sum,
|
||||
&sender_pub_excess,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// Receiver now builds final signature from sender and receiver parts
|
||||
let (final_sig, final_pubkey) = {
|
||||
let keychain = receiver_keychain.clone();
|
||||
|
||||
let msg = kernel_sig_msg();
|
||||
let our_sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&rx_cx.sec_key,
|
||||
&rx_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Receiver now generates final signature from the two parts
|
||||
let final_sig = aggsig::add_signatures(
|
||||
&keychain.secp(),
|
||||
vec![&sender_sig_part, &our_sig_part],
|
||||
&pub_nonce_sum,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Receiver calculates the final public key (to verify sig later)
|
||||
let final_pubkey = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).0,
|
||||
&rx_cx.get_public_keys(keychain.secp()).0,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(final_sig, final_pubkey)
|
||||
};
|
||||
|
||||
// Receiver checks the final signature verifies
|
||||
{
|
||||
let keychain = receiver_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
|
||||
// Receiver check the final signature verifies
|
||||
let sig_verifies = aggsig::verify_completed_sig(
|
||||
&keychain.secp(),
|
||||
&final_sig,
|
||||
&final_pubkey,
|
||||
Some(&final_pubkey),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// Check we can verify the sig using the kernel excess
|
||||
{
|
||||
let keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies =
|
||||
aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess);
|
||||
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggsig_sender_receiver_interaction_offset() {
|
||||
let sender_keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let receiver_keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
|
||||
// This is the kernel offset that we use to split the key
|
||||
// Summing these at the block level prevents the
|
||||
// kernels from being used to reconstruct (or identify) individual transactions
|
||||
let kernel_offset = SecretKey::new(&sender_keychain.secp(), &mut thread_rng());
|
||||
|
||||
// Calculate the kernel excess here for convenience.
|
||||
// Normally this would happen during transaction building.
|
||||
let kernel_excess = {
|
||||
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
let skey1 = sender_keychain.derive_key(0, &id1).unwrap();
|
||||
let skey2 = receiver_keychain.derive_key(0, &id1).unwrap();
|
||||
|
||||
let keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let blinding_factor = keychain
|
||||
.blind_sum(
|
||||
&BlindSum::new()
|
||||
.sub_blinding_factor(BlindingFactor::from_secret_key(skey1))
|
||||
.add_blinding_factor(BlindingFactor::from_secret_key(skey2))
|
||||
// subtract the kernel offset here like as would when
|
||||
// verifying a kernel signature
|
||||
.sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
keychain
|
||||
.secp()
|
||||
.commit(0, blinding_factor.secret_key(&keychain.secp()).unwrap())
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let s_cx;
|
||||
let mut rx_cx;
|
||||
// sender starts the tx interaction
|
||||
let (sender_pub_excess, _sender_pub_nonce) = {
|
||||
let keychain = sender_keychain.clone();
|
||||
let id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
let skey = keychain.derive_key(0, &id1).unwrap();
|
||||
|
||||
// dealing with an input here so we need to negate the blinding_factor
|
||||
// rather than use it as is
|
||||
let blinding_factor = keychain
|
||||
.blind_sum(
|
||||
&BlindSum::new()
|
||||
.sub_blinding_factor(BlindingFactor::from_secret_key(skey))
|
||||
// subtract the kernel offset to create an aggsig context
|
||||
// with our "split" key
|
||||
.sub_blinding_factor(BlindingFactor::from_secret_key(kernel_offset)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let blind = blinding_factor.secret_key(&keychain.secp()).unwrap();
|
||||
|
||||
s_cx = Context::new(&keychain.secp(), blind);
|
||||
s_cx.get_public_keys(&keychain.secp())
|
||||
};
|
||||
|
||||
// receiver receives partial tx
|
||||
let pub_nonce_sum;
|
||||
let pub_key_sum;
|
||||
let (receiver_pub_excess, _receiver_pub_nonce, sig_part) = {
|
||||
let keychain = receiver_keychain.clone();
|
||||
let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
|
||||
let blind = keychain.derive_key(0, &key_id).unwrap();
|
||||
|
||||
rx_cx = Context::new(&keychain.secp(), blind);
|
||||
let (pub_excess, pub_nonce) = rx_cx.get_public_keys(&keychain.secp());
|
||||
rx_cx.add_output(&key_id, &None);
|
||||
|
||||
pub_nonce_sum = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).1,
|
||||
&rx_cx.get_public_keys(keychain.secp()).1,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
pub_key_sum = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).0,
|
||||
&rx_cx.get_public_keys(keychain.secp()).0,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&rx_cx.sec_key,
|
||||
&rx_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
(pub_excess, pub_nonce, sig_part)
|
||||
};
|
||||
|
||||
// check the sender can verify the partial signature
|
||||
// received in the response back from the receiver
|
||||
{
|
||||
let keychain = sender_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies = aggsig::verify_partial_sig(
|
||||
&keychain.secp(),
|
||||
&sig_part,
|
||||
&pub_nonce_sum,
|
||||
&receiver_pub_excess,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// now sender signs with their key
|
||||
let sender_sig_part = {
|
||||
let keychain = sender_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&s_cx.sec_key,
|
||||
&s_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
sig_part
|
||||
};
|
||||
|
||||
// check the receiver can verify the partial signature
|
||||
// received by the sender
|
||||
{
|
||||
let keychain = receiver_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies = aggsig::verify_partial_sig(
|
||||
&keychain.secp(),
|
||||
&sender_sig_part,
|
||||
&pub_nonce_sum,
|
||||
&sender_pub_excess,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// Receiver now builds final signature from sender and receiver parts
|
||||
let (final_sig, final_pubkey) = {
|
||||
let keychain = receiver_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
let our_sig_part = aggsig::calculate_partial_sig(
|
||||
&keychain.secp(),
|
||||
&rx_cx.sec_key,
|
||||
&rx_cx.sec_nonce,
|
||||
&pub_nonce_sum,
|
||||
Some(&pub_key_sum),
|
||||
&msg,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Receiver now generates final signature from the two parts
|
||||
let final_sig = aggsig::add_signatures(
|
||||
&keychain.secp(),
|
||||
vec![&sender_sig_part, &our_sig_part],
|
||||
&pub_nonce_sum,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Receiver calculates the final public key (to verify sig later)
|
||||
let final_pubkey = PublicKey::from_combination(
|
||||
keychain.secp(),
|
||||
vec![
|
||||
&s_cx.get_public_keys(keychain.secp()).0,
|
||||
&rx_cx.get_public_keys(keychain.secp()).0,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(final_sig, final_pubkey)
|
||||
};
|
||||
|
||||
// Receiver checks the final signature verifies
|
||||
{
|
||||
let keychain = receiver_keychain.clone();
|
||||
let msg = kernel_sig_msg();
|
||||
|
||||
// Receiver check the final signature verifies
|
||||
let sig_verifies = aggsig::verify_completed_sig(
|
||||
&keychain.secp(),
|
||||
&final_sig,
|
||||
&final_pubkey,
|
||||
Some(&final_pubkey),
|
||||
&msg,
|
||||
);
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
|
||||
// Check we can verify the sig using the kernel excess
|
||||
{
|
||||
let keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let msg = kernel_sig_msg();
|
||||
let sig_verifies =
|
||||
aggsig::verify_single_from_commit(&keychain.secp(), &final_sig, &msg, &kernel_excess);
|
||||
|
||||
assert!(!sig_verifies.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewind_range_proof() {
|
||||
let keychain = ExtKeychain::from_random_seed(true).unwrap();
|
||||
let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
|
||||
let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0);
|
||||
let commit = keychain.commit(5, &key_id).unwrap();
|
||||
let extra_data = [99u8; 64];
|
||||
|
||||
let proof = proof::create(
|
||||
&keychain,
|
||||
5,
|
||||
&key_id,
|
||||
commit,
|
||||
Some(extra_data.to_vec().clone()),
|
||||
)
|
||||
.unwrap();
|
||||
let proof_info =
|
||||
proof::rewind(&keychain, commit, Some(extra_data.to_vec().clone()), proof).unwrap();
|
||||
|
||||
assert_eq!(proof_info.success, true);
|
||||
assert_eq!(proof_info.value, 5);
|
||||
assert_eq!(proof_info.message.as_bytes(), key_id.serialize_path());
|
||||
|
||||
// cannot rewind with a different commit
|
||||
let commit2 = keychain.commit(5, &key_id2).unwrap();
|
||||
let proof_info =
|
||||
proof::rewind(&keychain, commit2, Some(extra_data.to_vec().clone()), proof).unwrap();
|
||||
assert_eq!(proof_info.success, false);
|
||||
assert_eq!(proof_info.value, 0);
|
||||
assert_eq!(proof_info.message, secp::pedersen::ProofMessage::empty());
|
||||
|
||||
// cannot rewind with a commitment to a different value
|
||||
let commit3 = keychain.commit(4, &key_id).unwrap();
|
||||
let proof_info =
|
||||
proof::rewind(&keychain, commit3, Some(extra_data.to_vec().clone()), proof).unwrap();
|
||||
assert_eq!(proof_info.success, false);
|
||||
assert_eq!(proof_info.value, 0);
|
||||
|
||||
// cannot rewind with wrong extra committed data
|
||||
let commit3 = keychain.commit(4, &key_id).unwrap();
|
||||
let wrong_extra_data = [98u8; 64];
|
||||
let _should_err = proof::rewind(
|
||||
&keychain,
|
||||
commit3,
|
||||
Some(wrong_extra_data.to_vec().clone()),
|
||||
proof,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(proof_info.success, false);
|
||||
assert_eq!(proof_info.value, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_seed_encrypt() {
|
||||
let password = "passwoid";
|
||||
let wallet_seed = WalletSeed::init_new(32);
|
||||
let mut enc_wallet_seed = EncryptedWalletSeed::from_seed(&wallet_seed, password).unwrap();
|
||||
println!("EWS: {:?}", enc_wallet_seed);
|
||||
let decrypted_wallet_seed = enc_wallet_seed.decrypt(password).unwrap();
|
||||
assert_eq!(wallet_seed, decrypted_wallet_seed);
|
||||
|
||||
// Wrong password
|
||||
let decrypted_wallet_seed = enc_wallet_seed.decrypt("");
|
||||
assert!(decrypted_wallet_seed.is_err());
|
||||
|
||||
// Wrong nonce
|
||||
enc_wallet_seed.nonce = "wrongnonce".to_owned();
|
||||
let decrypted_wallet_seed = enc_wallet_seed.decrypt(password);
|
||||
assert!(decrypted_wallet_seed.is_err());
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test a wallet repost command
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::ExtKeychain;
|
||||
use self::libwallet::slate::Slate;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use self::wallet::{libwallet, FileWalletCommAdapter};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// self send impl
|
||||
fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.create_account_path("mining")?;
|
||||
api.create_account_path("listener")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
api.create_account_path("account1")?;
|
||||
api.create_account_path("account2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Get some mining done
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
let mut bh = 10u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize);
|
||||
|
||||
let send_file = format!("{}/part_tx_1.tx", test_dir);
|
||||
let receive_file = format!("{}/part_tx_2.tx", test_dir);
|
||||
|
||||
let mut slate = Slate::blank(2);
|
||||
|
||||
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
// send to send
|
||||
let (mut slate, lock_fn) = api.initiate_tx(
|
||||
Some("mining"),
|
||||
reward * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
// output tx file
|
||||
let file_adapter = FileWalletCommAdapter::new();
|
||||
file_adapter.send_tx_async(&send_file, &mut slate)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
|
||||
// wallet 1 receives file to different account, completes
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("listener")?;
|
||||
}
|
||||
|
||||
wallet::controller::foreign_single_use(wallet1.clone(), |api| {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
slate = adapter.receive_tx_async(&send_file)?;
|
||||
api.receive_tx(&mut slate, None, None)?;
|
||||
adapter.send_tx_async(&receive_file, &mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 receives file to different account, completes
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
|
||||
// wallet 1 finalize
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
slate = adapter.receive_tx_async(&receive_file)?;
|
||||
api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Now repost from cached
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, txs) = api.retrieve_txs(true, None, Some(slate.id))?;
|
||||
let stored_tx = api.get_stored_tx(&txs[0])?;
|
||||
api.post_tx(&stored_tx.unwrap(), false)?;
|
||||
bh += 1;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
|
||||
// update/test contents of both accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward - reward * 2);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("listener")?;
|
||||
}
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet2_info.total, 2 * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// as above, but syncronously
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
}
|
||||
|
||||
let mut slate = Slate::blank(2);
|
||||
let amount = 60_000_000_000;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None,
|
||||
amount * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
|
||||
// Now repost from cached
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, txs) = api.retrieve_txs(true, None, Some(slate.id))?;
|
||||
let stored_tx = api.get_stored_tx(&txs[0])?;
|
||||
api.post_tx(&stored_tx.unwrap(), false)?;
|
||||
bh += 1;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
//
|
||||
// update/test contents of both accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward - reward * 4);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet2_info.total, 2 * amount);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_file_repost() {
|
||||
let test_dir = "test_output/file_repost";
|
||||
if let Err(e) = file_repost_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! tests for wallet restore
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::{ExtKeychain, Identifier, Keychain};
|
||||
use self::libwallet::slate::Slate;
|
||||
use self::wallet::libwallet;
|
||||
use self::wallet::libwallet::types::AcctPathMapping;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
fn restore_wallet(base_dir: &str, wallet_dir: &str) -> Result<(), libwallet::Error> {
|
||||
let source_seed = format!("{}/{}/wallet.seed", base_dir, wallet_dir);
|
||||
let dest_dir = format!("{}/{}_restore", base_dir, wallet_dir);
|
||||
fs::create_dir_all(dest_dir.clone())?;
|
||||
let dest_seed = format!("{}/wallet.seed", dest_dir);
|
||||
println!("Source: {}, Dest: {}", source_seed, dest_seed);
|
||||
fs::copy(source_seed, dest_seed)?;
|
||||
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(base_dir);
|
||||
let client = LocalWalletClient::new(wallet_dir, wallet_proxy.tx.clone());
|
||||
|
||||
let wallet = test_framework::create_wallet(&dest_dir, client.clone(), None);
|
||||
|
||||
wallet_proxy.add_wallet(wallet_dir, client.get_send_instance(), wallet.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
let wp_running = wallet_proxy.running.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// perform the restore and update wallet info
|
||||
wallet::controller::owner_single_use(wallet.clone(), |api| {
|
||||
let _ = api.restore()?;
|
||||
let _ = api.retrieve_summary_info(true, 1)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wp_running.store(false, Ordering::Relaxed);
|
||||
//thread::sleep(Duration::from_millis(1000));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compare_wallet_restore(
|
||||
base_dir: &str,
|
||||
wallet_dir: &str,
|
||||
account_path: &Identifier,
|
||||
) -> Result<(), libwallet::Error> {
|
||||
let restore_name = format!("{}_restore", wallet_dir);
|
||||
let source_dir = format!("{}/{}", base_dir, wallet_dir);
|
||||
let dest_dir = format!("{}/{}", base_dir, restore_name);
|
||||
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(base_dir);
|
||||
|
||||
let client = LocalWalletClient::new(wallet_dir, wallet_proxy.tx.clone());
|
||||
let wallet_source = test_framework::create_wallet(&source_dir, client.clone(), None);
|
||||
wallet_proxy.add_wallet(
|
||||
&wallet_dir,
|
||||
client.get_send_instance(),
|
||||
wallet_source.clone(),
|
||||
);
|
||||
|
||||
let client = LocalWalletClient::new(&restore_name, wallet_proxy.tx.clone());
|
||||
let wallet_dest = test_framework::create_wallet(&dest_dir, client.clone(), None);
|
||||
wallet_proxy.add_wallet(
|
||||
&restore_name,
|
||||
client.get_send_instance(),
|
||||
wallet_dest.clone(),
|
||||
);
|
||||
|
||||
{
|
||||
let mut w = wallet_source.lock();
|
||||
w.set_parent_key_id(account_path.clone());
|
||||
}
|
||||
|
||||
{
|
||||
let mut w = wallet_dest.lock();
|
||||
w.set_parent_key_id(account_path.clone());
|
||||
}
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
let wp_running = wallet_proxy.running.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut src_info: Option<libwallet::types::WalletInfo> = None;
|
||||
let mut dest_info: Option<libwallet::types::WalletInfo> = None;
|
||||
|
||||
let mut src_txs: Option<Vec<libwallet::types::TxLogEntry>> = None;
|
||||
let mut dest_txs: Option<Vec<libwallet::types::TxLogEntry>> = None;
|
||||
|
||||
let mut src_accts: Option<Vec<AcctPathMapping>> = None;
|
||||
let mut dest_accts: Option<Vec<AcctPathMapping>> = None;
|
||||
|
||||
// Overall wallet info should be the same
|
||||
wallet::controller::owner_single_use(wallet_source.clone(), |api| {
|
||||
src_info = Some(api.retrieve_summary_info(true, 1)?.1);
|
||||
src_txs = Some(api.retrieve_txs(true, None, None)?.1);
|
||||
src_accts = Some(api.accounts()?);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet_dest.clone(), |api| {
|
||||
dest_info = Some(api.retrieve_summary_info(true, 1)?.1);
|
||||
dest_txs = Some(api.retrieve_txs(true, None, None)?.1);
|
||||
dest_accts = Some(api.accounts()?);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Info should all be the same
|
||||
assert_eq!(src_info, dest_info);
|
||||
|
||||
// Net differences in TX logs should be the same
|
||||
let src_sum: i64 = src_txs
|
||||
.clone()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t.amount_credited as i64 - t.amount_debited as i64)
|
||||
.sum();
|
||||
|
||||
let dest_sum: i64 = dest_txs
|
||||
.clone()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t.amount_credited as i64 - t.amount_debited as i64)
|
||||
.sum();
|
||||
|
||||
assert_eq!(src_sum, dest_sum);
|
||||
|
||||
// Number of created accounts should be the same
|
||||
assert_eq!(
|
||||
src_accts.as_ref().unwrap().len(),
|
||||
dest_accts.as_ref().unwrap().len()
|
||||
);
|
||||
|
||||
wp_running.store(false, Ordering::Relaxed);
|
||||
//thread::sleep(Duration::from_millis(1000));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build up 2 wallets, perform a few transactions on them
|
||||
/// Then attempt to restore them in separate directories and check contents are the same
|
||||
fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
// define recipient wallet, add to proxy
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// wallet 2 will use another account
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
api.create_account_path("account1")?;
|
||||
api.create_account_path("account2")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Default wallet 2 to listen on that account
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("account1")?;
|
||||
}
|
||||
|
||||
// Another wallet
|
||||
let client3 = LocalWalletClient::new("wallet3", wallet_proxy.tx.clone());
|
||||
let wallet3 =
|
||||
test_framework::create_wallet(&format!("{}/wallet3", test_dir), client3.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet3", client3.get_send_instance(), wallet3.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
let wp_running = wallet_proxy.running.clone();
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// mine a few blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10);
|
||||
|
||||
// assert wallet contents
|
||||
// and a single use api for a send command
|
||||
let amount = 60_000_000_000;
|
||||
let mut slate = Slate::blank(1);
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None, amount, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
sender_api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
|
||||
// Send some to wallet 3
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None,
|
||||
amount * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet3", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
sender_api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet3.clone(), 10);
|
||||
|
||||
// Wallet3 to wallet 2
|
||||
wallet::controller::owner_single_use(wallet3.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None,
|
||||
amount * 3, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client3.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
sender_api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Another listener account on wallet 2
|
||||
{
|
||||
let mut w = wallet2.lock();
|
||||
w.set_parent_key_id_by_name("account2")?;
|
||||
}
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 2);
|
||||
|
||||
// Wallet3 to wallet 2 again (to another account)
|
||||
wallet::controller::owner_single_use(wallet3.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None,
|
||||
amount * 3, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client3.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
sender_api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5);
|
||||
|
||||
// update everyone
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let _ = api.retrieve_summary_info(true, 1)?;
|
||||
Ok(())
|
||||
})?;
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let _ = api.retrieve_summary_info(true, 1)?;
|
||||
Ok(())
|
||||
})?;
|
||||
wallet::controller::owner_single_use(wallet3.clone(), |api| {
|
||||
let _ = api.retrieve_summary_info(true, 1)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wp_running.store(false, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn perform_restore(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
restore_wallet(test_dir, "wallet1")?;
|
||||
compare_wallet_restore(
|
||||
test_dir,
|
||||
"wallet1",
|
||||
&ExtKeychain::derive_key_id(2, 0, 0, 0, 0),
|
||||
)?;
|
||||
restore_wallet(test_dir, "wallet2")?;
|
||||
compare_wallet_restore(
|
||||
test_dir,
|
||||
"wallet2",
|
||||
&ExtKeychain::derive_key_id(2, 0, 0, 0, 0),
|
||||
)?;
|
||||
compare_wallet_restore(
|
||||
test_dir,
|
||||
"wallet2",
|
||||
&ExtKeychain::derive_key_id(2, 1, 0, 0, 0),
|
||||
)?;
|
||||
compare_wallet_restore(
|
||||
test_dir,
|
||||
"wallet2",
|
||||
&ExtKeychain::derive_key_id(2, 2, 0, 0, 0),
|
||||
)?;
|
||||
restore_wallet(test_dir, "wallet3")?;
|
||||
compare_wallet_restore(
|
||||
test_dir,
|
||||
"wallet3",
|
||||
&ExtKeychain::derive_key_id(2, 0, 0, 0, 0),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_restore() {
|
||||
let test_dir = "test_output/wallet_restore";
|
||||
if let Err(e) = setup_restore(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
if let Err(e) = perform_restore(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test a wallet sending to self
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::ExtKeychain;
|
||||
use self::wallet::libwallet;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// self send impl
|
||||
fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
|
||||
// add some accounts
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.create_account_path("mining")?;
|
||||
api.create_account_path("listener")?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Get some mining done
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("mining")?;
|
||||
}
|
||||
let mut bh = 10u64;
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), bh as usize);
|
||||
|
||||
// Should have 5 in account1 (5 spendable), 5 in account (2 spendable)
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward);
|
||||
// send to send
|
||||
let (mut slate, lock_fn) = api.initiate_tx(
|
||||
Some("mining"),
|
||||
reward * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
// Send directly to self
|
||||
wallet::controller::foreign_single_use(wallet1.clone(), |api| {
|
||||
api.receive_tx(&mut slate, Some("listener"), None)?;
|
||||
Ok(())
|
||||
})?;
|
||||
api.finalize_tx(&mut slate)?;
|
||||
api.post_tx(&slate.tx, false)?; // mines a block
|
||||
bh += 1;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
bh += 3;
|
||||
|
||||
// Check total in mining account
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, bh * reward - reward * 2);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check total in 'listener' account
|
||||
{
|
||||
let mut w = wallet1.lock();
|
||||
w.set_parent_key_id_by_name("listener")?;
|
||||
}
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(wallet1_info.last_confirmed_height, bh);
|
||||
assert_eq!(wallet1_info.total, 2 * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wallet_self_send() {
|
||||
let test_dir = "test_output/self_send";
|
||||
if let Err(e) = self_send_test_impl(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
// Copyright 2018 The Grin Developers
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! tests for transactions building within core::libtx
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
use self::core::global;
|
||||
use self::core::global::ChainTypes;
|
||||
use self::keychain::ExtKeychain;
|
||||
use self::libwallet::slate::Slate;
|
||||
use self::wallet::libwallet;
|
||||
use self::wallet::libwallet::types::OutputStatus;
|
||||
use self::wallet::test_framework::{self, LocalWalletClient, WalletProxy};
|
||||
use grin_core as core;
|
||||
use grin_keychain as keychain;
|
||||
use grin_util as util;
|
||||
use grin_wallet as wallet;
|
||||
use std::fs;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
}
|
||||
|
||||
/// Exercises the Transaction API fully with a test NodeClient operating
|
||||
/// directly on a chain instance
|
||||
/// Callable with any type of wallet
|
||||
fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
// define recipient wallet, add to proxy
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height
|
||||
// mine a few blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 10);
|
||||
|
||||
// Check wallet 1 contents are as expected
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
debug!(
|
||||
"Wallet 1 Info Pre-Transaction, after {} blocks: {:?}",
|
||||
wallet1_info.last_confirmed_height, wallet1_info
|
||||
);
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(
|
||||
wallet1_info.amount_currently_spendable,
|
||||
(wallet1_info.last_confirmed_height - cm) * reward
|
||||
);
|
||||
assert_eq!(wallet1_info.amount_immature, cm * reward);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// assert wallet contents
|
||||
// and a single use api for a send command
|
||||
let amount = 60_000_000_000;
|
||||
let mut slate = Slate::blank(1);
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None, amount, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 1
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (_, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
let fee = core::libtx::tx_fee(
|
||||
wallet1_info.last_confirmed_height as usize - cm as usize,
|
||||
2,
|
||||
1,
|
||||
None,
|
||||
);
|
||||
// we should have a transaction entry for this slate
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let tx = tx.unwrap();
|
||||
assert!(!tx.confirmed);
|
||||
assert!(tx.confirmation_ts.is_none());
|
||||
assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount);
|
||||
assert_eq!(Some(fee), tx.fee);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
// we should have a transaction entry for this slate
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let tx = tx.unwrap();
|
||||
assert!(!tx.confirmed);
|
||||
assert!(tx.confirmation_ts.is_none());
|
||||
assert_eq!(amount, tx.amount_credited);
|
||||
assert_eq!(0, tx.amount_debited);
|
||||
assert_eq!(None, tx.fee);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// post transaction
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
api.post_tx(&slate.tx, false)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check wallet 1 contents are as expected
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
debug!(
|
||||
"Wallet 1 Info Post Transaction, after {} blocks: {:?}",
|
||||
wallet1_info.last_confirmed_height, wallet1_info
|
||||
);
|
||||
let fee = core::libtx::tx_fee(
|
||||
wallet1_info.last_confirmed_height as usize - 1 - cm as usize,
|
||||
2,
|
||||
1,
|
||||
None,
|
||||
);
|
||||
assert!(wallet1_refreshed);
|
||||
// wallet 1 received fees, so amount should be the same
|
||||
assert_eq!(
|
||||
wallet1_info.total,
|
||||
amount * wallet1_info.last_confirmed_height - amount
|
||||
);
|
||||
assert_eq!(
|
||||
wallet1_info.amount_currently_spendable,
|
||||
(wallet1_info.last_confirmed_height - cm) * reward - amount - fee
|
||||
);
|
||||
assert_eq!(wallet1_info.amount_immature, cm * reward + fee);
|
||||
|
||||
// check tx log entry is confirmed
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let tx = tx.unwrap();
|
||||
assert!(tx.confirmed);
|
||||
assert!(tx.confirmation_ts.is_some());
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
|
||||
// refresh wallets and retrieve info/tests for each wallet after maturity
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
debug!("Wallet 1 Info: {:?}", wallet1_info);
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(
|
||||
wallet1_info.total,
|
||||
amount * wallet1_info.last_confirmed_height - amount
|
||||
);
|
||||
assert_eq!(
|
||||
wallet1_info.amount_currently_spendable,
|
||||
(wallet1_info.last_confirmed_height - cm - 1) * reward
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, amount);
|
||||
|
||||
// check tx log entry is confirmed
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let tx = tx.unwrap();
|
||||
assert!(tx.confirmed);
|
||||
assert!(tx.confirmation_ts.is_some());
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Estimate fee and locked amount for a transaction
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
let (total, fee) = sender_api.estimate_initiate_tx(
|
||||
None,
|
||||
amount * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
)?;
|
||||
assert_eq!(total, 600_000_000_000);
|
||||
assert_eq!(fee, 4_000_000);
|
||||
|
||||
let (total, fee) = sender_api.estimate_initiate_tx(
|
||||
None,
|
||||
amount * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
false, // select the smallest amount of outputs
|
||||
)?;
|
||||
assert_eq!(total, 180_000_000_000);
|
||||
assert_eq!(fee, 6_000_000);
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Send another transaction, but don't post to chain immediately and use
|
||||
// the stored transaction instead
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None,
|
||||
amount * 2, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
let (refreshed, _wallet1_info) = sender_api.retrieve_summary_info(true, 1)?;
|
||||
assert!(refreshed);
|
||||
let (_, txs) = sender_api.retrieve_txs(true, None, None)?;
|
||||
// find the transaction
|
||||
let tx = txs
|
||||
.iter()
|
||||
.find(|t| t.tx_slate_id == Some(slate.id))
|
||||
.unwrap();
|
||||
let stored_tx = sender_api.get_stored_tx(&tx)?;
|
||||
sender_api.post_tx(&stored_tx.unwrap(), false)?;
|
||||
let (_, wallet1_info) = sender_api.retrieve_summary_info(true, 1)?;
|
||||
// should be mined now
|
||||
assert_eq!(
|
||||
wallet1_info.total,
|
||||
amount * wallet1_info.last_confirmed_height - amount * 3
|
||||
);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
|
||||
// check wallet2 has stored transaction
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, amount * 3);
|
||||
|
||||
// check tx log entry is confirmed
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let tx = tx.unwrap();
|
||||
assert!(tx.confirmed);
|
||||
assert!(tx.confirmation_ts.is_some());
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test rolling back transactions and outputs when a transaction is never
|
||||
/// posted to a chain
|
||||
fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> {
|
||||
setup(test_dir);
|
||||
// Create a new proxy to simulate server and wallet responses
|
||||
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(test_dir);
|
||||
let chain = wallet_proxy.chain.clone();
|
||||
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client1 = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 =
|
||||
test_framework::create_wallet(&format!("{}/wallet1", test_dir), client1.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet1", client1.get_send_instance(), wallet1.clone());
|
||||
|
||||
// define recipient wallet, add to proxy
|
||||
let client2 = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 =
|
||||
test_framework::create_wallet(&format!("{}/wallet2", test_dir), client2.clone(), None);
|
||||
wallet_proxy.add_wallet("wallet2", client2.get_send_instance(), wallet2.clone());
|
||||
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!("Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity(); // assume all testing precedes soft fork height
|
||||
// mine a few blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5);
|
||||
|
||||
let amount = 30_000_000_000;
|
||||
let mut slate = Slate::blank(1);
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let (slate_i, lock_fn) = sender_api.initiate_tx(
|
||||
None, amount, // amount
|
||||
2, // minimum confirmations
|
||||
500, // max outputs
|
||||
1, // num change outputs
|
||||
true, // select all outputs
|
||||
None,
|
||||
)?;
|
||||
slate = client1.send_tx_slate_direct("wallet2", &slate_i)?;
|
||||
sender_api.tx_lock_outputs(&slate, lock_fn)?;
|
||||
sender_api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 1
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
println!(
|
||||
"last confirmed height: {}",
|
||||
wallet1_info.last_confirmed_height
|
||||
);
|
||||
assert!(refreshed);
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
// we should have a transaction entry for this slate
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
let mut locked_count = 0;
|
||||
let mut unconfirmed_count = 0;
|
||||
// get the tx entry, check outputs are as expected
|
||||
let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?;
|
||||
for (o, _) in outputs.clone() {
|
||||
if o.status == OutputStatus::Locked {
|
||||
locked_count = locked_count + 1;
|
||||
}
|
||||
if o.status == OutputStatus::Unconfirmed {
|
||||
unconfirmed_count = unconfirmed_count + 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(outputs.len(), 3);
|
||||
assert_eq!(locked_count, 2);
|
||||
assert_eq!(unconfirmed_count, 1);
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Check transaction log for wallet 2
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (refreshed, txs) = api.retrieve_txs(true, None, None)?;
|
||||
assert!(refreshed);
|
||||
let mut unconfirmed_count = 0;
|
||||
let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id));
|
||||
assert!(tx.is_some());
|
||||
// get the tx entry, check outputs are as expected
|
||||
let (_, outputs) = api.retrieve_outputs(true, false, Some(tx.unwrap().id))?;
|
||||
for (o, _) in outputs.clone() {
|
||||
if o.status == OutputStatus::Unconfirmed {
|
||||
unconfirmed_count = unconfirmed_count + 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert_eq!(unconfirmed_count, 1);
|
||||
let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, 0,);
|
||||
assert_eq!(wallet2_info.total, amount);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// wallet 1 is bold and doesn't ever post the transaction mine a few more blocks
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5);
|
||||
|
||||
// Wallet 1 decides to roll back instead
|
||||
wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
// can't roll back coinbase
|
||||
let res = api.cancel_tx(Some(1), None);
|
||||
assert!(res.is_err());
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
let tx = txs
|
||||
.iter()
|
||||
.find(|t| t.tx_slate_id == Some(slate.id))
|
||||
.unwrap();
|
||||
api.cancel_tx(Some(tx.id), None)?;
|
||||
let (refreshed, wallet1_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(refreshed);
|
||||
println!(
|
||||
"last confirmed height: {}",
|
||||
wallet1_info.last_confirmed_height
|
||||
);
|
||||
// check all eligible inputs should be now be spendable
|
||||
println!("cm: {}", cm);
|
||||
assert_eq!(
|
||||
wallet1_info.amount_currently_spendable,
|
||||
(wallet1_info.last_confirmed_height - cm) * reward
|
||||
);
|
||||
// can't roll back again
|
||||
let res = api.cancel_tx(Some(tx.id), None);
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Wallet 2 rolls back
|
||||
wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (_, txs) = api.retrieve_txs(true, None, None)?;
|
||||
let tx = txs
|
||||
.iter()
|
||||
.find(|t| t.tx_slate_id == Some(slate.id))
|
||||
.unwrap();
|
||||
api.cancel_tx(Some(tx.id), None)?;
|
||||
let (refreshed, wallet2_info) = api.retrieve_summary_info(true, 1)?;
|
||||
assert!(refreshed);
|
||||
// check all eligible inputs should be now be spendable
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, 0,);
|
||||
assert_eq!(wallet2_info.total, 0,);
|
||||
// can't roll back again
|
||||
let res = api.cancel_tx(Some(tx.id), None);
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_wallet_basic_transaction_api() {
|
||||
let test_dir = "test_output/basic_transaction_api";
|
||||
if let Err(e) = basic_transaction_api(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_wallet_tx_rollback() {
|
||||
let test_dir = "test_output/tx_rollback";
|
||||
if let Err(e) = tx_rollback(test_dir) {
|
||||
panic!("Libwallet Error: {} - {}", e, e.backtrace().unwrap());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user