mod command;
mod config;
mod interactive;
mod io;
mod menu;
mod settings;
pub(crate) use command::{Command, RunResult};
pub(crate) use menu::Menu;
use clap::Parser;
use std::fs;
use tracing::{warn, Level};
use bip39::{Language, Mnemonic, MnemonicType};
use blake3::Hash;
use crate::command::TransactionHistory;
use crate::settings::{LogFormat, Settings};
#[cfg(not(windows))]
use dusk_wallet::TransportUDS;
use dusk_wallet::{Dusk, SecureWalletFile, TransportTCP, Wallet, WalletPath};
use config::{Config, TransportMethod};
use io::{prompt, status};
use io::{GraphQL, WalletArgs};
#[derive(Debug, Clone)]
pub(crate) struct WalletFile {
path: WalletPath,
pwd: Hash,
}
impl SecureWalletFile for WalletFile {
fn path(&self) -> &WalletPath {
&self.path
}
fn pwd(&self) -> Hash {
self.pwd
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
if let Err(err) = exec().await {
match err.downcast_ref::<requestty::ErrorKind>() {
Some(requestty::ErrorKind::Interrupted) => {
}
_ => eprintln!("{err}"),
};
io::prompt::show_cursor()?;
}
Ok(())
}
async fn connect<F>(
mut wallet: Wallet<F>,
settings: &Settings,
status: fn(&str),
) -> Wallet<F>
where
F: SecureWalletFile + std::fmt::Debug,
{
let con = match (&settings.state.method(), &settings.prover.method()) {
(TransportMethod::Tcp, TransportMethod::Tcp) => {
wallet
.connect_with_status(
TransportTCP::new(&settings.state, &settings.prover),
status,
)
.await
}
#[cfg(not(windows))]
(TransportMethod::Uds, _) => {
wallet
.connect_with_status(TransportUDS::new(&settings.state), status)
.await
}
(_, _) => panic!("IPC method not supported"),
};
if con.is_err() {
warn!("Connection to Rusk Failed, some operations won't be available.");
}
wallet
}
async fn exec() -> anyhow::Result<()> {
let args = WalletArgs::parse();
let cmd = args.command.clone();
#[cfg(windows)]
requestty::symbols::set(requestty::symbols::ASCII);
let settings = Settings::args(args);
let profile_folder = settings.profile().clone();
fs::create_dir_all(&profile_folder)?;
let wallet_path = WalletPath::from(profile_folder.join("wallet.dat"));
let cfg = Config::load(&profile_folder)?;
let settings = settings.network(cfg.network);
WalletPath::set_cache_dir(&profile_folder)?;
let level = &settings.logging.level;
let level: Level = level.into();
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
.with_max_level(level)
.with_writer(std::io::stderr);
match settings.logging.format {
LogFormat::Json => {
let subscriber = subscriber.json().flatten_event(true).finish();
tracing::subscriber::set_global_default(subscriber)?;
}
LogFormat::Plain => {
let subscriber = subscriber.with_ansi(false).finish();
tracing::subscriber::set_global_default(subscriber)?;
}
LogFormat::Coloured => {
let subscriber = subscriber.finish();
tracing::subscriber::set_global_default(subscriber)?;
}
};
let is_headless = cmd.is_some();
let password = &settings.password;
match cmd {
Some(ref cmd) if cmd == &Command::Settings => {
println!("{}", &settings);
return Ok(());
}
_ => {}
};
let mut wallet: Wallet<WalletFile> = match cmd {
Some(ref cmd) => match cmd {
Command::Create { skip_recovery } => {
let mnemonic =
Mnemonic::new(MnemonicType::Words12, Language::English);
let pwd = prompt::create_password(password)?;
if !skip_recovery {
prompt::confirm_recovery_phrase(&mnemonic)?;
}
let mut w = Wallet::new(mnemonic)?;
w.save_to(WalletFile {
path: wallet_path,
pwd,
})?;
w
}
Command::Restore { file } => {
let (mut w, pwd) = match file {
Some(file) => {
let pwd = prompt::request_auth(
"Please enter wallet password",
password,
)?;
let w = Wallet::from_file(WalletFile {
path: file.clone(),
pwd,
})?;
(w, pwd)
}
None => {
let phrase = prompt::request_recovery_phrase()?;
let pwd = prompt::create_password(password)?;
let w = Wallet::new(phrase)?;
(w, pwd)
}
};
w.save_to(WalletFile {
path: wallet_path,
pwd,
})?;
w
}
_ => {
let pwd = prompt::request_auth(
"Please enter wallet password",
password,
)?;
Wallet::from_file(WalletFile {
path: wallet_path,
pwd,
})?
}
},
None => {
interactive::load_wallet(&wallet_path, &settings)?
}
};
let status_cb = match is_headless {
true => status::headless,
false => status::interactive,
};
wallet = connect(wallet, &settings, status_cb).await;
match cmd {
Some(cmd) => match cmd.run(&mut wallet, &settings).await? {
RunResult::Balance(balance, spendable) => {
if spendable {
println!("{}", Dusk::from(balance.spendable));
} else {
println!("{}", Dusk::from(balance.value));
}
}
RunResult::Address(addr) => {
println!("{}", addr);
}
RunResult::Addresses(addrs) => {
for a in addrs {
println!("{}", a);
}
}
RunResult::Tx(hash) => {
let txh = format!("{:x}", hash);
let gql = GraphQL::new(
&settings.graphql.to_string(),
status::headless,
);
gql.wait_for(&txh).await?;
println!("{}", txh);
}
RunResult::StakeInfo(info, reward) => {
if reward {
println!("{}", Dusk::from(info.reward));
} else {
let staked_amount = match info.amount {
Some((staked, ..)) => staked,
None => 0,
};
println!("{}", Dusk::from(staked_amount));
}
}
RunResult::ExportedKeys(pub_key, key_pair) => {
println!("{},{}", pub_key.display(), key_pair.display())
}
RunResult::History(transactions) => {
println!("{}", TransactionHistory::header());
for th in transactions {
println!("{th}");
}
}
RunResult::Settings() => {}
RunResult::Create() | RunResult::Restore() => {}
},
None => {
interactive::run_loop(&mut wallet, &settings).await?;
}
}
Ok(())
}