Move all handling except network/gossip from /bin to wallet module
This commit is contained in:
parent
9e24775051
commit
9ca2f5b3f7
|
@ -1,80 +1,65 @@
|
||||||
extern crate atty;
|
|
||||||
extern crate bincode;
|
|
||||||
extern crate bs58;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
extern crate dirs;
|
extern crate dirs;
|
||||||
extern crate serde_json;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate solana;
|
extern crate solana;
|
||||||
|
|
||||||
use clap::{App, Arg, SubCommand};
|
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||||
use solana::client::mk_client;
|
use solana::client::mk_client;
|
||||||
use solana::crdt::NodeInfo;
|
use solana::crdt::NodeInfo;
|
||||||
use solana::drone::DRONE_PORT;
|
use solana::drone::DRONE_PORT;
|
||||||
use solana::fullnode::Config;
|
|
||||||
use solana::logger;
|
use solana::logger;
|
||||||
use solana::signature::{read_keypair, Keypair, KeypairUtil, Pubkey, Signature};
|
use solana::signature::{read_keypair, KeypairUtil};
|
||||||
use solana::thin_client::{poll_gossip_for_leader, ThinClient};
|
use solana::thin_client::poll_gossip_for_leader;
|
||||||
use solana::wallet::request_airdrop;
|
use solana::wallet::{parse_command, process_command, read_leader, WalletConfig, WalletError};
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::fmt;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use std::thread::sleep;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
enum WalletCommand {
|
pub fn parse_args(matches: &ArgMatches) -> Result<WalletConfig, Box<error::Error>> {
|
||||||
Address,
|
let leader: NodeInfo;
|
||||||
Balance,
|
if let Some(l) = matches.value_of("leader") {
|
||||||
AirDrop(i64),
|
leader = read_leader(l)?.node_info;
|
||||||
Pay(i64, Pubkey),
|
} else {
|
||||||
Confirm(Signature),
|
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 8000);
|
||||||
}
|
leader = NodeInfo::new_with_socketaddr(&server_addr);
|
||||||
|
};
|
||||||
#[derive(Debug, Clone)]
|
let timeout: Option<u64>;
|
||||||
enum WalletError {
|
if let Some(secs) = matches.value_of("timeout") {
|
||||||
CommandNotRecognized(String),
|
timeout = Some(secs.to_string().parse().expect("integer"));
|
||||||
BadParameter(String),
|
} else {
|
||||||
}
|
timeout = None;
|
||||||
|
|
||||||
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> {
|
let mut path = dirs::home_dir().expect("home directory");
|
||||||
// Generic error, underlying cause isn't tracked.
|
let id_path = if matches.is_present("keypair") {
|
||||||
None
|
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 {
|
fn main() -> Result<(), Box<error::Error>> {
|
||||||
leader: NodeInfo,
|
logger::setup();
|
||||||
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<WalletConfig, Box<error::Error>> {
|
|
||||||
let matches = App::new("solana-wallet")
|
let matches = App::new("solana-wallet")
|
||||||
.version(crate_version!())
|
.version(crate_version!())
|
||||||
.arg(
|
.arg(
|
||||||
|
@ -146,185 +131,7 @@ fn parse_args() -> Result<WalletConfig, Box<error::Error>> {
|
||||||
.subcommand(SubCommand::with_name("address").about("Get your public key"))
|
.subcommand(SubCommand::with_name("address").about("Get your public key"))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let leader: NodeInfo;
|
let config = parse_args(&matches)?;
|
||||||
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<u64>;
|
|
||||||
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::<Pubkey>() {
|
|
||||||
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::<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
|
|
||||||
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<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>> {
|
|
||||||
logger::setup();
|
|
||||||
let config = parse_args()?;
|
|
||||||
let mut client = mk_client(&config.leader);
|
let mut client = mk_client(&config.leader);
|
||||||
process_command(&config, &mut client)
|
process_command(&config, &mut client)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,8 @@ extern crate bincode;
|
||||||
extern crate bs58;
|
extern crate bs58;
|
||||||
extern crate byteorder;
|
extern crate byteorder;
|
||||||
extern crate chrono;
|
extern crate chrono;
|
||||||
|
extern crate clap;
|
||||||
|
extern crate dirs;
|
||||||
extern crate generic_array;
|
extern crate generic_array;
|
||||||
extern crate itertools;
|
extern crate itertools;
|
||||||
extern crate jsonrpc_core;
|
extern crate jsonrpc_core;
|
||||||
|
|
208
src/wallet.rs
208
src/wallet.rs
|
@ -1,10 +1,211 @@
|
||||||
use bincode::{deserialize, serialize};
|
use bincode::{deserialize, serialize};
|
||||||
|
use bs58;
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use crdt::NodeInfo;
|
||||||
use drone::DroneRequest;
|
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::prelude::*;
|
||||||
use std::io::{Error, ErrorKind, Write};
|
use std::io::{Error, ErrorKind, Write};
|
||||||
use std::mem::size_of;
|
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<WalletCommand, Box<error::Error>> {
|
||||||
|
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::<Pubkey>() {
|
||||||
|
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::<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(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_command(
|
||||||
|
config: &WalletConfig,
|
||||||
|
client: &mut ThinClient,
|
||||||
|
) -> Result<(), Box<error::Error>> {
|
||||||
|
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<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
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn request_airdrop(
|
pub fn request_airdrop(
|
||||||
drone_addr: &SocketAddr,
|
drone_addr: &SocketAddr,
|
||||||
|
@ -32,3 +233,6 @@ pub fn request_airdrop(
|
||||||
// TODO: add timeout to this function, in case of unresponsive drone
|
// TODO: add timeout to this function, in case of unresponsive drone
|
||||||
Ok(signature)
|
Ok(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {}
|
||||||
|
|
Loading…
Reference in New Issue