mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 17:01:09 +03:00
Wallet API Test Client (#1242)
* beginnings to testclient implementation for more complete wallet API testing * rework TestWalletClient to be message-based proxy * test fix * wallet tests now exercising the API directly as much as possible, capable of instantiating multiple wallets * test in place to run both file and db wallets * ensure all wallet api functions lock wallet as needed. Split up transaction creation from posting to chain
This commit is contained in:
parent
25ca2de487
commit
bacadfb5ab
13 changed files with 917 additions and 475 deletions
|
@ -27,7 +27,7 @@ use std::default::Default;
|
|||
use std::sync::{Arc, Mutex};
|
||||
use std::{fs, thread, time};
|
||||
|
||||
use wallet::{HTTPWalletClient, FileWallet, WalletConfig};
|
||||
use wallet::{FileWallet, HTTPWalletClient, WalletConfig};
|
||||
|
||||
/// Just removes all results from previous runs
|
||||
pub fn clean_all_output(test_name_dir: &str) {
|
||||
|
@ -276,8 +276,10 @@ impl LocalServerContainer {
|
|||
)
|
||||
});
|
||||
|
||||
wallet::controller::foreign_listener(Box::new(wallet), &self.wallet_config.api_listen_addr())
|
||||
.unwrap_or_else(|e| {
|
||||
wallet::controller::foreign_listener(
|
||||
Box::new(wallet),
|
||||
&self.wallet_config.api_listen_addr(),
|
||||
).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Error creating wallet listener: {:?} Config: {:?}",
|
||||
e, self.wallet_config
|
||||
|
@ -335,14 +337,15 @@ impl LocalServerContainer {
|
|||
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
|
||||
wallet.keychain = Some(keychain);
|
||||
let _ =
|
||||
wallet::controller::owner_single_use(Box::new(wallet), |api| {
|
||||
wallet::controller::owner_single_use(
|
||||
Arc::new(Mutex::new(Box::new(wallet))),
|
||||
|api| {
|
||||
let result = api.issue_send_tx(
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
dest,
|
||||
max_outputs,
|
||||
selection_strategy == "all",
|
||||
fluff,
|
||||
);
|
||||
match result {
|
||||
Ok(_) => println!(
|
||||
|
@ -356,7 +359,8 @@ impl LocalServerContainer {
|
|||
}
|
||||
};
|
||||
Ok(())
|
||||
}).unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
|
||||
},
|
||||
).unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
|
||||
}
|
||||
|
||||
/// Stops the running wallet server
|
||||
|
|
|
@ -41,7 +41,7 @@ pub mod tui;
|
|||
use std::env::current_dir;
|
||||
use std::process::exit;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -53,8 +53,10 @@ use core::core::amount_to_hr_string;
|
|||
use core::global;
|
||||
use tui::ui;
|
||||
use util::{init_logger, LoggingConfig, LOGGER};
|
||||
use wallet::{libwallet, wallet_db_exists, FileWallet,
|
||||
HTTPWalletClient, LMDBBackend, WalletConfig, WalletInst};
|
||||
use wallet::{
|
||||
libwallet, wallet_db_exists, FileWallet, HTTPWalletClient, LMDBBackend, WalletConfig,
|
||||
WalletInst,
|
||||
};
|
||||
|
||||
// include build information
|
||||
pub mod built_info {
|
||||
|
@ -606,8 +608,8 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
|
|||
info!(LOGGER, "Wallet seed file created");
|
||||
if use_db {
|
||||
let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr);
|
||||
let _: LMDBBackend<HTTPWalletClient, keychain::ExtKeychain> = LMDBBackend::new(wallet_config.clone(), "", client)
|
||||
.unwrap_or_else(|e| {
|
||||
let _: LMDBBackend<HTTPWalletClient, keychain::ExtKeychain> =
|
||||
LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Error creating DB wallet: {} Config: {:?}",
|
||||
e, wallet_config
|
||||
|
@ -658,7 +660,11 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
|
|||
|
||||
// Handle single-use (command line) owner commands
|
||||
{
|
||||
let wallet = instantiate_wallet(wallet_config.clone(), passphrase, use_db);
|
||||
let wallet = Arc::new(Mutex::new(instantiate_wallet(
|
||||
wallet_config.clone(),
|
||||
passphrase,
|
||||
use_db,
|
||||
)));
|
||||
let _res = wallet::controller::owner_single_use(wallet, |api| {
|
||||
match wallet_args.subcommand() {
|
||||
("send", Some(send_args)) => {
|
||||
|
@ -689,21 +695,20 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
|
|||
dest,
|
||||
max_outputs,
|
||||
selection_strategy == "all",
|
||||
fluff,
|
||||
);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let slate = match result {
|
||||
Ok(s) => {
|
||||
info!(
|
||||
LOGGER,
|
||||
"Tx sent: {} grin to {} (strategy '{}')",
|
||||
"Tx created: {} grin to {} (strategy '{}')",
|
||||
amount_to_hr_string(amount),
|
||||
dest,
|
||||
selection_strategy,
|
||||
);
|
||||
Ok(())
|
||||
s
|
||||
}
|
||||
Err(e) => {
|
||||
error!(LOGGER, "Tx not sent: {:?}", e);
|
||||
error!(LOGGER, "Tx not created: {:?}", e);
|
||||
match e.kind() {
|
||||
// user errors, don't backtrace
|
||||
libwallet::ErrorKind::NotEnoughFunds { .. } => {}
|
||||
|
@ -714,6 +719,20 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
|
|||
error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap());
|
||||
}
|
||||
};
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
let result = api.post_tx(&slate, fluff);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
LOGGER,
|
||||
"Tx sent",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
error!(LOGGER, "Tx not sent: {:?}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ use tokio_core::reactor;
|
|||
|
||||
use api;
|
||||
use error::{Error, ErrorKind};
|
||||
use libwallet;
|
||||
use libtx::slate::Slate;
|
||||
use libwallet;
|
||||
use util::secp::pedersen;
|
||||
use util::{self, LOGGER};
|
||||
|
||||
|
@ -40,7 +40,7 @@ pub struct HTTPWalletClient {
|
|||
|
||||
impl HTTPWalletClient {
|
||||
/// Create a new client that will communicate with the given grin node
|
||||
pub fn new(node_url:&str) -> HTTPWalletClient {
|
||||
pub fn new(node_url: &str) -> HTTPWalletClient {
|
||||
HTTPWalletClient {
|
||||
node_url: node_url.to_owned(),
|
||||
}
|
||||
|
@ -52,9 +52,14 @@ impl WalletClient for HTTPWalletClient {
|
|||
&self.node_url
|
||||
}
|
||||
|
||||
/// Call the wallet API to create a coinbase output for the given block_fees.
|
||||
/// Will retry based on default "retry forever with backoff" behavior.
|
||||
fn create_coinbase(&self, dest: &str, block_fees: &BlockFees) -> Result<CbData, libwallet::Error> {
|
||||
/// Call the wallet API to create a coinbase output for the given
|
||||
/// block_fees. Will retry based on default "retry forever with backoff"
|
||||
/// behavior.
|
||||
fn create_coinbase(
|
||||
&self,
|
||||
dest: &str,
|
||||
block_fees: &BlockFees,
|
||||
) -> Result<CbData, libwallet::Error> {
|
||||
let url = format!("{}/v1/wallet/foreign/build_coinbase", dest);
|
||||
match single_create_coinbase(&url, &block_fees) {
|
||||
Err(e) => {
|
||||
|
@ -64,7 +69,9 @@ impl WalletClient for HTTPWalletClient {
|
|||
);
|
||||
error!(LOGGER, "Underlying Error: {}", e.cause().unwrap());
|
||||
error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap());
|
||||
Err(libwallet::ErrorKind::ClientCallback("Failed to get coinbase"))?
|
||||
Err(libwallet::ErrorKind::ClientCallback(
|
||||
"Failed to get coinbase",
|
||||
))?
|
||||
}
|
||||
Ok(res) => Ok(res),
|
||||
}
|
||||
|
@ -83,35 +90,41 @@ impl WalletClient for HTTPWalletClient {
|
|||
let url = format!("{}/v1/wallet/foreign/receive_tx", dest);
|
||||
debug!(LOGGER, "Posting transaction slate to {}", url);
|
||||
|
||||
let mut core = reactor::Core::new()
|
||||
.context(libwallet::ErrorKind::ClientCallback("Sending transaction: Initialise API"))?;
|
||||
let mut core = reactor::Core::new().context(libwallet::ErrorKind::ClientCallback(
|
||||
"Sending transaction: Initialise API",
|
||||
))?;
|
||||
let client = hyper::Client::new(&core.handle());
|
||||
|
||||
let url_pool = url.to_owned();
|
||||
|
||||
let mut req = Request::new(
|
||||
Method::Post,
|
||||
url_pool.parse::<hyper::Uri>()
|
||||
.context(libwallet::ErrorKind::ClientCallback("Sending transaction: parsing URL"))?
|
||||
url_pool
|
||||
.parse::<hyper::Uri>()
|
||||
.context(libwallet::ErrorKind::ClientCallback(
|
||||
"Sending transaction: parsing URL",
|
||||
))?,
|
||||
);
|
||||
req.headers_mut().set(ContentType::json());
|
||||
let json = serde_json::to_string(&slate)
|
||||
.context(libwallet::ErrorKind::ClientCallback("Sending transaction: parsing response"))?;
|
||||
let json = serde_json::to_string(&slate).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Sending transaction: parsing response",
|
||||
))?;
|
||||
req.set_body(json);
|
||||
|
||||
let work = client.request(req).and_then(|res| {
|
||||
res.body().concat2().and_then(move |body| {
|
||||
let slate: Slate =
|
||||
serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
let slate: Slate = serde_json::from_slice(&body)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(slate)
|
||||
})
|
||||
});
|
||||
let res = core.run(work)
|
||||
.context(libwallet::ErrorKind::ClientCallback("Sending transaction: posting request"))?;
|
||||
.context(libwallet::ErrorKind::ClientCallback(
|
||||
"Sending transaction: posting request",
|
||||
))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
|
||||
/// Posts a transaction to a grin node
|
||||
fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> {
|
||||
let url;
|
||||
|
@ -121,8 +134,9 @@ impl WalletClient for HTTPWalletClient {
|
|||
} else {
|
||||
url = format!("{}/v1/pool/push", dest);
|
||||
}
|
||||
api::client::post(url.as_str(), tx)
|
||||
.context(libwallet::ErrorKind::ClientCallback("Posting transaction to node"))?;
|
||||
api::client::post(url.as_str(), tx).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Posting transaction to node",
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -130,8 +144,9 @@ impl WalletClient for HTTPWalletClient {
|
|||
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())
|
||||
.context(libwallet::ErrorKind::ClientCallback("Getting chain height from node"))?;
|
||||
let res = api::client::get::<api::Tip>(url.as_str()).context(
|
||||
libwallet::ErrorKind::ClientCallback("Getting chain height from node"),
|
||||
)?;
|
||||
Ok(res.height)
|
||||
}
|
||||
|
||||
|
@ -205,7 +220,9 @@ impl WalletClient for HTTPWalletClient {
|
|||
LOGGER,
|
||||
"get_outputs_by_pmmr_index: unable to contact API {}. Error: {}", addr, e
|
||||
);
|
||||
Err(libwallet::ErrorKind::ClientCallback("unable to contact api"))?
|
||||
Err(libwallet::ErrorKind::ClientCallback(
|
||||
"unable to contact api",
|
||||
))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,8 +247,9 @@ pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result<CbData, Err
|
|||
|
||||
/// 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 mut core =
|
||||
reactor::Core::new().context(ErrorKind::GenericError("Could not create reactor"))?;
|
||||
let mut core = reactor::Core::new().context(ErrorKind::GenericError(
|
||||
"Could not create reactor".to_owned(),
|
||||
))?;
|
||||
let client = hyper::Client::new(&core.handle());
|
||||
|
||||
let mut req = Request::new(
|
||||
|
@ -253,7 +271,6 @@ fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result<CbData, E
|
|||
});
|
||||
|
||||
let res = core.run(work)
|
||||
.context(ErrorKind::GenericError("Could not run core"))?;
|
||||
.context(ErrorKind::GenericError("Could not run core".to_owned()))?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ pub enum ErrorKind {
|
|||
|
||||
/// Other
|
||||
#[fail(display = "Generic error: {}", _0)]
|
||||
GenericError(&'static str),
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Fail for Error {
|
||||
|
|
|
@ -177,13 +177,12 @@ where
|
|||
self.keychain = Some(keychain.context(libwallet::ErrorKind::CallbackImpl(
|
||||
"Error deriving keychain",
|
||||
))?);
|
||||
// Just blow up password for now after it's been used
|
||||
self.passphrase = String::from("");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close wallet and remove any stored credentials (TBD)
|
||||
fn close(&mut self) -> Result<(), libwallet::Error> {
|
||||
debug!(LOGGER, "Closing wallet keychain");
|
||||
self.keychain = None;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
//! Still experimental, not sure this is the best way to do this
|
||||
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use core::ser;
|
||||
use keychain::Keychain;
|
||||
|
@ -31,27 +32,27 @@ use util::{self, LOGGER};
|
|||
|
||||
/// Wrapper around internal API functions, containing a reference to
|
||||
/// the wallet/keychain that they're acting upon
|
||||
pub struct APIOwner<'a, W: ?Sized, C, K>
|
||||
pub struct APIOwner<W: ?Sized, C, K>
|
||||
where
|
||||
W: 'a + WalletBackend<C, K>,
|
||||
W: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Wallet, contains its keychain (TODO: Split these up into 2 traits
|
||||
/// perhaps)
|
||||
pub wallet: &'a mut Box<W>,
|
||||
pub wallet: Arc<Mutex<Box<W>>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, W: ?Sized, C, K> APIOwner<'a, W, C, K>
|
||||
impl<W: ?Sized, C, K> APIOwner<W, C, K>
|
||||
where
|
||||
W: 'a + WalletBackend<C, K>,
|
||||
W: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Create new API instance
|
||||
pub fn new(wallet_in: &'a mut Box<W>) -> APIOwner<'a, W, C, K> {
|
||||
pub fn new(wallet_in: Arc<Mutex<Box<W>>>) -> Self {
|
||||
APIOwner {
|
||||
wallet: wallet_in,
|
||||
phantom: PhantomData,
|
||||
|
@ -62,18 +63,26 @@ where
|
|||
/// Attempt to update and retrieve outputs
|
||||
/// Return (whether the outputs were validated against a node, OutputData)
|
||||
pub fn retrieve_outputs(
|
||||
&mut self,
|
||||
&self,
|
||||
include_spent: bool,
|
||||
refresh_from_node: bool,
|
||||
) -> Result<(bool, Vec<OutputData>), Error> {
|
||||
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
|
||||
let mut validated = false;
|
||||
if refresh_from_node {
|
||||
validated = self.update_outputs();
|
||||
validated = self.update_outputs(&mut w);
|
||||
}
|
||||
Ok((
|
||||
|
||||
let res = Ok((
|
||||
validated,
|
||||
updater::retrieve_outputs(&mut **self.wallet, include_spent)?,
|
||||
))
|
||||
updater::retrieve_outputs(&mut **w, include_spent)?,
|
||||
));
|
||||
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Retrieve summary info for wallet
|
||||
|
@ -81,12 +90,19 @@ where
|
|||
&mut self,
|
||||
refresh_from_node: bool,
|
||||
) -> Result<(bool, WalletInfo), Error> {
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
|
||||
let mut validated = false;
|
||||
if refresh_from_node {
|
||||
validated = self.update_outputs();
|
||||
validated = self.update_outputs(&mut w);
|
||||
}
|
||||
let wallet_info = updater::retrieve_info(&mut **self.wallet)?;
|
||||
Ok((validated, wallet_info))
|
||||
|
||||
let wallet_info = updater::retrieve_info(&mut **w)?;
|
||||
let res = Ok((validated, wallet_info));
|
||||
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Issues a send transaction and sends to recipient
|
||||
|
@ -97,17 +113,25 @@ where
|
|||
dest: &str,
|
||||
max_outputs: usize,
|
||||
selection_strategy_is_use_all: bool,
|
||||
fluff: bool,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<Slate, Error> {
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
|
||||
let client;
|
||||
let mut slate_out: Slate;
|
||||
let lock_fn_out;
|
||||
|
||||
client = w.client().clone();
|
||||
let (slate, context, lock_fn) = tx::create_send_tx(
|
||||
&mut **self.wallet,
|
||||
&mut **w,
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
selection_strategy_is_use_all,
|
||||
)?;
|
||||
|
||||
let mut slate = match self.wallet.client().send_tx_slate(dest, &slate) {
|
||||
lock_fn_out = lock_fn;
|
||||
slate_out = match w.client().send_tx_slate(dest, &slate) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!(
|
||||
|
@ -119,15 +143,12 @@ where
|
|||
}
|
||||
};
|
||||
|
||||
tx::complete_tx(&mut **self.wallet, &mut slate, &context)?;
|
||||
tx::complete_tx(&mut **w, &mut slate_out, &context)?;
|
||||
|
||||
// All good here, so let's post it
|
||||
let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap());
|
||||
self.wallet.client().post_tx(&TxWrapper { tx_hex: tx_hex }, fluff)?;
|
||||
|
||||
// All good here, lock our inputs
|
||||
lock_fn(self.wallet)?;
|
||||
Ok(())
|
||||
// lock our inputs
|
||||
lock_fn_out(&mut **w)?;
|
||||
w.close()?;
|
||||
Ok(slate_out)
|
||||
}
|
||||
|
||||
/// Issue a burn TX
|
||||
|
@ -137,40 +158,60 @@ where
|
|||
minimum_confirmations: u64,
|
||||
max_outputs: usize,
|
||||
) -> Result<(), Error> {
|
||||
let tx_burn = tx::issue_burn_tx(
|
||||
&mut **self.wallet,
|
||||
amount,
|
||||
minimum_confirmations,
|
||||
max_outputs,
|
||||
)?;
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
let tx_burn = tx::issue_burn_tx(&mut **w, amount, minimum_confirmations, max_outputs)?;
|
||||
let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap());
|
||||
self.wallet.client().post_tx(&TxWrapper { tx_hex: tx_hex }, false)?;
|
||||
w.client().post_tx(&TxWrapper { tx_hex: tx_hex }, false)?;
|
||||
w.close()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Posts a transaction to the chain
|
||||
pub fn post_tx(&self, slate: &Slate, fluff: bool) -> Result<(), Error> {
|
||||
let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap());
|
||||
let client = {
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.client().clone()
|
||||
};
|
||||
client.post_tx(&TxWrapper { tx_hex: tx_hex }, fluff)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to restore contents of wallet
|
||||
pub fn restore(&mut self) -> Result<(), Error> {
|
||||
self.wallet.restore()
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
let res = w.restore();
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Retrieve current height from node
|
||||
pub fn node_height(&mut self) -> Result<(u64, bool), Error> {
|
||||
match self.wallet.client().get_chain_height() {
|
||||
Ok(height) => Ok((height, true)),
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
let res = w.client().get_chain_height();
|
||||
match res {
|
||||
Ok(height) => {
|
||||
w.close()?;
|
||||
Ok((height, true))
|
||||
},
|
||||
Err(_) => {
|
||||
let outputs = self.retrieve_outputs(true, false)?;
|
||||
let height = match outputs.1.iter().map(|out| out.height).max() {
|
||||
Some(height) => height,
|
||||
None => 0,
|
||||
};
|
||||
w.close()?;
|
||||
Ok((height, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to update outputs in wallet, return whether it was successful
|
||||
fn update_outputs(&mut self) -> bool {
|
||||
match updater::refresh_outputs(&mut **self.wallet) {
|
||||
fn update_outputs(&self, w: &mut W ) -> bool {
|
||||
match updater::refresh_outputs(&mut *w) {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
|
@ -179,41 +220,50 @@ where
|
|||
|
||||
/// Wrapper around external API functions, intended to communicate
|
||||
/// with other parties
|
||||
pub struct APIForeign<'a, W: ?Sized, C, K>
|
||||
pub struct APIForeign<W: ?Sized, C, K>
|
||||
where
|
||||
W: 'a + WalletBackend<C, K>,
|
||||
W: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Wallet, contains its keychain (TODO: Split these up into 2 traits
|
||||
/// perhaps)
|
||||
pub wallet: &'a mut Box<W>,
|
||||
pub wallet: Arc<Mutex<Box<W>>>,
|
||||
phantom: PhantomData<K>,
|
||||
phantom_c: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<'a, W: ?Sized, C, K> APIForeign<'a, W, C, K>
|
||||
impl<'a, W: ?Sized, C, K> APIForeign<W, C, K>
|
||||
where
|
||||
W: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
/// Create new API instance
|
||||
pub fn new(wallet_in: &'a mut Box<W>) -> APIForeign<W, C, K> {
|
||||
APIForeign {
|
||||
pub fn new(wallet_in: Arc<Mutex<Box<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> {
|
||||
updater::build_coinbase(&mut **self.wallet, block_fees)
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
let res = updater::build_coinbase(&mut **w, block_fees);
|
||||
w.close()?;
|
||||
res
|
||||
|
||||
}
|
||||
|
||||
/// Receive a transaction from a sender
|
||||
pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> {
|
||||
tx::receive_tx(&mut **self.wallet, slate)
|
||||
let mut w = self.wallet.lock().unwrap();
|
||||
w.open_with_credentials()?;
|
||||
let res = tx::receive_tx(&mut **w, slate);
|
||||
w.close()?;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use keychain::Keychain;
|
|||
use libtx::slate::Slate;
|
||||
use libwallet::api::{APIForeign, APIOwner};
|
||||
use libwallet::types::{
|
||||
BlockFees, CbData, OutputData, SendTXArgs, WalletBackend, WalletClient, WalletInfo
|
||||
BlockFees, CbData, OutputData, SendTXArgs, WalletBackend, WalletClient, WalletInfo,
|
||||
};
|
||||
use libwallet::{Error, ErrorKind};
|
||||
|
||||
|
@ -40,32 +40,33 @@ use util::LOGGER;
|
|||
|
||||
/// 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: Box<T>, f: F) -> Result<(), Error>
|
||||
pub fn owner_single_use<F, T: ?Sized, C, K>(
|
||||
wallet: Arc<Mutex<Box<T>>>,
|
||||
f: F,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
F: FnOnce(&mut APIOwner<T, C, K>) -> Result<(), Error>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
let mut w = wallet;
|
||||
w.open_with_credentials()?;
|
||||
f(&mut APIOwner::new(&mut w))?;
|
||||
w.close()?;
|
||||
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: &mut Box<T>, f: F) -> Result<(), Error>
|
||||
pub fn foreign_single_use<F, T: ?Sized, C, K>(
|
||||
wallet: Arc<Mutex<Box<T>>>,
|
||||
f: F,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
F: FnOnce(&mut APIForeign<T, C, K>) -> Result<(), Error>,
|
||||
C: WalletClient,
|
||||
K: Keychain,
|
||||
{
|
||||
wallet.open_with_credentials()?;
|
||||
f(&mut APIForeign::new(wallet))?;
|
||||
wallet.close()?;
|
||||
f(&mut APIForeign::new(wallet.clone()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -193,7 +194,11 @@ where
|
|||
api.node_height()
|
||||
}
|
||||
|
||||
fn handle_request(&self, req: &mut Request, api: &mut APIOwner<T, C, K>) -> IronResult<Response> {
|
||||
fn handle_request(
|
||||
&self,
|
||||
req: &mut Request,
|
||||
api: &mut APIOwner<T, C, K>,
|
||||
) -> IronResult<Response> {
|
||||
let url = req.url.clone();
|
||||
let path_elems = url.path();
|
||||
match *path_elems.last().unwrap() {
|
||||
|
@ -218,16 +223,7 @@ where
|
|||
K: Keychain + 'static,
|
||||
{
|
||||
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||
// every request should open with stored credentials,
|
||||
// do its thing and then de-init whatever secrets have been
|
||||
// stored
|
||||
let mut wallet = self.wallet.lock().unwrap();
|
||||
wallet.open_with_credentials().map_err(|e| {
|
||||
error!(LOGGER, "Error opening wallet: {:?}", e);
|
||||
IronError::new(Fail::compat(e), status::BadRequest)
|
||||
})?;
|
||||
let mut w = wallet;
|
||||
let mut api = APIOwner::new(&mut w);
|
||||
let mut api = APIOwner::new(self.wallet.clone());
|
||||
let mut resp_json = self.handle_request(req, &mut api);
|
||||
if !resp_json.is_err() {
|
||||
resp_json
|
||||
|
@ -236,9 +232,6 @@ where
|
|||
.headers
|
||||
.set_raw("access-control-allow-origin", vec![b"*".to_vec()]);
|
||||
}
|
||||
api.wallet
|
||||
.close()
|
||||
.map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?;
|
||||
resp_json
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +264,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn issue_send_tx(&self, req: &mut Request, api: &mut APIOwner<T, C, K>) -> Result<(), Error> {
|
||||
fn issue_send_tx(&self, req: &mut Request, api: &mut APIOwner<T, C, K>) -> Result<Slate, Error> {
|
||||
let struct_body = req.get::<bodyparser::Struct<SendTXArgs>>();
|
||||
match struct_body {
|
||||
Ok(Some(args)) => api.issue_send_tx(
|
||||
|
@ -280,18 +273,17 @@ where
|
|||
&args.dest,
|
||||
args.max_outputs,
|
||||
args.selection_strategy_is_use_all,
|
||||
args.fluff,
|
||||
),
|
||||
Ok(None) => {
|
||||
error!(LOGGER, "Missing request body: issue_send_tx");
|
||||
Err(ErrorKind::GenericError(
|
||||
"Invalid request body: issue_send_tx",
|
||||
"Invalid request body: issue_send_tx".to_owned(),
|
||||
))?
|
||||
}
|
||||
Err(e) => {
|
||||
error!(LOGGER, "Invalid request body: issue_send_tx {:?}", e);
|
||||
Err(ErrorKind::GenericError(
|
||||
"Invalid request body: issue_send_tx",
|
||||
"Invalid request body: issue_send_tx".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
@ -302,14 +294,18 @@ where
|
|||
api.issue_burn_tx(60, 10, 1000)
|
||||
}
|
||||
|
||||
fn handle_request(&self, req: &mut Request, api: &mut APIOwner<T, C, K>) -> Result<String, Error> {
|
||||
fn handle_request(
|
||||
&self,
|
||||
req: &mut Request,
|
||||
api: &mut APIOwner<T, C, K>,
|
||||
) -> Result<String, Error> {
|
||||
let url = req.url.clone();
|
||||
let path_elems = url.path();
|
||||
match *path_elems.last().unwrap() {
|
||||
"issue_send_tx" => json_response_pretty(&self.issue_send_tx(req, api)?),
|
||||
"issue_burn_tx" => json_response_pretty(&self.issue_burn_tx(req, api)?),
|
||||
_ => Err(ErrorKind::GenericError(
|
||||
"Unknown error handling post request",
|
||||
"Unknown error handling post request".to_owned(),
|
||||
))?,
|
||||
}
|
||||
}
|
||||
|
@ -343,18 +339,7 @@ where
|
|||
K: Keychain + 'static,
|
||||
{
|
||||
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||
// every request should open with stored credentials,
|
||||
// do its thing and then de-init whatever secrets have been
|
||||
// stored
|
||||
{
|
||||
let mut wallet = self.wallet.lock().unwrap();
|
||||
wallet.open_with_credentials().map_err(|e| {
|
||||
error!(LOGGER, "Error opening wallet: {:?}", e);
|
||||
IronError::new(Fail::compat(e), status::BadRequest)
|
||||
})?;
|
||||
}
|
||||
let mut wallet = self.wallet.lock().unwrap();
|
||||
let mut api = APIOwner::new(&mut wallet);
|
||||
let mut api = APIOwner::new(self.wallet.clone());
|
||||
let resp = match self.handle_request(req, &mut api) {
|
||||
Ok(r) => self.create_ok_response(&r),
|
||||
Err(e) => {
|
||||
|
@ -362,9 +347,6 @@ where
|
|||
self.create_error_response(e)
|
||||
}
|
||||
};
|
||||
api.wallet
|
||||
.close()
|
||||
.map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?;
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
@ -425,13 +407,13 @@ where
|
|||
Ok(None) => {
|
||||
error!(LOGGER, "Missing request body: build_coinbase");
|
||||
Err(ErrorKind::GenericError(
|
||||
"Invalid request body: build_coinbase",
|
||||
"Invalid request body: build_coinbase".to_owned(),
|
||||
))?
|
||||
}
|
||||
Err(e) => {
|
||||
error!(LOGGER, "Invalid request body: build_coinbase: {:?}", e);
|
||||
Err(ErrorKind::GenericError(
|
||||
"Invalid request body: build_coinbase",
|
||||
"Invalid request body: build_coinbase".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +425,9 @@ where
|
|||
api.receive_tx(&mut slate)?;
|
||||
Ok(slate.clone())
|
||||
} else {
|
||||
Err(ErrorKind::GenericError("Invalid request body: receive_tx"))?
|
||||
Err(ErrorKind::GenericError(
|
||||
"Invalid request body: receive_tx".to_owned(),
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -473,22 +457,8 @@ where
|
|||
K: Keychain + 'static,
|
||||
{
|
||||
fn handle(&self, req: &mut Request) -> IronResult<Response> {
|
||||
// every request should open with stored credentials,
|
||||
// do its thing and then de-init whatever secrets have been
|
||||
// stored
|
||||
{
|
||||
let mut wallet = self.wallet.lock().unwrap();
|
||||
wallet.open_with_credentials().map_err(|e| {
|
||||
error!(LOGGER, "Error opening wallet: {:?}", e);
|
||||
IronError::new(Fail::compat(e), status::BadRequest)
|
||||
})?;
|
||||
}
|
||||
let mut wallet = self.wallet.lock().unwrap();
|
||||
let mut api = APIForeign::new(&mut wallet);
|
||||
let resp_json = self.handle_request(req, &mut api);
|
||||
api.wallet
|
||||
.close()
|
||||
.map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?;
|
||||
let mut api = APIForeign::new(self.wallet.clone());
|
||||
let resp_json = self.handle_request(req, &mut *api);
|
||||
resp_json
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ pub enum ErrorKind {
|
|||
|
||||
/// Other
|
||||
#[fail(display = "Generic error: {}", _0)]
|
||||
GenericError(&'static str),
|
||||
GenericError(String),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
|
|
|
@ -306,7 +306,7 @@ impl BlockIdentifier {
|
|||
|
||||
/// convert to hex string
|
||||
pub fn from_hex(hex: &str) -> Result<BlockIdentifier, Error> {
|
||||
let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex"))?;
|
||||
let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex".to_owned()))?;
|
||||
Ok(BlockIdentifier(hash))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,8 +78,8 @@ impl WalletSeed {
|
|||
}
|
||||
|
||||
fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
|
||||
let bytes =
|
||||
util::from_hex(hex.to_string()).context(ErrorKind::GenericError("Invalid hex"))?;
|
||||
let bytes = util::from_hex(hex.to_string())
|
||||
.context(ErrorKind::GenericError("Invalid hex".to_owned()))?;
|
||||
Ok(WalletSeed::from_bytes(&bytes))
|
||||
}
|
||||
|
||||
|
|
|
@ -12,112 +12,42 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Common functions to facilitate wallet, walletlib and transaction testing
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
extern crate failure;
|
||||
extern crate grin_api as api;
|
||||
extern crate grin_chain as chain;
|
||||
extern crate grin_core as core;
|
||||
extern crate grin_keychain as keychain;
|
||||
extern crate grin_wallet as wallet;
|
||||
extern crate serde_json;
|
||||
extern crate time;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use chain::Chain;
|
||||
use core::core::hash::Hashed;
|
||||
use core::core::{Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel};
|
||||
use core::{consensus, global, pow};
|
||||
use keychain::ExtKeychain;
|
||||
use wallet::{HTTPWalletClient, WalletConfig};
|
||||
use core::core::{OutputFeatures, OutputIdentifier, Transaction};
|
||||
use core::{consensus, global, pow, ser};
|
||||
use wallet::file_wallet::FileWallet;
|
||||
use wallet::libwallet::internal::updater;
|
||||
use wallet::libwallet::types::{BlockFees, BlockIdentifier, OutputStatus,
|
||||
WalletBackend, WalletClient};
|
||||
use wallet::libwallet::{Error, ErrorKind};
|
||||
use wallet::libwallet;
|
||||
use wallet::libwallet::types::{BlockFees, CbData, WalletClient, WalletInst};
|
||||
use wallet::lmdb_wallet::LMDBBackend;
|
||||
use wallet::WalletConfig;
|
||||
|
||||
use util;
|
||||
use util::secp::pedersen;
|
||||
|
||||
/// Mostly for testing, refreshes output state against a local chain instance
|
||||
/// instead of via an http API call
|
||||
pub fn refresh_output_state_local<T, C, K>(wallet: &mut T, chain: &chain::Chain) -> Result<(), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
let wallet_outputs = updater::map_wallet_outputs(wallet)?;
|
||||
let chain_outputs: Vec<Option<api::Output>> = wallet_outputs
|
||||
.keys()
|
||||
.map(|k| match get_output_local(chain, &k) {
|
||||
Err(_) => None,
|
||||
Ok(k) => Some(k),
|
||||
})
|
||||
.collect();
|
||||
let mut api_outputs: HashMap<pedersen::Commitment, String> = HashMap::new();
|
||||
for out in chain_outputs {
|
||||
match out {
|
||||
Some(o) => {
|
||||
api_outputs.insert(o.commit.commit(), util::to_hex(o.commit.to_vec()));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
let height = chain.head().unwrap().height;
|
||||
updater::apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height)?;
|
||||
Ok(())
|
||||
}
|
||||
pub mod testclient;
|
||||
|
||||
/// Return the spendable wallet balance from the local chain
|
||||
/// (0:total, 1:amount_awaiting_confirmation, 2:confirmed but locked,
|
||||
/// 3:currently_spendable, 4:locked total) TODO: Should be a wallet lib
|
||||
/// function with nicer return values
|
||||
pub fn get_wallet_balances<T, C, K>(
|
||||
wallet: &mut T,
|
||||
height: u64,
|
||||
) -> Result<(u64, u64, u64, u64, u64), Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
let mut unspent_total = 0;
|
||||
let mut unspent_but_locked_total = 0;
|
||||
let mut unconfirmed_total = 0;
|
||||
let mut locked_total = 0;
|
||||
let keychain = wallet.keychain().clone();
|
||||
for out in wallet
|
||||
.iter()
|
||||
.filter(|out| out.root_key_id == keychain.root_key_id())
|
||||
{
|
||||
if out.status == OutputStatus::Unspent {
|
||||
unspent_total += out.value;
|
||||
if out.lock_height > height {
|
||||
unspent_but_locked_total += out.value;
|
||||
}
|
||||
}
|
||||
if out.status == OutputStatus::Unconfirmed && !out.is_coinbase {
|
||||
unconfirmed_total += out.value;
|
||||
}
|
||||
if out.status == OutputStatus::Locked {
|
||||
locked_total += out.value;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
unspent_total + unconfirmed_total, //total
|
||||
unconfirmed_total, //amount_awaiting_confirmation
|
||||
unspent_but_locked_total, // confirmed but locked
|
||||
unspent_total - unspent_but_locked_total, // currently spendable
|
||||
locked_total, // locked total
|
||||
))
|
||||
/// 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,
|
||||
) -> Result<api::Output, Error> {
|
||||
fn get_output_local(chain: &chain::Chain, commit: &pedersen::Commitment) -> Option<api::Output> {
|
||||
let outputs = [
|
||||
OutputIdentifier::new(OutputFeatures::DEFAULT_OUTPUT, commit),
|
||||
OutputIdentifier::new(OutputFeatures::COINBASE_OUTPUT, commit),
|
||||
|
@ -125,23 +55,25 @@ fn get_output_local(
|
|||
|
||||
for x in outputs.iter() {
|
||||
if let Ok(_) = chain.is_unspent(&x) {
|
||||
return Ok(api::Output::new(&commit));
|
||||
return Some(api::Output::new(&commit));
|
||||
}
|
||||
}
|
||||
Err(ErrorKind::GenericError(
|
||||
"Can't get output from local instance of chain",
|
||||
))?
|
||||
None
|
||||
}
|
||||
|
||||
/// 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: (Output, TxKernel)) {
|
||||
pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: CbData) {
|
||||
let prev = chain.head_header().unwrap();
|
||||
let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap();
|
||||
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(),
|
||||
difficulty.clone(),
|
||||
reward,
|
||||
(output, kernel),
|
||||
).unwrap();
|
||||
b.header.timestamp = prev.timestamp + time::Duration::seconds(60);
|
||||
chain.set_txhashset_roots(&mut b, false).unwrap();
|
||||
|
@ -158,57 +90,82 @@ pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: (Out
|
|||
/// 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<T, C, K>(chain: &Chain, txs: Vec<&Transaction>, wallet: &mut T)
|
||||
pub fn award_block_to_wallet<C, K>(
|
||||
chain: &Chain,
|
||||
txs: Vec<&Transaction>,
|
||||
wallet: Arc<Mutex<Box<WalletInst<C, K>>>>,
|
||||
) -> Result<(), libwallet::Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
// build block fees
|
||||
let prev = chain.head_header().unwrap();
|
||||
let fee_amt = txs.iter().map(|tx| tx.fee()).sum();
|
||||
let fees = BlockFees {
|
||||
let block_fees = BlockFees {
|
||||
fees: fee_amt,
|
||||
key_id: None,
|
||||
height: prev.height + 1,
|
||||
};
|
||||
let coinbase_tx = wallet::libwallet::internal::updater::receive_coinbase(wallet, &fees);
|
||||
let (coinbase_tx, fees) = match coinbase_tx {
|
||||
Ok(t) => ((t.0, t.1), t.2),
|
||||
Err(e) => {
|
||||
panic!("Unable to create block reward: {:?}", e);
|
||||
}
|
||||
};
|
||||
// build coinbase (via api) and add block
|
||||
libwallet::controller::foreign_single_use(wallet.clone(), |api| {
|
||||
let coinbase_tx = api.build_coinbase(&block_fees)?;
|
||||
add_block_with_reward(chain, txs, coinbase_tx.clone());
|
||||
let output = wallet.get(&fees.key_id.unwrap()).unwrap();
|
||||
let mut batch = wallet.batch().unwrap();
|
||||
batch.save(output).unwrap();
|
||||
batch.commit().unwrap();
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// adds many block rewards to a wallet, no transactions
|
||||
pub fn award_blocks_to_wallet<T, C, K>(chain: &Chain, wallet: &mut T, num_rewards: usize)
|
||||
/// Award a blocks to a wallet directly
|
||||
pub fn award_blocks_to_wallet<C, K>(
|
||||
chain: &Chain,
|
||||
wallet: Arc<Mutex<Box<WalletInst<C, K>>>>,
|
||||
number: usize,
|
||||
) -> Result<(), libwallet::Error>
|
||||
where
|
||||
T: WalletBackend<C, K>,
|
||||
C: WalletClient,
|
||||
K: keychain::Keychain,
|
||||
{
|
||||
for _ in 0..num_rewards {
|
||||
award_block_to_wallet(chain, vec![], wallet);
|
||||
for _ in 0..number {
|
||||
award_block_to_wallet(chain, vec![], wallet.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a new wallet in a particular directory
|
||||
pub fn create_wallet(dir: &str, client: HTTPWalletClient) -> FileWallet<HTTPWalletClient, ExtKeychain> {
|
||||
/// dispatch a wallet (extend later to optionally dispatch a db wallet)
|
||||
pub fn create_wallet<C, K>(
|
||||
dir: &str,
|
||||
client: C,
|
||||
backend_type: BackendType,
|
||||
) -> Arc<Mutex<Box<WalletInst<C, K>>>>
|
||||
where
|
||||
C: WalletClient + 'static,
|
||||
K: keychain::Keychain + 'static,
|
||||
{
|
||||
let mut wallet_config = WalletConfig::default();
|
||||
wallet_config.data_file_dir = String::from(dir);
|
||||
wallet::WalletSeed::init_file(&wallet_config).expect("Failed to create wallet seed file.");
|
||||
let mut wallet = FileWallet::new(wallet_config.clone(), "", client)
|
||||
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config));
|
||||
let _ = wallet::WalletSeed::init_file(&wallet_config);
|
||||
let mut wallet: Box<WalletInst<C, K>> = match backend_type {
|
||||
BackendType::FileBackend => {
|
||||
let mut wallet: FileWallet<C, K> = FileWallet::new(wallet_config.clone(), "", client)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)
|
||||
});
|
||||
Box::new(wallet)
|
||||
}
|
||||
BackendType::LMDBBackend => {
|
||||
let mut wallet: LMDBBackend<C, K> = LMDBBackend::new(wallet_config.clone(), "", client)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)
|
||||
});
|
||||
Box::new(wallet)
|
||||
}
|
||||
};
|
||||
wallet.open_with_credentials().unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Error initializing wallet: {:?} Config: {:?}",
|
||||
e, wallet_config
|
||||
)
|
||||
});
|
||||
wallet
|
||||
Arc::new(Mutex::new(wallet))
|
||||
}
|
||||
|
|
420
wallet/tests/common/testclient.rs
Normal file
420
wallet/tests/common/testclient.rs
Normal file
|
@ -0,0 +1,420 @@
|
|||
// 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 std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::api;
|
||||
use common::serde_json;
|
||||
use store;
|
||||
use util::secp::pedersen::Commitment;
|
||||
use util::{self, LOGGER};
|
||||
|
||||
use common::failure::ResultExt;
|
||||
|
||||
use chain::types::NoopAdapter;
|
||||
use chain::Chain;
|
||||
use core::core::Transaction;
|
||||
use core::global::{set_mining_mode, ChainTypes};
|
||||
use core::{pow, ser};
|
||||
use keychain::Keychain;
|
||||
|
||||
use util::secp::pedersen;
|
||||
use wallet::libtx::slate::Slate;
|
||||
use wallet::libwallet;
|
||||
use wallet::libwallet::types::*;
|
||||
|
||||
use common;
|
||||
|
||||
/// 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: WalletClient,
|
||||
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<Box<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: WalletClient,
|
||||
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 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,
|
||||
).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<Box<WalletInst<LocalWalletClient, K>>>>,
|
||||
) {
|
||||
self.wallets.insert(addr.to_owned(), (tx, wallet));
|
||||
}
|
||||
|
||||
/// 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!(LOGGER, "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)?,
|
||||
"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"),
|
||||
)?;
|
||||
|
||||
let tx_bin = util::from_hex(wrapper.tx_hex).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Error parsing TxWrapper: tx_bin",
|
||||
))?;
|
||||
|
||||
let tx: Transaction = ser::deserialize(&mut &tx_bin[..]).context(
|
||||
libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx"),
|
||||
)?;
|
||||
|
||||
common::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();
|
||||
libwallet::controller::foreign_single_use(w.clone(), |listener_api| {
|
||||
listener_api.receive_tx(&mut slate)?;
|
||||
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 c = util::from_hex(String::from(o)).unwrap();
|
||||
let commit = Commitment::from_vec(c);
|
||||
let out = common::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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalWalletClient {
|
||||
/// wallet identifier for the proxy queue
|
||||
pub id: String,
|
||||
/// proxy's tx queue (recieve messsages 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().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletClient for LocalWalletClient {
|
||||
fn node_url(&self) -> &str {
|
||||
"node"
|
||||
}
|
||||
|
||||
/// Call the wallet API to create a coinbase output for the given
|
||||
/// block_fees. Will retry based on default "retry forever with backoff"
|
||||
/// behavior.
|
||||
fn create_coinbase(
|
||||
&self,
|
||||
_dest: &str,
|
||||
_block_fees: &BlockFees,
|
||||
) -> Result<CbData, libwallet::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Send the slate to a listening wallet instance
|
||||
fn send_tx_slate(&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().unwrap();
|
||||
p.send(m)
|
||||
.context(libwallet::ErrorKind::ClientCallback("Send TX Slate"))?;
|
||||
}
|
||||
let r = self.rx.lock().unwrap();
|
||||
let m = r.recv().unwrap();
|
||||
trace!(LOGGER, "Received send_tx_slate response: {:?}", m.clone());
|
||||
Ok(
|
||||
serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Parsing send_tx_slate response",
|
||||
))?,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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().unwrap();
|
||||
p.send(m)
|
||||
.context(libwallet::ErrorKind::ClientCallback("post_tx send"))?;
|
||||
}
|
||||
let r = self.rx.lock().unwrap();
|
||||
let m = r.recv().unwrap();
|
||||
trace!(LOGGER, "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().unwrap();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Get chain height send",
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock().unwrap();
|
||||
let m = r.recv().unwrap();
|
||||
trace!(
|
||||
LOGGER,
|
||||
"Received get_chain_height response: {:?}",
|
||||
m.clone()
|
||||
);
|
||||
Ok(m.body
|
||||
.parse::<u64>()
|
||||
.context(libwallet::ErrorKind::ClientCallback(
|
||||
"Parsing get_height response",
|
||||
))?)
|
||||
}
|
||||
|
||||
/// Retrieve outputs from node
|
||||
fn get_outputs_from_node(
|
||||
&self,
|
||||
wallet_outputs: Vec<pedersen::Commitment>,
|
||||
) -> Result<HashMap<pedersen::Commitment, String>, 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().unwrap();
|
||||
p.send(m).context(libwallet::ErrorKind::ClientCallback(
|
||||
"Get outputs from node send",
|
||||
))?;
|
||||
}
|
||||
let r = self.rx.lock().unwrap();
|
||||
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> = HashMap::new();
|
||||
for out in outputs {
|
||||
api_outputs.insert(out.commit.commit(), util::to_hex(out.commit.to_vec()));
|
||||
}
|
||||
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)>,
|
||||
),
|
||||
libwallet::Error,
|
||||
> {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
|
@ -26,186 +26,192 @@ extern crate time;
|
|||
extern crate uuid;
|
||||
|
||||
mod common;
|
||||
use common::testclient::{LocalWalletClient, WalletProxy};
|
||||
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use chain::Chain;
|
||||
use chain::types::NoopAdapter;
|
||||
use core::global;
|
||||
use core::global::ChainTypes;
|
||||
use core::{global, pow};
|
||||
use keychain::ExtKeychain;
|
||||
use util::LOGGER;
|
||||
use wallet::HTTPWalletClient;
|
||||
use wallet::libwallet::internal::selection;
|
||||
|
||||
fn clean_output_dir(test_dir: &str) {
|
||||
let _ = fs::remove_dir_all(test_dir);
|
||||
}
|
||||
|
||||
fn setup(test_dir: &str, chain_dir: &str) -> Chain {
|
||||
fn setup(test_dir: &str) {
|
||||
util::init_test_logger();
|
||||
clean_output_dir(test_dir);
|
||||
global::set_mining_mode(ChainTypes::AutomatedTesting);
|
||||
let genesis_block = pow::mine_genesis_block().unwrap();
|
||||
let dir_name = format!("{}/{}", test_dir, chain_dir);
|
||||
let db_env = Arc::new(store::new_env(dir_name.to_string()));
|
||||
chain::Chain::init(
|
||||
dir_name.to_string(),
|
||||
db_env,
|
||||
Arc::new(NoopAdapter {}),
|
||||
genesis_block,
|
||||
pow::verify_size,
|
||||
).unwrap()
|
||||
}
|
||||
|
||||
/// Build and test new version of sending API
|
||||
#[test]
|
||||
fn build_transaction() {
|
||||
let client = HTTPWalletClient::new("");
|
||||
let chain = setup("test_output", "build_transaction_2/.grin");
|
||||
let mut wallet1 = common::create_wallet("test_output/build_transaction_2/wallet1", client.clone());
|
||||
let mut wallet2 = common::create_wallet("test_output/build_transaction_2/wallet2", client);
|
||||
common::award_blocks_to_wallet(&chain, &mut wallet1, 10);
|
||||
// Wallet 1 has 600 Grins, wallet 2 has 0. Create a transaction that sends
|
||||
// 300 Grins from wallet 1 to wallet 2, using libtx
|
||||
/// Exercises the Transaction API fully with a test WalletClient operating
|
||||
/// directly on a chain instance
|
||||
/// Callable with any type of wallet
|
||||
fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) {
|
||||
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();
|
||||
|
||||
// Get lock height
|
||||
let chain_tip = chain.head().unwrap();
|
||||
let amount = 300_000_000_000;
|
||||
let min_confirmations = 3;
|
||||
// Create a new wallet test client, and set its queues to communicate with the
|
||||
// proxy
|
||||
let client = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone());
|
||||
let wallet1 = common::create_wallet(
|
||||
&format!("{}/wallet1", test_dir),
|
||||
client.clone(),
|
||||
backend_type.clone(),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet1", client.get_send_instance(), wallet1.clone());
|
||||
|
||||
// ensure outputs we're selecting are up to date
|
||||
let res = common::refresh_output_state_local(&mut wallet1, &chain);
|
||||
// define recipient wallet, add to proxy
|
||||
let client = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone());
|
||||
let wallet2 = common::create_wallet(
|
||||
&format!("{}/wallet2", test_dir),
|
||||
client.clone(),
|
||||
backend_type.clone(),
|
||||
);
|
||||
wallet_proxy.add_wallet("wallet2", client.get_send_instance(), wallet2.clone());
|
||||
|
||||
if let Err(e) = res {
|
||||
panic!("Unable to refresh sender wallet outputs: {}", e);
|
||||
// Set the wallet proxy listener running
|
||||
thread::spawn(move || {
|
||||
if let Err(e) = wallet_proxy.run() {
|
||||
error!(LOGGER, "Wallet Proxy error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// TRANSACTION WORKFLOW STARTS HERE
|
||||
// Sender selects outputs into a new slate and save our corresponding IDs 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 slate, mut sender_context, sender_lock_fn) = selection::build_send_tx_slate(
|
||||
&mut wallet1,
|
||||
2,
|
||||
amount,
|
||||
chain_tip.height,
|
||||
min_confirmations,
|
||||
chain_tip.height,
|
||||
1000,
|
||||
true,
|
||||
).unwrap();
|
||||
|
||||
// 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(
|
||||
wallet1.keychain.as_ref().unwrap(),
|
||||
&mut sender_context.sec_key,
|
||||
&sender_context.sec_nonce,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
debug!(LOGGER, "Transaction Slate after step 1: sender initiation");
|
||||
debug!(LOGGER, "-----------------------------------------");
|
||||
debug!(LOGGER, "{:?}", slate);
|
||||
|
||||
// Now, just like the sender did, recipient is going to select a target output,
|
||||
// add it to the transaction, and keep track of the corresponding wallet
|
||||
// Identifier Again, this is a helper to do that, which returns a closure that
|
||||
// creates the output when we're satisfied the process was successful
|
||||
let (_, mut recp_context, receiver_create_fn) =
|
||||
selection::build_recipient_output_with_slate(&mut wallet2, &mut slate).unwrap();
|
||||
|
||||
let _ = slate
|
||||
.fill_round_1(
|
||||
wallet2.keychain.as_ref().unwrap(),
|
||||
&mut recp_context.sec_key,
|
||||
&recp_context.sec_nonce,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// recipient can proceed to round 2 now
|
||||
let _ = receiver_create_fn(&mut wallet2);
|
||||
|
||||
let _ = slate
|
||||
.fill_round_2(
|
||||
wallet1.keychain.as_ref().unwrap(),
|
||||
&recp_context.sec_key,
|
||||
&recp_context.sec_nonce,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
// few values to keep things shorter
|
||||
let reward = core::consensus::REWARD;
|
||||
let cm = global::coinbase_maturity();
|
||||
// mine a few blocks
|
||||
let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 10);
|
||||
|
||||
// Check wallet 1 contents are as expected
|
||||
let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?;
|
||||
debug!(
|
||||
LOGGER,
|
||||
"Transaction Slate after step 2: receiver initiation"
|
||||
"Wallet 1 Info Pre-Transaction, after {} blocks: {:?}",
|
||||
wallet1_info.last_confirmed_height,
|
||||
wallet1_info
|
||||
);
|
||||
debug!(LOGGER, "-----------------------------------------");
|
||||
debug!(LOGGER, "{:?}", slate);
|
||||
|
||||
// SENDER Part 3: Sender confirmation
|
||||
let _ = slate
|
||||
.fill_round_2(
|
||||
wallet1.keychain.as_ref().unwrap(),
|
||||
&sender_context.sec_key,
|
||||
&sender_context.sec_nonce,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
debug!(LOGGER, "PartialTx after step 3: sender confirmation");
|
||||
debug!(LOGGER, "--------------------------------------------");
|
||||
debug!(LOGGER, "{:?}", slate);
|
||||
|
||||
// Final transaction can be built by anyone at this stage
|
||||
let res = slate.finalize(wallet1.keychain.as_ref().unwrap());
|
||||
|
||||
if let Err(e) = res {
|
||||
panic!("Error creating final tx: {:?}", e);
|
||||
}
|
||||
|
||||
debug!(LOGGER, "Final transaction is:");
|
||||
debug!(LOGGER, "--------------------------------------------");
|
||||
debug!(LOGGER, "{:?}", slate.tx);
|
||||
|
||||
// All okay, lock sender's outputs
|
||||
let _ = sender_lock_fn(&mut wallet1);
|
||||
|
||||
// Insert this transaction into a new block, then mine till confirmation
|
||||
common::award_block_to_wallet(&chain, vec![&slate.tx], &mut wallet1);
|
||||
common::award_blocks_to_wallet(&chain, &mut wallet1, 5);
|
||||
|
||||
// Refresh wallets
|
||||
let res = common::refresh_output_state_local(&mut wallet2, &chain);
|
||||
if let Err(e) = res {
|
||||
panic!("Error refreshing output state for wallet: {:?}", e);
|
||||
}
|
||||
|
||||
// check recipient wallet
|
||||
let chain_tip = chain.head().unwrap();
|
||||
let balances = common::get_wallet_balances(&mut wallet2, chain_tip.height).unwrap();
|
||||
|
||||
assert_eq!(balances.3, 300_000_000_000);
|
||||
|
||||
// check sender wallet
|
||||
let res = common::refresh_output_state_local(&mut wallet1, &chain);
|
||||
if let Err(e) = res {
|
||||
panic!("Error refreshing output state for wallet: {:?}", e);
|
||||
}
|
||||
let balances = common::get_wallet_balances(&mut wallet1, chain_tip.height).unwrap();
|
||||
println!("tip height: {:?}", chain_tip.height);
|
||||
println!("Sender balances: {:?}", balances);
|
||||
// num blocks * grins per block, and wallet1 mined the fee
|
||||
assert!(wallet1_refreshed);
|
||||
assert_eq!(
|
||||
balances.3,
|
||||
(chain_tip.height - min_confirmations) * 60_000_000_000 - amount
|
||||
wallet1_info.amount_currently_spendable,
|
||||
(wallet1_info.last_confirmed_height - cm) * reward
|
||||
);
|
||||
assert_eq!(wallet1_info.amount_immature, cm * reward);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = sender_res {
|
||||
println!("Error starting sender API: {}", e);
|
||||
}
|
||||
|
||||
// assert wallet contents
|
||||
// and a single use api for a send command
|
||||
let amount = 60_000_000_000;
|
||||
let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
|
||||
// note this will increment the block count as part of the transaction "Posting"
|
||||
let issue_tx_res = sender_api.issue_send_tx(
|
||||
amount, // amount
|
||||
2, // minimum confirmations
|
||||
"wallet2", // dest
|
||||
500, // max outputs
|
||||
true, // select all outputs
|
||||
);
|
||||
if issue_tx_res.is_err() {
|
||||
panic!("Error issuing send tx: {}", issue_tx_res.err().unwrap());
|
||||
}
|
||||
let post_res = sender_api.post_tx(&issue_tx_res.unwrap(), false);
|
||||
if post_res.is_err() {
|
||||
panic!("Error posting tx: {}", post_res.err().unwrap());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = sender_res {
|
||||
panic!("Error starting sender API: {}", e);
|
||||
}
|
||||
|
||||
// Check wallet 1 contents are as expected
|
||||
let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?;
|
||||
debug!(
|
||||
LOGGER,
|
||||
"Wallet 1 Info Post Transaction, after {} blocks: {:?}",
|
||||
wallet1_info.last_confirmed_height,
|
||||
wallet1_info
|
||||
);
|
||||
let fee = wallet::libtx::tx_fee(
|
||||
wallet1_info.last_confirmed_height as usize - 1 - cm as usize,
|
||||
2,
|
||||
None,
|
||||
);
|
||||
assert!(wallet1_refreshed);
|
||||
// wallet 1 recieved 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);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = sender_res {
|
||||
println!("Error starting sender API: {}", e);
|
||||
}
|
||||
|
||||
// mine a few more blocks
|
||||
let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3);
|
||||
|
||||
// refresh wallets and retrieve info/tests for each wallet after maturity
|
||||
let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| {
|
||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?;
|
||||
debug!(LOGGER, "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(())
|
||||
});
|
||||
if let Err(e) = sender_res {
|
||||
println!("Error starting sender API: {}", e);
|
||||
}
|
||||
|
||||
let sender_res = wallet::controller::owner_single_use(wallet2.clone(), |api| {
|
||||
let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true)?;
|
||||
assert!(wallet2_refreshed);
|
||||
assert_eq!(wallet2_info.amount_currently_spendable, amount);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = sender_res {
|
||||
println!("Error starting sender API: {}", e);
|
||||
}
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_wallet_basic_transaction_api() {
|
||||
let test_dir = "test_output/basic_transaction_api_file";
|
||||
basic_transaction_api(test_dir, common::BackendType::FileBackend);
|
||||
}
|
||||
|
||||
// not yet ready
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn db_wallet_basic_transaction_api() {
|
||||
let test_dir = "test_output/basic_transaction_api_db";
|
||||
basic_transaction_api(test_dir, common::BackendType::LMDBBackend);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue