2018-06-27 14:41:49 -07:00
|
|
|
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;
|
2018-06-29 18:33:20 -07:00
|
|
|
extern crate clap;
|
2018-06-26 22:52:34 -07:00
|
|
|
extern crate env_logger;
|
|
|
|
extern crate serde_json;
|
|
|
|
extern crate solana;
|
|
|
|
|
|
|
|
use bincode::serialize;
|
2018-06-29 18:33:20 -07:00
|
|
|
use clap::{App, Arg, SubCommand};
|
2018-06-29 14:12:26 -07:00
|
|
|
use solana::crdt::ReplicatedData;
|
2018-06-26 22:52:34 -07:00
|
|
|
use solana::drone::DroneRequest;
|
2018-07-05 12:01:40 -07:00
|
|
|
use solana::fullnode::Config;
|
2018-06-27 14:41:49 -07:00
|
|
|
use solana::mint::Mint;
|
2018-06-29 16:06:05 -07:00
|
|
|
use solana::signature::{PublicKey, Signature};
|
2018-06-26 22:52:34 -07:00
|
|
|
use solana::thin_client::ThinClient;
|
2018-06-29 10:38:00 -07:00
|
|
|
use std::error;
|
|
|
|
use std::fmt;
|
2018-06-26 22:52:34 -07:00
|
|
|
use std::fs::File;
|
2018-06-27 14:41:49 -07:00
|
|
|
use std::io;
|
2018-06-26 22:52:34 -07:00
|
|
|
use std::io::prelude::*;
|
2018-07-02 13:43:33 -07:00
|
|
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket};
|
2018-06-26 22:52:34 -07:00
|
|
|
use std::process::exit;
|
|
|
|
use std::thread::sleep;
|
2018-06-27 14:21:30 -07:00
|
|
|
use std::time::Duration;
|
2018-06-26 22:52:34 -07:00
|
|
|
|
2018-06-29 10:38:00 -07:00
|
|
|
enum WalletCommand {
|
2018-06-29 16:08:55 -07:00
|
|
|
Address,
|
2018-06-29 10:38:00 -07:00
|
|
|
Balance,
|
2018-06-29 18:33:20 -07:00
|
|
|
AirDrop(i64),
|
2018-06-29 16:06:05 -07:00
|
|
|
Pay(i64, PublicKey),
|
|
|
|
Confirm(Signature),
|
2018-06-29 10:38:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
enum WalletError {
|
|
|
|
CommandNotRecognized(String),
|
2018-07-02 14:54:35 -07:00
|
|
|
BadParameter(String),
|
2018-06-29 10:38:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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: ReplicatedData,
|
|
|
|
id: Mint,
|
|
|
|
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 {
|
|
|
|
leader: ReplicatedData::new_leader(&default_addr.clone()),
|
|
|
|
id: Mint::new(0),
|
|
|
|
drone_addr: default_addr.clone(),
|
|
|
|
command: WalletCommand::Balance,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-29 18:33:20 -07:00
|
|
|
fn parse_args() -> Result<WalletConfig, Box<error::Error>> {
|
|
|
|
let matches = App::new("solana-wallet")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("leader")
|
|
|
|
.short("l")
|
|
|
|
.long("leader")
|
|
|
|
.value_name("PATH")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("/path/to/leader.json"),
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("mint")
|
|
|
|
.short("m")
|
|
|
|
.long("mint")
|
|
|
|
.value_name("PATH")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("/path/to/mint.json"),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("airdrop")
|
|
|
|
.about("Request a batch of tokens")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("tokens")
|
|
|
|
// .index(1)
|
|
|
|
.long("tokens")
|
|
|
|
.value_name("NUMBER")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("The number of tokens to request"),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.subcommand(
|
|
|
|
SubCommand::with_name("pay")
|
|
|
|
.about("Send a payment")
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("tokens")
|
|
|
|
// .index(2)
|
|
|
|
.long("tokens")
|
|
|
|
.value_name("NUMBER")
|
|
|
|
.takes_value(true)
|
|
|
|
.required(true)
|
|
|
|
.help("the number of tokens to send"),
|
|
|
|
)
|
|
|
|
.arg(
|
|
|
|
Arg::with_name("to")
|
|
|
|
// .index(1)
|
|
|
|
.long("to")
|
|
|
|
.value_name("PUBKEY")
|
|
|
|
.takes_value(true)
|
|
|
|
.required(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
|
|
|
|
2018-06-29 18:33:20 -07:00
|
|
|
let leader: ReplicatedData;
|
|
|
|
if let Some(l) = matches.value_of("leader") {
|
2018-07-10 12:02:51 -07:00
|
|
|
leader = read_leader(l.to_string()).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);
|
2018-06-29 18:33:20 -07:00
|
|
|
leader = ReplicatedData::new_leader(&server_addr);
|
2018-06-26 22:52:34 -07:00
|
|
|
};
|
2018-06-29 10:38:00 -07:00
|
|
|
|
2018-06-29 18:33:20 -07:00
|
|
|
let id: Mint;
|
|
|
|
if let Some(m) = matches.value_of("mint") {
|
|
|
|
id = read_mint(m.to_string())?;
|
2018-06-28 11:58:33 -07:00
|
|
|
} else {
|
2018-06-29 09:30:09 -07:00
|
|
|
eprintln!("No mint found!");
|
2018-06-28 12:09:10 -07:00
|
|
|
exit(1);
|
2018-06-28 11:58:33 -07:00
|
|
|
};
|
2018-06-26 22:52:34 -07:00
|
|
|
|
2018-07-09 17:55:11 -07:00
|
|
|
let mut drone_addr = leader.contact_info.tpu.clone();
|
2018-06-26 22:52:34 -07:00
|
|
|
drone_addr.set_port(9900);
|
|
|
|
|
2018-06-29 18:33:20 -07:00
|
|
|
let command = match matches.subcommand() {
|
|
|
|
("airdrop", Some(airdrop_matches)) => {
|
|
|
|
let mut tokens: i64 = id.tokens;
|
|
|
|
if airdrop_matches.is_present("tokens") {
|
|
|
|
tokens = airdrop_matches.value_of("tokens").unwrap().parse()?;
|
|
|
|
}
|
|
|
|
Ok(WalletCommand::AirDrop(tokens))
|
|
|
|
}
|
|
|
|
("pay", Some(pay_matches)) => {
|
|
|
|
let to: PublicKey;
|
|
|
|
if pay_matches.is_present("to") {
|
|
|
|
let pubkey_vec = bs58::decode(pay_matches.value_of("to").unwrap())
|
|
|
|
.into_vec()
|
|
|
|
.expect("base58-encoded public key");
|
2018-07-02 14:54:35 -07:00
|
|
|
|
|
|
|
if pubkey_vec.len() != std::mem::size_of::<PublicKey>() {
|
|
|
|
display_actions();
|
|
|
|
Err(WalletError::BadParameter("Invalid public key".to_string()))?;
|
|
|
|
}
|
2018-06-29 18:33:20 -07:00
|
|
|
to = PublicKey::clone_from_slice(&pubkey_vec);
|
|
|
|
} else {
|
|
|
|
to = id.pubkey();
|
|
|
|
}
|
|
|
|
let mut tokens: i64 = id.tokens;
|
|
|
|
if pay_matches.is_present("tokens") {
|
|
|
|
tokens = pay_matches.value_of("tokens").unwrap().parse()?;
|
|
|
|
}
|
|
|
|
Ok(WalletCommand::Pay(tokens, to))
|
|
|
|
}
|
|
|
|
("confirm", Some(confirm_matches)) => {
|
2018-07-02 11:19:44 -07:00
|
|
|
let sig_vec = bs58::decode(confirm_matches.value_of("signature").unwrap())
|
2018-06-29 18:33:20 -07:00
|
|
|
.into_vec()
|
|
|
|
.expect("base58-encoded signature");
|
2018-07-02 14:54:35 -07:00
|
|
|
|
|
|
|
if sig_vec.len() == std::mem::size_of::<Signature>() {
|
|
|
|
let sig = Signature::clone_from_slice(&sig_vec);
|
|
|
|
Ok(WalletCommand::Confirm(sig))
|
|
|
|
} else {
|
|
|
|
display_actions();
|
|
|
|
Err(WalletError::BadParameter("Invalid signature".to_string()))
|
|
|
|
}
|
2018-06-29 18:33:20 -07:00
|
|
|
}
|
|
|
|
("balance", Some(_balance_matches)) => Ok(WalletCommand::Balance),
|
|
|
|
("address", Some(_address_matches)) => Ok(WalletCommand::Address),
|
|
|
|
("", None) => {
|
|
|
|
display_actions();
|
|
|
|
Err(WalletError::CommandNotRecognized(
|
|
|
|
"no subcommand given".to_string(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
}?;
|
|
|
|
|
2018-06-29 10:38:00 -07:00
|
|
|
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 => {
|
|
|
|
println!("{}", bs58::encode(config.id.pubkey()).into_string());
|
|
|
|
}
|
2018-06-29 10:38:00 -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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-06-29 10:38:00 -07:00
|
|
|
// Request an airdrop from Solana Drone;
|
|
|
|
// Request amount is set in request_airdrop function
|
2018-06-29 18:33:20 -07:00
|
|
|
WalletCommand::AirDrop(tokens) => {
|
2018-06-29 10:38:00 -07:00
|
|
|
println!("Airdrop requested...");
|
2018-06-29 18:33:20 -07:00
|
|
|
println!("Airdropping {:?} tokens", tokens);
|
2018-07-02 15:22:25 -07:00
|
|
|
let _airdrop = request_airdrop(&config.drone_addr, &config.id, tokens as u64)?;
|
2018-06-29 10:38:00 -07:00
|
|
|
// TODO: return airdrop Result from Drone
|
|
|
|
sleep(Duration::from_millis(100));
|
|
|
|
println!(
|
|
|
|
"Your balance is: {:?}",
|
|
|
|
client.poll_get_balance(&config.id.pubkey()).unwrap()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// If client has positive balance, spend tokens in {balance} number of transactions
|
2018-06-29 16:06:05 -07:00
|
|
|
WalletCommand::Pay(tokens, to) => {
|
2018-06-29 10:38:00 -07:00
|
|
|
let last_id = client.get_last_id();
|
2018-06-29 16:06:05 -07:00
|
|
|
let sig = client.transfer(tokens, &config.id.keypair(), to, &last_id)?;
|
|
|
|
println!("{}", bs58::encode(sig).into_string());
|
2018-06-29 10:38:00 -07:00
|
|
|
}
|
|
|
|
// Confirm the last client transaction by signature
|
2018-06-29 16:06:05 -07:00
|
|
|
WalletCommand::Confirm(sig) => {
|
|
|
|
if client.check_signature(&sig) {
|
|
|
|
println!("Confirmed");
|
|
|
|
} else {
|
|
|
|
println!("Not found");
|
2018-06-29 10:38:00 -07:00
|
|
|
}
|
2018-06-29 16:06:05 -07:00
|
|
|
}
|
2018-06-26 22:52:34 -07:00
|
|
|
}
|
2018-06-29 10:38:00 -07:00
|
|
|
Ok(())
|
2018-06-26 22:52:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
fn display_actions() {
|
|
|
|
println!("");
|
2018-06-29 15:04:35 -07:00
|
|
|
println!("Commands:");
|
2018-06-29 16:08:55 -07:00
|
|
|
println!(" address Get your public key");
|
2018-06-29 15:04:35 -07:00
|
|
|
println!(" balance Get your account balance");
|
|
|
|
println!(" airdrop Request a batch of tokens");
|
2018-07-02 19:15:40 -07:00
|
|
|
println!(" pay Send tokens to a public key");
|
2018-06-29 15:04:35 -07:00
|
|
|
println!(" confirm Confirm your last payment by signature");
|
2018-06-26 22:52:34 -07:00
|
|
|
println!("");
|
|
|
|
}
|
|
|
|
|
2018-07-05 12:01:40 -07:00
|
|
|
fn read_leader(path: String) -> Config {
|
2018-06-26 22:52:34 -07:00
|
|
|
let file = File::open(path.clone()).expect(&format!("file not found: {}", path));
|
|
|
|
serde_json::from_reader(file).expect(&format!("failed to parse {}", path))
|
2018-06-28 11:58:33 -07:00
|
|
|
}
|
|
|
|
|
2018-06-29 10:38:00 -07:00
|
|
|
fn read_mint(path: String) -> Result<Mint, Box<error::Error>> {
|
|
|
|
let file = File::open(path.clone())?;
|
|
|
|
let mint = serde_json::from_reader(file)?;
|
|
|
|
Ok(mint)
|
2018-06-26 22:52:34 -07:00
|
|
|
}
|
|
|
|
|
2018-06-29 14:12:26 -07:00
|
|
|
fn mk_client(r: &ReplicatedData) -> io::Result<ThinClient> {
|
2018-07-02 13:43:33 -07:00
|
|
|
let requests_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
|
|
|
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
|
|
|
requests_socket
|
2018-06-29 14:12:26 -07:00
|
|
|
.set_read_timeout(Some(Duration::new(1, 0)))
|
|
|
|
.unwrap();
|
2018-06-26 22:52:34 -07:00
|
|
|
|
2018-06-27 14:41:49 -07:00
|
|
|
Ok(ThinClient::new(
|
2018-07-09 17:55:11 -07:00
|
|
|
r.contact_info.rpu,
|
2018-07-02 13:43:33 -07:00
|
|
|
requests_socket,
|
2018-07-09 17:55:11 -07:00
|
|
|
r.contact_info.tpu,
|
2018-07-02 13:43:33 -07:00
|
|
|
transactions_socket,
|
2018-06-27 14:41:49 -07:00
|
|
|
))
|
2018-06-26 22:52:34 -07:00
|
|
|
}
|
|
|
|
|
2018-07-02 15:22:25 -07:00
|
|
|
fn request_airdrop(
|
|
|
|
drone_addr: &SocketAddr,
|
|
|
|
id: &Mint,
|
|
|
|
tokens: u64,
|
|
|
|
) -> Result<(), Box<error::Error>> {
|
|
|
|
let mut stream = TcpStream::connect(drone_addr)?;
|
2018-06-26 22:52:34 -07:00
|
|
|
let req = DroneRequest::GetAirdrop {
|
2018-06-29 18:33:20 -07:00
|
|
|
airdrop_request_amount: tokens,
|
2018-06-27 14:41:49 -07:00
|
|
|
client_public_key: id.pubkey(),
|
2018-06-26 22:52:34 -07:00
|
|
|
};
|
|
|
|
let tx = serialize(&req).expect("serialize drone request");
|
|
|
|
stream.write_all(&tx).unwrap();
|
|
|
|
// TODO: add timeout to this function, in case of unresponsive drone
|
2018-07-02 15:22:25 -07:00
|
|
|
Ok(())
|
2018-06-26 22:52:34 -07:00
|
|
|
}
|
2018-06-29 10:38:00 -07:00
|
|
|
|
|
|
|
fn main() -> Result<(), Box<error::Error>> {
|
2018-07-02 10:54:07 -07:00
|
|
|
env_logger::init();
|
2018-06-29 18:33:20 -07:00
|
|
|
let config = parse_args()?;
|
2018-06-29 14:12:26 -07:00
|
|
|
let mut client = mk_client(&config.leader)?;
|
2018-06-29 10:38:00 -07:00
|
|
|
process_command(&config, &mut client)
|
|
|
|
}
|