mod history;
use clap::Subcommand;
use dusk_bls12_381_sign::PublicKey;
use dusk_bytes::DeserializableSlice;
use dusk_jubjub::BlsScalar;
use std::{fmt, path::PathBuf};
use crate::io::prompt;
use crate::settings::Settings;
use crate::{WalletFile, WalletPath};
use dusk_wallet::gas::Gas;
use dusk_wallet::{Address, Dusk, Lux, Wallet, EPOCH};
use dusk_wallet_core::{BalanceInfo, StakeInfo};
pub use history::TransactionHistory;
pub const DEFAULT_STAKE_GAS_LIMIT: u64 = 2_900_000_000;
#[allow(clippy::large_enum_variant)]
#[derive(PartialEq, Eq, Hash, Clone, Subcommand, Debug)]
pub(crate) enum Command {
Create {
#[clap(long, action)]
skip_recovery: bool,
},
Restore {
#[clap(short, long)]
file: Option<WalletPath>,
},
Balance {
#[clap(short, long)]
addr: Option<Address>,
#[clap(long)]
spendable: bool,
},
Addresses {
#[clap(short, long, action)]
new: bool,
},
History {
#[clap(short, long)]
addr: Option<Address>,
},
Transfer {
#[clap(short, long)]
sndr: Option<Address>,
#[clap(short, long)]
rcvr: Address,
#[clap(short, long)]
amt: Dusk,
#[clap(short = 'l', long)]
gas_limit: Option<u64>,
#[clap(short = 'p', long)]
gas_price: Option<Lux>,
},
Stake {
#[clap(short, long)]
addr: Option<Address>,
#[clap(short, long)]
amt: Dusk,
#[clap(short = 'l', long)]
gas_limit: Option<u64>,
#[clap(short = 'p', long)]
gas_price: Option<Lux>,
},
StakeInfo {
#[clap(short, long)]
addr: Option<Address>,
#[clap(long, action)]
reward: bool,
},
StakeAllow {
#[clap(short, long)]
addr: Option<Address>,
#[clap(short, long)]
key: String,
#[clap(short = 'l', long)]
gas_limit: Option<u64>,
#[clap(short = 'p', long)]
gas_price: Option<Lux>,
},
Unstake {
#[clap(short, long)]
addr: Option<Address>,
#[clap(short = 'l', long)]
gas_limit: Option<u64>,
#[clap(short = 'p', long)]
gas_price: Option<Lux>,
},
Withdraw {
#[clap(short, long)]
addr: Option<Address>,
#[clap(short = 'l', long)]
gas_limit: Option<u64>,
#[clap(short = 'p', long)]
gas_price: Option<Lux>,
},
Export {
#[clap(short, long)]
addr: Option<Address>,
#[clap(short, long)]
dir: PathBuf,
},
Settings,
}
impl Command {
pub async fn run(
self,
wallet: &mut Wallet<WalletFile>,
settings: &Settings,
) -> anyhow::Result<RunResult> {
match self {
Command::Balance { addr, spendable } => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let balance = wallet.get_balance(addr).await?;
Ok(RunResult::Balance(balance, spendable))
}
Command::Addresses { new } => {
if new {
let addr = wallet.new_address().clone();
wallet.save()?;
Ok(RunResult::Address(Box::new(addr)))
} else {
Ok(RunResult::Addresses(wallet.addresses().clone()))
}
}
Command::Transfer {
sndr,
rcvr,
amt,
gas_limit,
gas_price,
} => {
let sender = match sndr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let mut gas = Gas::default();
gas.set_price(gas_price);
gas.set_limit(gas_limit);
let tx = wallet.transfer(sender, &rcvr, amt, gas).await?;
Ok(RunResult::Tx(tx.hash()))
}
Command::Stake {
addr,
amt,
gas_limit,
gas_price,
} => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let mut gas = Gas::new(DEFAULT_STAKE_GAS_LIMIT);
gas.set_price(gas_price);
gas.set_limit(gas_limit);
let tx = wallet.stake(addr, amt, gas).await?;
Ok(RunResult::Tx(tx.hash()))
}
Command::StakeAllow {
addr,
key,
gas_limit,
gas_price,
} => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let mut gas = Gas::new(DEFAULT_STAKE_GAS_LIMIT);
gas.set_price(gas_price);
gas.set_limit(gas_limit);
let key_data = bs58::decode(key).into_vec()?;
let key = PublicKey::from_slice(&key_data[..])
.map_err(dusk_wallet::Error::from)?;
let tx = wallet.stake_allow(addr, &key, gas).await?;
Ok(RunResult::Tx(tx.hash()))
}
Command::StakeInfo { addr, reward } => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let si = wallet.stake_info(addr).await?;
Ok(RunResult::StakeInfo(si, reward))
}
Command::Unstake {
addr,
gas_limit,
gas_price,
} => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let mut gas = Gas::new(DEFAULT_STAKE_GAS_LIMIT);
gas.set_price(gas_price);
gas.set_limit(gas_limit);
let tx = wallet.unstake(addr, gas).await?;
Ok(RunResult::Tx(tx.hash()))
}
Command::Withdraw {
addr,
gas_limit,
gas_price,
} => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let mut gas = Gas::new(DEFAULT_STAKE_GAS_LIMIT);
gas.set_price(gas_price);
gas.set_limit(gas_limit);
let tx = wallet.withdraw_reward(addr, gas).await?;
Ok(RunResult::Tx(tx.hash()))
}
Command::Export { addr, dir } => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let pwd = prompt::request_auth(
"Encryption password",
&settings.password,
)?;
let (pub_key, key_pair) =
wallet.export_keys(addr, &dir, pwd)?;
Ok(RunResult::ExportedKeys(pub_key, key_pair))
}
Command::History { addr } => {
let addr = match addr {
Some(addr) => wallet.claim_as_address(addr)?,
None => wallet.default_address(),
};
let notes = wallet.get_all_notes(addr)?;
let transactions =
history::transaction_from_notes(settings, notes).await?;
Ok(RunResult::History(transactions))
}
Command::Create { .. } => Ok(RunResult::Create()),
Command::Restore { .. } => Ok(RunResult::Restore()),
Command::Settings => Ok(RunResult::Settings()),
}
}
}
pub enum RunResult {
Tx(BlsScalar),
Balance(BalanceInfo, bool),
StakeInfo(StakeInfo, bool),
Address(Box<Address>),
Addresses(Vec<Address>),
ExportedKeys(PathBuf, PathBuf),
Create(),
Restore(),
Settings(),
History(Vec<TransactionHistory>),
}
impl fmt::Display for RunResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use RunResult::*;
match self {
Balance(balance, _) => {
write!(
f,
"> Total balance is: {} DUSK\n> Maximum spendable per TX is: {} DUSK",
Dusk::from(balance.value),
Dusk::from(balance.spendable)
)
}
Address(addr) => {
write!(f, "> {}", addr)
}
Addresses(addrs) => {
let str_addrs = addrs
.iter()
.map(|a| format!("{}", a))
.collect::<Vec<String>>()
.join("\n>");
write!(f, "> {}", str_addrs)
}
Tx(hash) => {
write!(f, "> Transaction sent: {:x}", hash)
}
StakeInfo(si, _) => {
let stake_str = match si.amount {
Some((value, eligibility)) => format!(
"Current stake amount is: {} DUSK\n> Stake eligibility from block #{} (Epoch {})",
Dusk::from(value),
eligibility,
eligibility / EPOCH
),
None => "No active stake found for this key".to_string(),
};
write!(
f,
"> {}\n> Accumulated reward is: {} DUSK",
stake_str,
Dusk::from(si.reward)
)
}
ExportedKeys(pk, kp) => {
write!(
f,
"> Public key exported to: {}\n> Key pair exported to: {}",
pk.display(),
kp.display()
)
}
History(transactions) => {
writeln!(f, "{}", TransactionHistory::header())?;
for th in transactions {
writeln!(f, "{th}")?;
}
Ok(())
}
Create() | Restore() | Settings() => unreachable!(),
}
}
}