initial commit

This commit is contained in:
yeastplume
2019-02-13 15:05:19 +00:00
commit da288f0139
57 changed files with 17941 additions and 0 deletions
Generated
+2665
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -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"
+23
View File
@@ -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.
+223
View File
@@ -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()
}
+275
View File
@@ -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(())
}
}
+34
View File
@@ -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};
+87
View File
@@ -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>,
}
+28
View File
@@ -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" }
+968
View File
@@ -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(())
}
}
+300
View File
@@ -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)),
}
}
}
+28
View File
@@ -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;
+120
View File
@@ -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(())
}
+452
View File
@@ -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(())
}
+547
View File
@@ -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
}
}
+321
View File
@@ -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());
}
}
+517
View File
@@ -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))
}
+48
View File
@@ -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};
+559
View File
@@ -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(())
}
}
+18
View File
@@ -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;
+370
View File
@@ -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,
}
}
}
+721
View File
@@ -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.
+90
View File
@@ -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(&current_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(&current_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();
}
}
+21
View File
@@ -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};
+68
View File
@@ -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
}
}
+634
View File
@@ -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())
}
}
+528
View File
@@ -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(&current_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());
}
}
}
+187
View File
@@ -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()),
}
}
+303
View File
@@ -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
+69
View File
@@ -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!();
}
}
+93
View File
@@ -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(())
}
}
+463
View File
@@ -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");
}
}
}
+57
View File
@@ -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>;
}
+59
View File
@@ -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!();
}
}
+23
View File
@@ -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()
}
+553
View File
@@ -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(())
}
+822
View File
@@ -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())
}
}),
)
}
+476
View File
@@ -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(())
}
+210
View File
@@ -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())),
}
}
}
+74
View File
@@ -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)))
}
+570
View File
@@ -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(())
}
}
+219
View File
@@ -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)
}
+17
View File
@@ -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};
+225
View File
@@ -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)
}
+532
View File
@@ -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))
}
}
+361
View File
@@ -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))
}
}
+50
View File
@@ -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"),
);
}
+259
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}
+521
View File
@@ -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
View File
@@ -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());
}
}
+389
View File
@@ -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));
}
+144
View File
@@ -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());
}
}
+498
View File
@@ -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());
}
}