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:
Yeastplume
2018-07-12 16:49:37 +01:00
committed by GitHub
parent 25ca2de487
commit bacadfb5ab
13 changed files with 917 additions and 475 deletions
+42 -25
View File
@@ -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)
}
+1 -1
View File
@@ -89,7 +89,7 @@ pub enum ErrorKind {
/// Other
#[fail(display = "Generic error: {}", _0)]
GenericError(&'static str),
GenericError(String),
}
impl Fail for Error {
+1 -2
View File
@@ -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(())
}
+101 -51
View File
@@ -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,37 +113,42 @@ 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!(
LOGGER,
"Communication with receiver failed on SenderInitiation send. Aborting transaction {:?}",
e,
);
LOGGER,
"Communication with receiver failed on SenderInitiation send. Aborting transaction {:?}",
e,
);
return Err(e)?;
}
};
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
}
}
+34 -64
View File
@@ -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
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ pub enum ErrorKind {
/// Other
#[fail(display = "Generic error: {}", _0)]
GenericError(&'static str),
GenericError(String),
}
impl Display for Error {
+1 -1
View File
@@ -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))
}
}
+2 -2
View File
@@ -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))
}