use clap::Parser;
use jsonrpsee::http_client::HttpClientBuilder;
use parity_scale_codec::{Decode, Encode};
use runtime::{OuterConstraintChecker, OuterVerifier};
use sp_core::H256;
use std::path::PathBuf;
use tuxedo_core::{types::OutputRef, verifier::*};
mod amoeba;
mod cli;
mod keystore;
mod money;
mod parachain;
mod rpc;
mod sync;
mod timestamp;
use cli::{Cli, Command};
use parachain::ParachainConstraintChecker;
const DEFAULT_ENDPOINT: &str = "http://localhost:9944";
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let cli = Cli::parse();
let tmp = cli.tmp || cli.dev;
let data_path = match tmp {
true => temp_dir(),
_ => cli.base_path.unwrap_or_else(default_data_path),
};
let keystore_path = data_path.join("keystore");
let db_path = data_path.join("wallet_database");
let keystore = sc_keystore::LocalKeystore::open(keystore_path.clone(), None)?;
if cli.dev {
crate::keystore::insert_development_key_for_this_session(&keystore)?;
}
let client = HttpClientBuilder::default().build(cli.endpoint)?;
let metadata = rpc::node_get_metadata(&client).await?;
let node_genesis_hash = rpc::node_get_block_hash(0, &client)
.await?
.expect("node should be able to return some genesis hash");
let node_genesis_block = rpc::node_get_block(node_genesis_hash, &client)
.await?
.expect("node should be able to return some genesis block");
log::debug!("Node's Genesis block::{:?}", node_genesis_hash);
let db = sync::open_db(db_path, node_genesis_hash, node_genesis_block.clone())?;
let num_blocks =
sync::height(&db)?.expect("db should be initialized automatically when opening.");
log::info!("Number of blocks in the db: {num_blocks}");
let keystore_filter = |v: &OuterVerifier| -> bool {
matches![v,
OuterVerifier::Sr25519Signature(Sr25519Signature { owner_pubkey })
if crate::keystore::has_key(&keystore, owner_pubkey)
]
};
if !sled::Db::was_recovered(&db) {
if metadata.is_parachain() {
sync::apply_block::<_, ParachainConstraintChecker>(
&db,
node_genesis_block,
node_genesis_hash,
&keystore_filter,
)
.await?;
} else {
sync::apply_block::<_, OuterConstraintChecker>(
&db,
node_genesis_block,
node_genesis_hash,
&keystore_filter,
)
.await?;
}
}
if cli.no_sync {
log::warn!("Skipping sync with node. Using previously synced information.")
} else {
sync::synchronize(metadata.is_parachain(), &db, &client, &keystore_filter).await?;
log::info!(
"Wallet database synchronized with node to height {:?}",
sync::height(&db)?.expect("We just synced, so there is a height available")
);
}
match cli.command {
Some(Command::AmoebaDemo) => amoeba::amoeba_demo(metadata.is_parachain(), &client).await,
Some(Command::MintCoins(args)) => {
money::mint_coins(metadata.is_parachain(), &client, args).await
}
Some(Command::VerifyCoin { output_ref }) => {
println!("Details of coin {}:", hex::encode(output_ref.encode()));
let (coin_from_storage, verifier_from_storage) =
money::get_coin_from_storage(&output_ref, &client).await?;
print!("Found in storage. Value: {}, ", coin_from_storage.0);
pretty_print_verifier(&verifier_from_storage);
match sync::get_unspent(&db, &output_ref)? {
Some((owner, amount)) => {
println!("Found in local db. Value: {amount}, owned by {owner}");
}
None => {
println!("Not found in local db");
}
}
Ok(())
}
Some(Command::SpendCoins(args)) => {
money::spend_coins(metadata.is_parachain(), &db, &client, &keystore, args).await
}
Some(Command::InsertKey { seed }) => crate::keystore::insert_key(&keystore, &seed),
Some(Command::GenerateKey { password }) => {
crate::keystore::generate_key(&keystore, password)?;
Ok(())
}
Some(Command::ShowKeys) => {
crate::keystore::get_keys(&keystore)?.for_each(|pubkey| {
println!("key: 0x{}", hex::encode(pubkey));
});
Ok(())
}
Some(Command::RemoveKey { pub_key }) => {
println!("CAUTION!!! About permanently remove {pub_key}. This action CANNOT BE REVERSED. Type \"proceed\" to confirm deletion.");
let mut confirmation = String::new();
std::io::stdin()
.read_line(&mut confirmation)
.expect("Failed to read line");
if confirmation.trim() == "proceed" {
crate::keystore::remove_key(&keystore_path, &pub_key)
} else {
println!("Deletion aborted. That was close.");
Ok(())
}
}
Some(Command::ShowBalance) => {
println!("Balance Summary");
let mut total = 0;
let balances = sync::get_balances(&db)?;
for (account, balance) in balances {
total += balance;
println!("{account}: {balance}");
}
println!("--------------------");
println!("total : {total}");
Ok(())
}
Some(Command::ShowAllOutputs) => {
println!("###### Unspent outputs ###########");
sync::print_unspent_tree(&db)?;
Ok(())
}
Some(Command::ShowTimestamp) => {
println!("Timestamp: {}", timestamp::get_timestamp(&db)?);
Ok(())
}
None => {
log::info!("No Wallet Command invoked. Exiting.");
Ok(())
}
}?;
if tmp {
std::fs::remove_dir_all(data_path.clone()).map_err(|e| {
log::warn!(
"Unable to remove temporary data directory at {}\nPlease remove it manually.",
data_path.to_string_lossy()
);
e
})?;
}
Ok(())
}
pub(crate) fn h256_from_string(s: &str) -> anyhow::Result<H256> {
let s = strip_0x_prefix(s);
let mut bytes: [u8; 32] = [0; 32];
hex::decode_to_slice(s, &mut bytes as &mut [u8])
.map_err(|_| clap::Error::new(clap::error::ErrorKind::ValueValidation))?;
Ok(H256::from(bytes))
}
fn output_ref_from_string(s: &str) -> Result<OutputRef, clap::Error> {
let s = strip_0x_prefix(s);
let bytes =
hex::decode(s).map_err(|_| clap::Error::new(clap::error::ErrorKind::ValueValidation))?;
OutputRef::decode(&mut &bytes[..])
.map_err(|_| clap::Error::new(clap::error::ErrorKind::ValueValidation))
}
fn strip_0x_prefix(s: &str) -> &str {
if &s[..2] == "0x" {
&s[2..]
} else {
s
}
}
fn temp_dir() -> PathBuf {
std::env::temp_dir().join(format!(
"tuxedo-wallet-{}",
std::time::UNIX_EPOCH.elapsed().unwrap().as_millis(),
))
}
fn default_data_path() -> PathBuf {
let qualifier = "";
let organization = "";
let application = env!("CARGO_PKG_NAME");
directories::ProjectDirs::from(qualifier, organization, application)
.expect("app directories exist on all supported platforms; qed")
.data_dir()
.into()
}
pub fn pretty_print_verifier(v: &OuterVerifier) {
match v {
OuterVerifier::Sr25519Signature(sr25519_signature) => {
println! {"owned by {}", sr25519_signature.owner_pubkey}
}
OuterVerifier::UpForGrabs(_) => println!("that can be spent by anyone"),
OuterVerifier::ThresholdMultiSignature(multi_sig) => {
let string_sigs: Vec<_> = multi_sig
.signatories
.iter()
.map(|sig| format!("0x{}", hex::encode(sig)))
.collect();
println!(
"Owned by {:?}, with a threshold of {} sigs necessary",
string_sigs, multi_sig.threshold
);
}
}
}