solana-with-rpc-optimizations/src/bin/wallet.rs

352 lines
11 KiB
Rust
Raw Normal View History

extern crate atty;
2018-06-26 22:52:34 -07:00
extern crate bincode;
2018-06-29 16:06:05 -07:00
extern crate bs58;
#[macro_use]
extern crate clap;
2018-07-12 17:16:30 -07:00
extern crate dirs;
2018-06-26 22:52:34 -07:00
extern crate serde_json;
extern crate solana;
use clap::{App, Arg, SubCommand};
use solana::client::mk_client;
use solana::crdt::NodeInfo;
use solana::drone::DRONE_PORT;
use solana::fullnode::Config;
2018-07-27 21:37:53 -07:00
use solana::logger;
use solana::nat::get_public_ip_addr;
use solana::signature::{read_keypair, Keypair, KeypairUtil, Pubkey, Signature};
use solana::thin_client::{poll_gossip_for_leader, ThinClient};
use solana::wallet::request_airdrop;
use std::error;
use std::fmt;
2018-06-26 22:52:34 -07:00
use std::fs::File;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::process::exit;
2018-06-26 22:52:34 -07:00
use std::thread::sleep;
use std::time::Duration;
2018-06-26 22:52:34 -07:00
enum WalletCommand {
2018-06-29 16:08:55 -07:00
Address,
Balance,
AirDrop(i64),
Pay(i64, Pubkey),
2018-06-29 16:06:05 -07:00
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"
}
fn cause(&self) -> Option<&error::Error> {
// Generic error, underlying cause isn't tracked.
None
}
}
struct WalletConfig {
leader: NodeInfo,
2018-08-09 07:56:04 -07:00
id: Keypair,
drone_addr: SocketAddr,
command: WalletCommand,
}
impl Default for WalletConfig {
fn default() -> WalletConfig {
let default_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
WalletConfig {
2018-07-11 12:32:54 -07:00
leader: NodeInfo::new_leader(&default_addr),
2018-08-09 07:56:04 -07:00
id: Keypair::new(),
2018-07-11 12:32:54 -07:00
drone_addr: default_addr,
command: WalletCommand::Balance,
}
}
}
fn parse_args() -> Result<WalletConfig, Box<error::Error>> {
let matches = App::new("solana-wallet")
.version(crate_version!())
.arg(
Arg::with_name("leader")
.short("l")
.long("leader")
.value_name("PATH")
.takes_value(true)
.help("/path/to/leader.json"),
)
.arg(
2018-07-12 15:02:14 -07:00
Arg::with_name("keypair")
.short("k")
.long("keypair")
.value_name("PATH")
.takes_value(true)
2018-07-12 15:02:14 -07:00
.help("/path/to/id.json"),
)
.arg(
Arg::with_name("addr")
.short("a")
.long("addr")
.value_name("IPADDR")
.takes_value(true)
.help("address to advertise to the network"),
)
.arg(
Arg::with_name("timeout")
.long("timeout")
.value_name("SECONDS")
.takes_value(true)
.help("Max SECONDS to wait to get necessary gossip from the network"),
)
.subcommand(
SubCommand::with_name("airdrop")
.about("Request a batch of tokens")
.arg(
Arg::with_name("tokens")
.long("tokens")
.value_name("NUMBER")
.takes_value(true)
.required(true)
.help("The number of tokens to request"),
),
)
.subcommand(
SubCommand::with_name("pay")
.about("Send a payment")
.arg(
Arg::with_name("tokens")
.long("tokens")
.value_name("NUMBER")
.takes_value(true)
.required(true)
.help("The number of tokens to send"),
)
.arg(
Arg::with_name("to")
.long("to")
.value_name("PUBKEY")
.takes_value(true)
.help("The pubkey of recipient"),
),
)
.subcommand(
SubCommand::with_name("confirm")
.about("Confirm your payment by signature")
.arg(
Arg::with_name("signature")
.index(1)
.value_name("SIGNATURE")
.required(true)
.help("The transaction signature to confirm"),
),
)
.subcommand(SubCommand::with_name("balance").about("Get your balance"))
.subcommand(SubCommand::with_name("address").about("Get your public key"))
.get_matches();
2018-06-26 22:52:34 -07:00
let addr = if let Some(s) = matches.value_of("addr") {
s.to_string().parse().unwrap_or_else(|e| {
eprintln!("failed to parse {} as IP address error: {:?}", s, e);
exit(1)
})
} else {
get_public_ip_addr().unwrap_or_else(|e| {
eprintln!("failed to get public IP, try --addr? error: {:?}", e);
exit(1)
})
};
let leader: NodeInfo;
if let Some(l) = matches.value_of("leader") {
leader = read_leader(l)?.node_info;
2018-06-26 22:52:34 -07:00
} else {
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
leader = NodeInfo::new_leader(&server_addr);
2018-06-26 22:52:34 -07:00
};
let timeout: Option<u64>;
if let Some(secs) = matches.value_of("timeout") {
timeout = Some(secs.to_string().parse().expect("integer"));
} else {
timeout = None;
}
2018-07-12 17:16:30 -07:00
let mut path = dirs::home_dir().expect("home directory");
2018-07-12 16:04:42 -07:00
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
)))
})?;
2018-06-26 22:52:34 -07:00
let leader = poll_gossip_for_leader(leader.contact_info.ncp, timeout, addr)?;
2018-07-11 12:32:54 -07:00
let mut drone_addr = leader.contact_info.tpu;
2018-07-18 12:38:18 -07:00
drone_addr.set_port(DRONE_PORT);
2018-06-26 22:52:34 -07:00
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)) => {
2018-07-11 12:32:54 -07:00
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::<Pubkey>() {
eprintln!("{}", pay_matches.usage());
Err(WalletError::BadParameter("Invalid public key".to_string()))?;
}
Pubkey::new(&pubkey_vec)
} else {
2018-07-11 12:32:54 -07:00
id.pubkey()
};
let tokens = pay_matches.value_of("tokens").unwrap().parse()?;
2018-07-11 12:32:54 -07:00
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::<Signature>() {
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<error::Error>> {
match config.command {
// Check client balance
2018-06-29 16:06:05 -07:00
WalletCommand::Address => {
2018-07-31 10:27:08 -07:00
println!("{}", config.id.pubkey());
2018-06-29 16:06:05 -07:00
}
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);
2018-06-26 22:52:34 -07:00
}
}
}
// Request an airdrop from Solana Drone;
// Request amount is set in request_airdrop function
WalletCommand::AirDrop(tokens) => {
2018-07-13 17:44:47 -07:00
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!(".");
}
2018-07-13 17:44:47 -07:00
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
2018-06-29 16:06:05 -07:00
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) {
2018-06-29 16:06:05 -07:00
println!("Confirmed");
} else {
println!("Not found");
}
2018-06-29 16:06:05 -07:00
}
2018-06-26 22:52:34 -07:00
}
Ok(())
2018-06-26 22:52:34 -07:00
}
fn read_leader(path: &str) -> Result<Config, WalletError> {
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<error::Error>> {
2018-07-27 21:37:53 -07:00
logger::setup();
let config = parse_args()?;
let mut client = mk_client(&config.leader);
process_command(&config, &mut client)
}