2018-08-26 21:14:20 -07:00
|
|
|
use bincode::{deserialize, serialize};
|
2018-09-14 01:58:39 -07:00
|
|
|
use bs58;
|
|
|
|
use clap::ArgMatches;
|
|
|
|
use crdt::NodeInfo;
|
2018-07-31 22:07:53 -07:00
|
|
|
use drone::DroneRequest;
|
2018-09-14 01:58:39 -07:00
|
|
|
use fullnode::Config;
|
2018-09-20 22:27:06 -07:00
|
|
|
use hash::Hash;
|
|
|
|
use reqwest;
|
|
|
|
use reqwest::header::CONTENT_TYPE;
|
2018-09-17 09:48:04 -07:00
|
|
|
use ring::rand::SystemRandom;
|
|
|
|
use ring::signature::Ed25519KeyPair;
|
2018-09-20 22:27:06 -07:00
|
|
|
use serde_json::{self, Value};
|
2018-09-14 01:58:39 -07:00
|
|
|
use signature::{Keypair, KeypairUtil, Pubkey, Signature};
|
2018-09-17 09:48:04 -07:00
|
|
|
use std::fs::{self, File};
|
2018-08-26 21:14:20 -07:00
|
|
|
use std::io::prelude::*;
|
|
|
|
use std::io::{Error, ErrorKind, Write};
|
|
|
|
use std::mem::size_of;
|
2018-09-14 01:58:39 -07:00
|
|
|
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
|
2018-09-17 09:48:04 -07:00
|
|
|
use std::path::Path;
|
2018-09-14 01:58:39 -07:00
|
|
|
use std::thread::sleep;
|
|
|
|
use std::time::Duration;
|
|
|
|
use std::{error, fmt, mem};
|
2018-09-20 22:27:06 -07:00
|
|
|
use transaction::Transaction;
|
2018-09-14 01:58:39 -07:00
|
|
|
|
2018-09-14 01:59:09 -07:00
|
|
|
#[derive(Debug, PartialEq)]
|
2018-09-14 01:58:39 -07:00
|
|
|
pub enum WalletCommand {
|
|
|
|
Address,
|
|
|
|
Balance,
|
|
|
|
AirDrop(i64),
|
|
|
|
Pay(i64, Pubkey),
|
|
|
|
Confirm(Signature),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub enum WalletError {
|
|
|
|
CommandNotRecognized(String),
|
|
|
|
BadParameter(String),
|
2018-09-20 22:27:06 -07:00
|
|
|
RpcRequestError(String),
|
2018-09-14 01:58:39 -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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct WalletConfig {
|
|
|
|
pub leader: NodeInfo,
|
|
|
|
pub id: Keypair,
|
|
|
|
pub drone_addr: SocketAddr,
|
2018-09-21 14:27:03 -07:00
|
|
|
pub rpc_addr: String,
|
2018-09-14 01:58:39 -07:00
|
|
|
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,
|
2018-09-21 14:27:03 -07:00
|
|
|
rpc_addr: default_addr.to_string(),
|
2018-09-14 01:58:39 -07:00
|
|
|
command: WalletCommand::Balance,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_command(
|
|
|
|
pubkey: Pubkey,
|
|
|
|
matches: &ArgMatches,
|
|
|
|
) -> Result<WalletCommand, Box<error::Error>> {
|
|
|
|
let response = match matches.subcommand() {
|
2018-09-20 22:27:06 -07:00
|
|
|
("address", Some(_address_matches)) => Ok(WalletCommand::Address),
|
2018-09-14 01:58:39 -07:00
|
|
|
("airdrop", Some(airdrop_matches)) => {
|
|
|
|
let tokens = airdrop_matches.value_of("tokens").unwrap().parse()?;
|
|
|
|
Ok(WalletCommand::AirDrop(tokens))
|
|
|
|
}
|
2018-09-20 22:27:06 -07:00
|
|
|
("balance", Some(_balance_matches)) => Ok(WalletCommand::Balance),
|
|
|
|
("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()))
|
|
|
|
}
|
|
|
|
}
|
2018-09-14 01:58:39 -07:00
|
|
|
("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))
|
|
|
|
}
|
|
|
|
("", None) => {
|
|
|
|
println!("{}", matches.usage());
|
|
|
|
Err(WalletError::CommandNotRecognized(
|
|
|
|
"no subcommand given".to_string(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
_ => unreachable!(),
|
|
|
|
}?;
|
|
|
|
Ok(response)
|
|
|
|
}
|
|
|
|
|
2018-09-20 22:27:06 -07:00
|
|
|
pub fn process_command(config: &WalletConfig) -> Result<String, Box<error::Error>> {
|
2018-09-14 01:58:39 -07:00
|
|
|
match config.command {
|
2018-09-20 22:27:06 -07:00
|
|
|
// Get address of this client
|
2018-09-17 12:15:26 -07:00
|
|
|
WalletCommand::Address => Ok(format!("{}", config.id.pubkey())),
|
2018-09-14 01:58:39 -07:00
|
|
|
// Request an airdrop from Solana Drone;
|
|
|
|
WalletCommand::AirDrop(tokens) => {
|
|
|
|
println!(
|
|
|
|
"Requesting airdrop of {:?} tokens from {}",
|
|
|
|
tokens, config.drone_addr
|
|
|
|
);
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(format!("{}", config.id.pubkey()));
|
|
|
|
let previous_balance = match WalletRpcRequest::GetBalance
|
2018-09-20 22:27:06 -07:00
|
|
|
.make_rpc_request(&config.rpc_addr, 1, Some(params))?
|
|
|
|
.as_i64()
|
2018-09-21 14:27:03 -07:00
|
|
|
{
|
|
|
|
Some(tokens) => tokens,
|
|
|
|
None => Err(WalletError::RpcRequestError(
|
|
|
|
"Received result of an unexpected type".to_string(),
|
|
|
|
))?,
|
|
|
|
};
|
2018-09-14 01:58:39 -07:00
|
|
|
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));
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(format!("{}", config.id.pubkey()));
|
2018-09-20 22:27:06 -07:00
|
|
|
current_balance = WalletRpcRequest::GetBalance
|
|
|
|
.make_rpc_request(&config.rpc_addr, 1, Some(params))?
|
|
|
|
.as_i64()
|
2018-09-14 01:58:39 -07:00
|
|
|
.unwrap_or(previous_balance);
|
|
|
|
|
|
|
|
if previous_balance != current_balance {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
println!(".");
|
|
|
|
}
|
|
|
|
if current_balance - previous_balance != tokens {
|
|
|
|
Err("Airdrop failed!")?;
|
|
|
|
}
|
2018-09-17 12:15:26 -07:00
|
|
|
Ok(format!("Your balance is: {:?}", current_balance))
|
2018-09-14 01:58:39 -07:00
|
|
|
}
|
2018-09-20 22:27:06 -07:00
|
|
|
WalletCommand::Balance => {
|
|
|
|
println!("Balance requested...");
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(format!("{}", config.id.pubkey()));
|
2018-09-20 22:27:06 -07:00
|
|
|
let balance = WalletRpcRequest::GetBalance
|
|
|
|
.make_rpc_request(&config.rpc_addr, 1, Some(params))?
|
|
|
|
.as_i64();
|
|
|
|
match balance {
|
|
|
|
Some(0) => Ok("No account found! Request an airdrop to get started.".to_string()),
|
|
|
|
Some(tokens) => Ok(format!("Your balance is: {:?}", tokens)),
|
|
|
|
None => Err(WalletError::RpcRequestError(
|
|
|
|
"Received result of an unexpected type".to_string(),
|
|
|
|
))?,
|
|
|
|
}
|
2018-09-14 01:58:39 -07:00
|
|
|
}
|
|
|
|
// Confirm the last client transaction by signature
|
|
|
|
WalletCommand::Confirm(signature) => {
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(format!("{}", signature));
|
2018-09-20 22:27:06 -07:00
|
|
|
let confirmation = WalletRpcRequest::ConfirmTransaction
|
|
|
|
.make_rpc_request(&config.rpc_addr, 1, Some(params))?
|
|
|
|
.as_bool();
|
|
|
|
match confirmation {
|
|
|
|
Some(b) => {
|
|
|
|
if b {
|
|
|
|
Ok("Confirmed".to_string())
|
|
|
|
} else {
|
|
|
|
Ok("Not found".to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => Err(WalletError::RpcRequestError(
|
|
|
|
"Received result of an unexpected type".to_string(),
|
|
|
|
))?,
|
|
|
|
}
|
|
|
|
}
|
2018-09-21 14:27:03 -07:00
|
|
|
// If client has positive balance, pay tokens to another address
|
2018-09-20 22:27:06 -07:00
|
|
|
WalletCommand::Pay(tokens, to) => {
|
|
|
|
let result = WalletRpcRequest::GetLastId.make_rpc_request(&config.rpc_addr, 1, None)?;
|
|
|
|
if result.as_str().is_none() {
|
|
|
|
Err(WalletError::RpcRequestError(
|
|
|
|
"Received bad last_id".to_string(),
|
|
|
|
))?
|
2018-09-14 01:58:39 -07:00
|
|
|
}
|
2018-09-20 22:27:06 -07:00
|
|
|
let last_id_str = result.as_str().unwrap();
|
|
|
|
let last_id_vec = bs58::decode(last_id_str)
|
|
|
|
.into_vec()
|
|
|
|
.map_err(|_| WalletError::RpcRequestError("Received bad last_id".to_string()))?;
|
|
|
|
let last_id = Hash::new(&last_id_vec);
|
|
|
|
|
|
|
|
let tx = Transaction::new(&config.id, to, tokens, last_id);
|
|
|
|
let serialized = serialize(&tx).unwrap();
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(serialized);
|
2018-09-20 22:27:06 -07:00
|
|
|
let signature = WalletRpcRequest::SendTransaction.make_rpc_request(
|
|
|
|
&config.rpc_addr,
|
|
|
|
2,
|
2018-09-21 14:27:03 -07:00
|
|
|
Some(params),
|
2018-09-20 22:27:06 -07:00
|
|
|
)?;
|
|
|
|
if signature.as_str().is_none() {
|
|
|
|
Err(WalletError::RpcRequestError(
|
|
|
|
"Received result of an unexpected type".to_string(),
|
|
|
|
))?
|
|
|
|
}
|
|
|
|
let signature_str = signature.as_str().unwrap();
|
|
|
|
|
2018-09-23 14:38:17 -07:00
|
|
|
Ok(signature_str.to_string())
|
2018-09-14 01:58:39 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
)))
|
|
|
|
})
|
|
|
|
}
|
2018-07-31 22:07:53 -07:00
|
|
|
|
|
|
|
pub fn request_airdrop(
|
|
|
|
drone_addr: &SocketAddr,
|
2018-08-09 08:13:57 -07:00
|
|
|
id: &Pubkey,
|
2018-07-31 22:07:53 -07:00
|
|
|
tokens: u64,
|
2018-08-24 08:38:09 -07:00
|
|
|
) -> Result<Signature, Error> {
|
2018-08-26 21:14:20 -07:00
|
|
|
// TODO: make this async tokio client
|
2018-07-31 22:07:53 -07:00
|
|
|
let mut stream = TcpStream::connect(drone_addr)?;
|
|
|
|
let req = DroneRequest::GetAirdrop {
|
|
|
|
airdrop_request_amount: tokens,
|
2018-08-09 08:03:08 -07:00
|
|
|
client_pubkey: *id,
|
2018-07-31 22:07:53 -07:00
|
|
|
};
|
|
|
|
let tx = serialize(&req).expect("serialize drone request");
|
2018-08-24 08:38:09 -07:00
|
|
|
stream.write_all(&tx)?;
|
2018-08-26 21:14:20 -07:00
|
|
|
let mut buffer = [0; size_of::<Signature>()];
|
|
|
|
stream
|
|
|
|
.read_exact(&mut buffer)
|
|
|
|
.or_else(|_| Err(Error::new(ErrorKind::Other, "Airdrop failed")))?;
|
|
|
|
let signature: Signature = deserialize(&buffer).or_else(|err| {
|
|
|
|
Err(Error::new(
|
|
|
|
ErrorKind::Other,
|
|
|
|
format!("deserialize signature in request_airdrop: {:?}", err),
|
|
|
|
))
|
|
|
|
})?;
|
2018-07-31 22:07:53 -07:00
|
|
|
// TODO: add timeout to this function, in case of unresponsive drone
|
2018-08-26 21:14:20 -07:00
|
|
|
Ok(signature)
|
2018-07-31 22:07:53 -07:00
|
|
|
}
|
2018-09-14 01:58:39 -07:00
|
|
|
|
2018-09-17 09:48:04 -07:00
|
|
|
pub fn gen_keypair_file(outfile: String) -> Result<String, Box<error::Error>> {
|
|
|
|
let rnd = SystemRandom::new();
|
|
|
|
let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rnd)?;
|
|
|
|
let serialized = serde_json::to_string(&pkcs8_bytes.to_vec())?;
|
|
|
|
|
|
|
|
if outfile != "-" {
|
|
|
|
if let Some(outdir) = Path::new(&outfile).parent() {
|
|
|
|
fs::create_dir_all(outdir)?;
|
|
|
|
}
|
|
|
|
let mut f = File::create(outfile)?;
|
|
|
|
f.write_all(&serialized.clone().into_bytes())?;
|
|
|
|
}
|
|
|
|
Ok(serialized)
|
|
|
|
}
|
|
|
|
|
2018-09-20 22:27:06 -07:00
|
|
|
pub enum WalletRpcRequest {
|
|
|
|
ConfirmTransaction,
|
|
|
|
GetAccountInfo,
|
|
|
|
GetBalance,
|
|
|
|
GetFinality,
|
|
|
|
GetLastId,
|
|
|
|
GetTransactionCount,
|
|
|
|
RequestAirdrop,
|
|
|
|
SendTransaction,
|
|
|
|
}
|
|
|
|
impl WalletRpcRequest {
|
|
|
|
fn make_rpc_request(
|
|
|
|
&self,
|
2018-09-23 14:38:17 -07:00
|
|
|
rpc_addr: &str,
|
2018-09-20 22:27:06 -07:00
|
|
|
id: u64,
|
2018-09-21 14:27:03 -07:00
|
|
|
params: Option<Value>,
|
2018-09-20 22:27:06 -07:00
|
|
|
) -> Result<Value, Box<error::Error>> {
|
|
|
|
let jsonrpc = "2.0";
|
|
|
|
let method = match self {
|
|
|
|
WalletRpcRequest::ConfirmTransaction => "confirmTransaction",
|
|
|
|
WalletRpcRequest::GetAccountInfo => "getAccountInfo",
|
|
|
|
WalletRpcRequest::GetBalance => "getBalance",
|
|
|
|
WalletRpcRequest::GetFinality => "getFinality",
|
|
|
|
WalletRpcRequest::GetLastId => "getLastId",
|
|
|
|
WalletRpcRequest::GetTransactionCount => "getTransactionCount",
|
|
|
|
WalletRpcRequest::RequestAirdrop => "requestAirdrop",
|
|
|
|
WalletRpcRequest::SendTransaction => "sendTransaction",
|
|
|
|
};
|
|
|
|
let client = reqwest::Client::new();
|
2018-09-21 14:27:03 -07:00
|
|
|
let mut request = json!({
|
|
|
|
"jsonrpc": jsonrpc,
|
|
|
|
"id": id,
|
|
|
|
"method": method,
|
|
|
|
});
|
2018-09-20 22:27:06 -07:00
|
|
|
if let Some(param_string) = params {
|
2018-09-21 14:27:03 -07:00
|
|
|
request["params"] = json!(vec![param_string]);
|
2018-09-20 22:27:06 -07:00
|
|
|
}
|
|
|
|
let mut response = client
|
2018-09-21 14:27:03 -07:00
|
|
|
.post(rpc_addr)
|
2018-09-20 22:27:06 -07:00
|
|
|
.header(CONTENT_TYPE, "application/json")
|
2018-09-21 14:27:03 -07:00
|
|
|
.body(request.to_string())
|
2018-09-20 22:27:06 -07:00
|
|
|
.send()?;
|
|
|
|
let json: Value = serde_json::from_str(&response.text()?)?;
|
|
|
|
if json["error"].is_object() {
|
|
|
|
Err(WalletError::RpcRequestError(format!(
|
|
|
|
"RPC Error response: {}",
|
|
|
|
serde_json::to_string(&json["error"]).unwrap()
|
|
|
|
)))?
|
|
|
|
}
|
|
|
|
Ok(json["result"].clone())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-14 01:58:39 -07:00
|
|
|
#[cfg(test)]
|
2018-09-14 01:59:09 -07:00
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use bank::Bank;
|
|
|
|
use clap::{App, Arg, SubCommand};
|
|
|
|
use crdt::Node;
|
|
|
|
use drone::run_local_drone;
|
|
|
|
use fullnode::Fullnode;
|
|
|
|
use ledger::LedgerWriter;
|
|
|
|
use mint::Mint;
|
2018-09-17 09:48:04 -07:00
|
|
|
use signature::{read_keypair, read_pkcs8, Keypair, KeypairUtil};
|
2018-09-14 01:59:09 -07:00
|
|
|
use std::sync::mpsc::channel;
|
|
|
|
|
|
|
|
fn tmp_ledger(name: &str, mint: &Mint) -> String {
|
|
|
|
use std::env;
|
|
|
|
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| "target".to_string());
|
|
|
|
let keypair = Keypair::new();
|
|
|
|
|
|
|
|
let path = format!("{}/tmp-ledger-{}-{}", out_dir, name, keypair.pubkey());
|
|
|
|
|
|
|
|
let mut writer = LedgerWriter::open(&path, true).unwrap();
|
|
|
|
writer.write_entries(mint.create_entries()).unwrap();
|
|
|
|
|
|
|
|
path
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_command() {
|
|
|
|
let pubkey = Keypair::new().pubkey();
|
|
|
|
let test_commands = App::new("test")
|
|
|
|
.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"),
|
|
|
|
),
|
2018-09-14 16:25:14 -07:00
|
|
|
).subcommand(
|
2018-09-14 01:59:09 -07:00
|
|
|
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"),
|
2018-09-14 16:25:14 -07:00
|
|
|
).arg(
|
2018-09-14 01:59:09 -07:00
|
|
|
Arg::with_name("to")
|
|
|
|
.long("to")
|
|
|
|
.value_name("PUBKEY")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("The pubkey of recipient"),
|
|
|
|
),
|
2018-09-14 16:25:14 -07:00
|
|
|
).subcommand(
|
2018-09-14 01:59:09 -07:00
|
|
|
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"),
|
|
|
|
),
|
2018-09-14 16:25:14 -07:00
|
|
|
).subcommand(SubCommand::with_name("balance").about("Get your balance"))
|
2018-09-14 01:59:09 -07:00
|
|
|
.subcommand(SubCommand::with_name("address").about("Get your public key"));
|
|
|
|
|
|
|
|
let test_airdrop = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "airdrop", "--tokens", "50"]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(pubkey, &test_airdrop).unwrap(),
|
|
|
|
WalletCommand::AirDrop(50)
|
|
|
|
);
|
|
|
|
let test_bad_airdrop = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "airdrop", "--tokens", "notint"]);
|
|
|
|
assert!(parse_command(pubkey, &test_bad_airdrop).is_err());
|
|
|
|
|
|
|
|
let pubkey_string = format!("{}", pubkey);
|
|
|
|
let test_pay = test_commands.clone().get_matches_from(vec![
|
|
|
|
"test",
|
|
|
|
"pay",
|
|
|
|
"--tokens",
|
|
|
|
"50",
|
|
|
|
"--to",
|
|
|
|
&pubkey_string,
|
|
|
|
]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(pubkey, &test_pay).unwrap(),
|
|
|
|
WalletCommand::Pay(50, pubkey)
|
|
|
|
);
|
|
|
|
let test_bad_pubkey = test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "pay", "--tokens", "50", "--to", "deadbeef"]);
|
|
|
|
assert!(parse_command(pubkey, &test_bad_pubkey).is_err());
|
|
|
|
|
|
|
|
let signature = Signature::new(&vec![1; 64]);
|
|
|
|
let signature_string = format!("{:?}", signature);
|
|
|
|
let test_confirm =
|
|
|
|
test_commands
|
|
|
|
.clone()
|
|
|
|
.get_matches_from(vec!["test", "confirm", &signature_string]);
|
|
|
|
assert_eq!(
|
|
|
|
parse_command(pubkey, &test_confirm).unwrap(),
|
|
|
|
WalletCommand::Confirm(signature)
|
|
|
|
);
|
|
|
|
let test_bad_signature =
|
|
|
|
test_commands.get_matches_from(vec!["test", "confirm", "deadbeef"]);
|
|
|
|
assert!(parse_command(pubkey, &test_bad_signature).is_err());
|
|
|
|
}
|
|
|
|
#[test]
|
|
|
|
fn test_process_command() {
|
|
|
|
let leader_keypair = Keypair::new();
|
|
|
|
let leader = Node::new_localhost_with_pubkey(leader_keypair.pubkey());
|
|
|
|
|
|
|
|
let alice = Mint::new(10_000_000);
|
|
|
|
let bank = Bank::new(&alice);
|
|
|
|
let bob_pubkey = Keypair::new().pubkey();
|
|
|
|
let leader_data = leader.info.clone();
|
|
|
|
let leader_data1 = leader.info.clone();
|
|
|
|
let ledger_path = tmp_ledger("thin_client", &alice);
|
|
|
|
|
|
|
|
let mut config = WalletConfig::default();
|
2018-09-20 22:38:59 -07:00
|
|
|
let rpc_port = 12345; // Needs to be distinct known number to not conflict with following test
|
2018-09-14 01:59:09 -07:00
|
|
|
|
|
|
|
let _server = Fullnode::new_with_bank(
|
|
|
|
leader_keypair,
|
|
|
|
bank,
|
|
|
|
0,
|
|
|
|
&[],
|
|
|
|
leader,
|
|
|
|
None,
|
2018-09-14 01:53:18 -07:00
|
|
|
&ledger_path,
|
2018-09-14 01:59:09 -07:00
|
|
|
false,
|
2018-09-17 15:43:23 -07:00
|
|
|
None,
|
2018-09-20 22:38:59 -07:00
|
|
|
Some(rpc_port),
|
2018-09-14 01:59:09 -07:00
|
|
|
);
|
|
|
|
sleep(Duration::from_millis(200));
|
|
|
|
|
|
|
|
let (sender, receiver) = channel();
|
|
|
|
run_local_drone(alice.keypair(), leader_data.contact_info.ncp, sender);
|
|
|
|
config.drone_addr = receiver.recv().unwrap();
|
|
|
|
config.leader = leader_data1;
|
|
|
|
|
2018-09-20 22:27:06 -07:00
|
|
|
let mut rpc_addr = leader_data.contact_info.ncp;
|
2018-09-20 22:38:59 -07:00
|
|
|
rpc_addr.set_port(rpc_port);
|
2018-09-21 14:27:03 -07:00
|
|
|
config.rpc_addr = format!("http://{}", rpc_addr.to_string());
|
2018-09-20 22:27:06 -07:00
|
|
|
|
2018-09-17 12:15:26 -07:00
|
|
|
let tokens = 50;
|
|
|
|
config.command = WalletCommand::AirDrop(tokens);
|
|
|
|
assert_eq!(
|
2018-09-20 22:27:06 -07:00
|
|
|
process_command(&config).unwrap(),
|
2018-09-17 12:15:26 -07:00
|
|
|
format!("Your balance is: {:?}", tokens)
|
|
|
|
);
|
2018-09-14 01:59:09 -07:00
|
|
|
|
|
|
|
config.command = WalletCommand::Balance;
|
2018-09-17 12:15:26 -07:00
|
|
|
assert_eq!(
|
2018-09-20 22:27:06 -07:00
|
|
|
process_command(&config).unwrap(),
|
2018-09-17 12:15:26 -07:00
|
|
|
format!("Your balance is: {:?}", tokens)
|
|
|
|
);
|
2018-09-14 01:59:09 -07:00
|
|
|
|
|
|
|
config.command = WalletCommand::Address;
|
2018-09-17 12:15:26 -07:00
|
|
|
assert_eq!(
|
2018-09-20 22:27:06 -07:00
|
|
|
process_command(&config).unwrap(),
|
2018-09-17 12:15:26 -07:00
|
|
|
format!("{}", config.id.pubkey())
|
|
|
|
);
|
2018-09-14 01:59:09 -07:00
|
|
|
|
|
|
|
config.command = WalletCommand::Pay(10, bob_pubkey);
|
2018-09-20 22:27:06 -07:00
|
|
|
let sig_response = process_command(&config);
|
2018-09-17 12:15:26 -07:00
|
|
|
assert!(sig_response.is_ok());
|
|
|
|
sleep(Duration::from_millis(100));
|
|
|
|
|
|
|
|
let signatures = bs58::decode(sig_response.unwrap())
|
|
|
|
.into_vec()
|
|
|
|
.expect("base58-encoded signature");
|
|
|
|
let signature = Signature::new(&signatures);
|
|
|
|
config.command = WalletCommand::Confirm(signature);
|
2018-09-20 22:27:06 -07:00
|
|
|
assert_eq!(process_command(&config).unwrap(), "Confirmed");
|
2018-09-14 01:59:09 -07:00
|
|
|
|
|
|
|
config.command = WalletCommand::Balance;
|
2018-09-17 12:15:26 -07:00
|
|
|
assert_eq!(
|
2018-09-20 22:27:06 -07:00
|
|
|
process_command(&config).unwrap(),
|
2018-09-17 12:15:26 -07:00
|
|
|
format!("Your balance is: {:?}", tokens - 10)
|
|
|
|
);
|
2018-09-14 01:59:09 -07:00
|
|
|
}
|
|
|
|
#[test]
|
|
|
|
fn test_request_airdrop() {
|
|
|
|
let leader_keypair = Keypair::new();
|
|
|
|
let leader = Node::new_localhost_with_pubkey(leader_keypair.pubkey());
|
|
|
|
|
|
|
|
let alice = Mint::new(10_000_000);
|
|
|
|
let bank = Bank::new(&alice);
|
|
|
|
let bob_pubkey = Keypair::new().pubkey();
|
|
|
|
let leader_data = leader.info.clone();
|
|
|
|
let ledger_path = tmp_ledger("thin_client", &alice);
|
|
|
|
|
2018-09-20 22:38:59 -07:00
|
|
|
let rpc_port = 11111; // Needs to be distinct known number to not conflict with previous test
|
|
|
|
|
2018-09-14 01:59:09 -07:00
|
|
|
let _server = Fullnode::new_with_bank(
|
|
|
|
leader_keypair,
|
|
|
|
bank,
|
|
|
|
0,
|
|
|
|
&[],
|
|
|
|
leader,
|
|
|
|
None,
|
2018-09-14 01:53:18 -07:00
|
|
|
&ledger_path,
|
2018-09-14 01:59:09 -07:00
|
|
|
false,
|
2018-09-17 15:43:23 -07:00
|
|
|
None,
|
2018-09-20 22:38:59 -07:00
|
|
|
Some(rpc_port),
|
2018-09-14 01:59:09 -07:00
|
|
|
);
|
|
|
|
sleep(Duration::from_millis(200));
|
|
|
|
|
|
|
|
let (sender, receiver) = channel();
|
|
|
|
run_local_drone(alice.keypair(), leader_data.contact_info.ncp, sender);
|
|
|
|
let drone_addr = receiver.recv().unwrap();
|
|
|
|
|
2018-09-21 14:27:03 -07:00
|
|
|
let mut addr = leader_data.contact_info.ncp;
|
|
|
|
addr.set_port(rpc_port);
|
|
|
|
let rpc_addr = format!("http://{}", addr.to_string());
|
2018-09-20 22:27:06 -07:00
|
|
|
|
2018-09-14 01:59:09 -07:00
|
|
|
let signature = request_airdrop(&drone_addr, &bob_pubkey, 50);
|
|
|
|
assert!(signature.is_ok());
|
2018-09-21 14:27:03 -07:00
|
|
|
let params = json!(format!("{}", signature.unwrap()));
|
2018-09-20 22:27:06 -07:00
|
|
|
let confirmation = WalletRpcRequest::ConfirmTransaction
|
|
|
|
.make_rpc_request(&rpc_addr, 1, Some(params))
|
|
|
|
.unwrap()
|
|
|
|
.as_bool()
|
|
|
|
.unwrap();
|
|
|
|
assert!(confirmation);
|
2018-09-14 01:59:09 -07:00
|
|
|
}
|
2018-09-17 09:48:04 -07:00
|
|
|
#[test]
|
|
|
|
fn test_gen_keypair_file() {
|
|
|
|
let outfile = "test_gen_keypair_file.json";
|
|
|
|
let serialized_keypair = gen_keypair_file(outfile.to_string()).unwrap();
|
|
|
|
let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
|
|
|
|
assert!(Path::new(outfile).exists());
|
|
|
|
assert_eq!(keypair_vec, read_pkcs8(&outfile).unwrap());
|
|
|
|
assert!(read_keypair(&outfile).is_ok());
|
|
|
|
assert_eq!(
|
|
|
|
read_keypair(&outfile).unwrap().pubkey().as_ref().len(),
|
|
|
|
mem::size_of::<Pubkey>()
|
|
|
|
);
|
|
|
|
fs::remove_file(outfile).unwrap();
|
|
|
|
assert!(!Path::new(outfile).exists());
|
|
|
|
}
|
2018-09-14 01:59:09 -07:00
|
|
|
}
|