From 9ca2f5b3f71aa689de0606bc073453e127862983 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Fri, 14 Sep 2018 02:58:39 -0600 Subject: [PATCH] Move all handling except network/gossip from /bin to wallet module --- src/bin/wallet.rs | 287 ++++++++-------------------------------------- src/lib.rs | 2 + src/wallet.rs | 208 ++++++++++++++++++++++++++++++++- 3 files changed, 255 insertions(+), 242 deletions(-) diff --git a/src/bin/wallet.rs b/src/bin/wallet.rs index aac5e8908f..9c789deb6e 100644 --- a/src/bin/wallet.rs +++ b/src/bin/wallet.rs @@ -1,80 +1,65 @@ -extern crate atty; -extern crate bincode; -extern crate bs58; #[macro_use] extern crate clap; extern crate dirs; -extern crate serde_json; -#[macro_use] extern crate solana; -use clap::{App, Arg, SubCommand}; +use clap::{App, Arg, ArgMatches, SubCommand}; use solana::client::mk_client; use solana::crdt::NodeInfo; use solana::drone::DRONE_PORT; -use solana::fullnode::Config; use solana::logger; -use solana::signature::{read_keypair, Keypair, KeypairUtil, Pubkey, Signature}; -use solana::thin_client::{poll_gossip_for_leader, ThinClient}; -use solana::wallet::request_airdrop; +use solana::signature::{read_keypair, KeypairUtil}; +use solana::thin_client::poll_gossip_for_leader; +use solana::wallet::{parse_command, process_command, read_leader, WalletConfig, WalletError}; use std::error; -use std::fmt; -use std::fs::File; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::thread::sleep; -use std::time::Duration; -enum WalletCommand { - Address, - Balance, - AirDrop(i64), - Pay(i64, Pubkey), - Confirm(Signature), -} - -#[derive(Debug, Clone)] -enum WalletError { - CommandNotRecognized(String), - BadParameter(String), -} - -impl fmt::Display for WalletError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "invalid") - } -} - -impl error::Error for WalletError { - fn description(&self) -> &str { - "invalid" +pub fn parse_args(matches: &ArgMatches) -> Result> { + let leader: NodeInfo; + if let Some(l) = matches.value_of("leader") { + leader = read_leader(l)?.node_info; + } else { + let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000); + leader = NodeInfo::new_with_socketaddr(&server_addr); + }; + let timeout: Option; + if let Some(secs) = matches.value_of("timeout") { + timeout = Some(secs.to_string().parse().expect("integer")); + } else { + timeout = None; } - fn cause(&self) -> Option<&error::Error> { - // Generic error, underlying cause isn't tracked. - None - } + let mut path = dirs::home_dir().expect("home directory"); + let id_path = if matches.is_present("keypair") { + matches.value_of("keypair").unwrap() + } else { + path.extend(&[".config", "solana", "id.json"]); + path.to_str().unwrap() + }; + let id = read_keypair(id_path).or_else(|err| { + Err(WalletError::BadParameter(format!( + "{}: Unable to open keypair file: {}", + err, id_path + ))) + })?; + + let leader = poll_gossip_for_leader(leader.contact_info.ncp, timeout)?; + + let mut drone_addr = leader.contact_info.tpu; + drone_addr.set_port(DRONE_PORT); + + let command = parse_command(id.pubkey(), &matches)?; + + Ok(WalletConfig { + leader, + id, + drone_addr, // TODO: Add an option for this. + command, + }) } -struct WalletConfig { - leader: NodeInfo, - id: Keypair, - drone_addr: SocketAddr, - command: WalletCommand, -} - -impl Default for WalletConfig { - fn default() -> WalletConfig { - let default_addr = socketaddr!(0, 8000); - WalletConfig { - leader: NodeInfo::new_with_socketaddr(&default_addr), - id: Keypair::new(), - drone_addr: default_addr, - command: WalletCommand::Balance, - } - } -} - -fn parse_args() -> Result> { +fn main() -> Result<(), Box> { + logger::setup(); let matches = App::new("solana-wallet") .version(crate_version!()) .arg( @@ -146,185 +131,7 @@ fn parse_args() -> Result> { .subcommand(SubCommand::with_name("address").about("Get your public key")) .get_matches(); - let leader: NodeInfo; - if let Some(l) = matches.value_of("leader") { - leader = read_leader(l)?.node_info; - } else { - let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000); - leader = NodeInfo::new_with_socketaddr(&server_addr); - }; - let timeout: Option; - if let Some(secs) = matches.value_of("timeout") { - timeout = Some(secs.to_string().parse().expect("integer")); - } else { - timeout = None; - } - - let mut path = dirs::home_dir().expect("home directory"); - let id_path = if matches.is_present("keypair") { - matches.value_of("keypair").unwrap() - } else { - path.extend(&[".config", "solana", "id.json"]); - path.to_str().unwrap() - }; - let id = read_keypair(id_path).or_else(|err| { - Err(WalletError::BadParameter(format!( - "{}: Unable to open keypair file: {}", - err, id_path - ))) - })?; - - let leader = poll_gossip_for_leader(leader.contact_info.ncp, timeout)?; - - let mut drone_addr = leader.contact_info.tpu; - drone_addr.set_port(DRONE_PORT); - - let command = match matches.subcommand() { - ("airdrop", Some(airdrop_matches)) => { - let tokens = airdrop_matches.value_of("tokens").unwrap().parse()?; - Ok(WalletCommand::AirDrop(tokens)) - } - ("pay", Some(pay_matches)) => { - let to = if pay_matches.is_present("to") { - let pubkey_vec = bs58::decode(pay_matches.value_of("to").unwrap()) - .into_vec() - .expect("base58-encoded public key"); - - if pubkey_vec.len() != std::mem::size_of::() { - eprintln!("{}", pay_matches.usage()); - Err(WalletError::BadParameter("Invalid public key".to_string()))?; - } - Pubkey::new(&pubkey_vec) - } else { - id.pubkey() - }; - - let tokens = pay_matches.value_of("tokens").unwrap().parse()?; - - Ok(WalletCommand::Pay(tokens, to)) - } - ("confirm", Some(confirm_matches)) => { - let signatures = bs58::decode(confirm_matches.value_of("signature").unwrap()) - .into_vec() - .expect("base58-encoded signature"); - - if signatures.len() == std::mem::size_of::() { - let signature = Signature::new(&signatures); - Ok(WalletCommand::Confirm(signature)) - } else { - eprintln!("{}", confirm_matches.usage()); - Err(WalletError::BadParameter("Invalid signature".to_string())) - } - } - ("balance", Some(_balance_matches)) => Ok(WalletCommand::Balance), - ("address", Some(_address_matches)) => Ok(WalletCommand::Address), - ("", None) => { - println!("{}", matches.usage()); - Err(WalletError::CommandNotRecognized( - "no subcommand given".to_string(), - )) - } - _ => unreachable!(), - }?; - - Ok(WalletConfig { - leader, - id, - drone_addr, // TODO: Add an option for this. - command, - }) -} - -fn process_command( - config: &WalletConfig, - client: &mut ThinClient, -) -> Result<(), Box> { - match config.command { - // Check client balance - WalletCommand::Address => { - println!("{}", config.id.pubkey()); - } - WalletCommand::Balance => { - println!("Balance requested..."); - let balance = client.poll_get_balance(&config.id.pubkey()); - match balance { - Ok(balance) => { - println!("Your balance is: {:?}", balance); - } - Err(ref e) if e.kind() == std::io::ErrorKind::Other => { - println!("No account found! Request an airdrop to get started."); - } - Err(error) => { - println!("An error occurred: {:?}", error); - } - } - } - // Request an airdrop from Solana Drone; - // Request amount is set in request_airdrop function - WalletCommand::AirDrop(tokens) => { - println!( - "Requesting airdrop of {:?} tokens from {}", - tokens, config.drone_addr - ); - let previous_balance = client.poll_get_balance(&config.id.pubkey()).unwrap_or(0); - request_airdrop(&config.drone_addr, &config.id.pubkey(), tokens as u64)?; - - // TODO: return airdrop Result from Drone instead of polling the - // network - let mut current_balance = previous_balance; - for _ in 0..20 { - sleep(Duration::from_millis(500)); - current_balance = client - .poll_get_balance(&config.id.pubkey()) - .unwrap_or(previous_balance); - - if previous_balance != current_balance { - break; - } - println!("."); - } - println!("Your balance is: {:?}", current_balance); - if current_balance - previous_balance != tokens { - Err("Airdrop failed!")?; - } - } - // If client has positive balance, spend tokens in {balance} number of transactions - WalletCommand::Pay(tokens, to) => { - let last_id = client.get_last_id(); - let signature = client.transfer(tokens, &config.id, to, &last_id)?; - println!("{}", signature); - } - // Confirm the last client transaction by signature - WalletCommand::Confirm(signature) => { - if client.check_signature(&signature) { - println!("Confirmed"); - } else { - println!("Not found"); - } - } - } - Ok(()) -} - -fn read_leader(path: &str) -> Result { - let file = File::open(path.to_string()).or_else(|err| { - Err(WalletError::BadParameter(format!( - "{}: Unable to open leader file: {}", - err, path - ))) - })?; - - serde_json::from_reader(file).or_else(|err| { - Err(WalletError::BadParameter(format!( - "{}: Failed to parse leader file: {}", - err, path - ))) - }) -} - -fn main() -> Result<(), Box> { - logger::setup(); - let config = parse_args()?; + let config = parse_args(&matches)?; let mut client = mk_client(&config.leader); process_command(&config, &mut client) } diff --git a/src/lib.rs b/src/lib.rs index 4364588d00..11dc25c158 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,8 @@ extern crate bincode; extern crate bs58; extern crate byteorder; extern crate chrono; +extern crate clap; +extern crate dirs; extern crate generic_array; extern crate itertools; extern crate jsonrpc_core; diff --git a/src/wallet.rs b/src/wallet.rs index bf549b7f4e..e0714161c7 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,10 +1,211 @@ use bincode::{deserialize, serialize}; +use bs58; +use clap::ArgMatches; +use crdt::NodeInfo; use drone::DroneRequest; -use signature::{Pubkey, Signature}; +use fullnode::Config; +use serde_json; +use signature::{Keypair, KeypairUtil, Pubkey, Signature}; +use std::fs::File; use std::io::prelude::*; use std::io::{Error, ErrorKind, Write}; use std::mem::size_of; -use std::net::{SocketAddr, TcpStream}; +use std::net::{Ipv4Addr, SocketAddr, TcpStream}; +use std::thread::sleep; +use std::time::Duration; +use std::{error, fmt, mem}; +use thin_client::ThinClient; + +pub enum WalletCommand { + Address, + Balance, + AirDrop(i64), + Pay(i64, Pubkey), + Confirm(Signature), +} + +#[derive(Debug, Clone)] +pub enum WalletError { + CommandNotRecognized(String), + BadParameter(String), +} + +impl fmt::Display for WalletError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid") + } +} + +impl error::Error for WalletError { + fn description(&self) -> &str { + "invalid" + } + + fn cause(&self) -> Option<&error::Error> { + // Generic error, underlying cause isn't tracked. + None + } +} + +pub struct WalletConfig { + pub leader: NodeInfo, + pub id: Keypair, + pub drone_addr: SocketAddr, + pub command: WalletCommand, +} + +impl Default for WalletConfig { + fn default() -> WalletConfig { + let default_addr = socketaddr!(0, 8000); + WalletConfig { + leader: NodeInfo::new_with_socketaddr(&default_addr), + id: Keypair::new(), + drone_addr: default_addr, + command: WalletCommand::Balance, + } + } +} + +pub fn parse_command( + pubkey: Pubkey, + matches: &ArgMatches, +) -> Result> { + let response = match matches.subcommand() { + ("airdrop", Some(airdrop_matches)) => { + let tokens = airdrop_matches.value_of("tokens").unwrap().parse()?; + Ok(WalletCommand::AirDrop(tokens)) + } + ("pay", Some(pay_matches)) => { + let to = if pay_matches.is_present("to") { + let pubkey_vec = bs58::decode(pay_matches.value_of("to").unwrap()) + .into_vec() + .expect("base58-encoded public key"); + + if pubkey_vec.len() != mem::size_of::() { + eprintln!("{}", pay_matches.usage()); + Err(WalletError::BadParameter("Invalid public key".to_string()))?; + } + Pubkey::new(&pubkey_vec) + } else { + pubkey + }; + + let tokens = pay_matches.value_of("tokens").unwrap().parse()?; + + Ok(WalletCommand::Pay(tokens, to)) + } + ("confirm", Some(confirm_matches)) => { + let signatures = bs58::decode(confirm_matches.value_of("signature").unwrap()) + .into_vec() + .expect("base58-encoded signature"); + + if signatures.len() == mem::size_of::() { + let signature = Signature::new(&signatures); + Ok(WalletCommand::Confirm(signature)) + } else { + eprintln!("{}", confirm_matches.usage()); + Err(WalletError::BadParameter("Invalid signature".to_string())) + } + } + ("balance", Some(_balance_matches)) => Ok(WalletCommand::Balance), + ("address", Some(_address_matches)) => Ok(WalletCommand::Address), + ("", None) => { + println!("{}", matches.usage()); + Err(WalletError::CommandNotRecognized( + "no subcommand given".to_string(), + )) + } + _ => unreachable!(), + }?; + Ok(response) +} + +pub fn process_command( + config: &WalletConfig, + client: &mut ThinClient, +) -> Result<(), Box> { + match config.command { + // Check client balance + WalletCommand::Address => { + println!("{}", config.id.pubkey()); + } + WalletCommand::Balance => { + println!("Balance requested..."); + let balance = client.poll_get_balance(&config.id.pubkey()); + match balance { + Ok(balance) => { + println!("Your balance is: {:?}", balance); + } + Err(ref e) if e.kind() == ErrorKind::Other => { + println!("No account found! Request an airdrop to get started."); + } + Err(error) => { + println!("An error occurred: {:?}", error); + } + } + } + // Request an airdrop from Solana Drone; + // Request amount is set in request_airdrop function + WalletCommand::AirDrop(tokens) => { + println!( + "Requesting airdrop of {:?} tokens from {}", + tokens, config.drone_addr + ); + let previous_balance = client.poll_get_balance(&config.id.pubkey()).unwrap_or(0); + request_airdrop(&config.drone_addr, &config.id.pubkey(), tokens as u64)?; + + // TODO: return airdrop Result from Drone instead of polling the + // network + let mut current_balance = previous_balance; + for _ in 0..20 { + sleep(Duration::from_millis(500)); + current_balance = client + .poll_get_balance(&config.id.pubkey()) + .unwrap_or(previous_balance); + + if previous_balance != current_balance { + break; + } + println!("."); + } + println!("Your balance is: {:?}", current_balance); + if current_balance - previous_balance != tokens { + Err("Airdrop failed!")?; + } + } + // If client has positive balance, spend tokens in {balance} number of transactions + WalletCommand::Pay(tokens, to) => { + let last_id = client.get_last_id(); + let signature = client.transfer(tokens, &config.id, to, &last_id)?; + println!("{}", signature); + } + // Confirm the last client transaction by signature + WalletCommand::Confirm(signature) => { + if client.check_signature(&signature) { + println!("Confirmed"); + } else { + println!("Not found"); + } + } + } + Ok(()) +} + +pub fn read_leader(path: &str) -> Result { + let file = File::open(path.to_string()).or_else(|err| { + Err(WalletError::BadParameter(format!( + "{}: Unable to open leader file: {}", + err, path + ))) + })?; + + serde_json::from_reader(file).or_else(|err| { + Err(WalletError::BadParameter(format!( + "{}: Failed to parse leader file: {}", + err, path + ))) + }) +} pub fn request_airdrop( drone_addr: &SocketAddr, @@ -32,3 +233,6 @@ pub fn request_airdrop( // TODO: add timeout to this function, in case of unresponsive drone Ok(signature) } + +#[cfg(test)] +mod tests {}