[wasm] Fix SystemTime for wasm and refactor the cli part
This commit is contained in:
		
							parent
							
								
									e5ff696e73
								
							
						
					
					
						commit
						1ff9852cff
					
				@ -18,6 +18,7 @@ sled = { version = "0.31.0", optional = true }
 | 
			
		||||
electrum-client = { git = "https://github.com/MagicalBitcoin/rust-electrum-client.git", optional = true }
 | 
			
		||||
reqwest = { version = "0.10", optional = true, features = ["json"] }
 | 
			
		||||
futures = { version = "0.3", optional = true }
 | 
			
		||||
clap = { version = "2.33", optional = true }
 | 
			
		||||
 | 
			
		||||
[features]
 | 
			
		||||
minimal = []
 | 
			
		||||
@ -26,18 +27,19 @@ default = ["key-value-db", "electrum"]
 | 
			
		||||
electrum = ["electrum-client"]
 | 
			
		||||
esplora = ["reqwest", "futures"]
 | 
			
		||||
key-value-db = ["sled"]
 | 
			
		||||
cli-utils = ["clap"]
 | 
			
		||||
 | 
			
		||||
[dev-dependencies]
 | 
			
		||||
tokio = { version = "0.2", features = ["macros"] }
 | 
			
		||||
lazy_static = "1.4"
 | 
			
		||||
rustyline = "5.0" # newer version requires 2018 edition
 | 
			
		||||
clap = "2.33"
 | 
			
		||||
rustyline = "6.0"
 | 
			
		||||
dirs = "2.0"
 | 
			
		||||
env_logger = "0.7"
 | 
			
		||||
rand = "0.7"
 | 
			
		||||
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "repl"
 | 
			
		||||
required-features = ["cli-utils"]
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "psbt"
 | 
			
		||||
[[example]]
 | 
			
		||||
@ -52,4 +54,5 @@ required-features = ["compiler"]
 | 
			
		||||
[[example]]
 | 
			
		||||
name = "magic"
 | 
			
		||||
path = "examples/repl.rs"
 | 
			
		||||
required-features = ["cli-utils"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										297
									
								
								examples/repl.rs
									
									
									
									
									
								
							
							
						
						
									
										297
									
								
								examples/repl.rs
									
									
									
									
									
								
							@ -1,32 +1,21 @@
 | 
			
		||||
extern crate base64;
 | 
			
		||||
extern crate clap;
 | 
			
		||||
extern crate dirs;
 | 
			
		||||
extern crate env_logger;
 | 
			
		||||
extern crate log;
 | 
			
		||||
extern crate magical_bitcoin_wallet;
 | 
			
		||||
extern crate rustyline;
 | 
			
		||||
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
 | 
			
		||||
 | 
			
		||||
use rustyline::error::ReadlineError;
 | 
			
		||||
use rustyline::Editor;
 | 
			
		||||
 | 
			
		||||
use clap::AppSettings;
 | 
			
		||||
 | 
			
		||||
#[allow(unused_imports)]
 | 
			
		||||
use log::{debug, error, info, trace, LevelFilter};
 | 
			
		||||
 | 
			
		||||
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
 | 
			
		||||
use bitcoin::util::psbt::PartiallySignedTransaction;
 | 
			
		||||
use bitcoin::{Address, Network, OutPoint};
 | 
			
		||||
use bitcoin::Network;
 | 
			
		||||
 | 
			
		||||
use magical_bitcoin_wallet::bitcoin;
 | 
			
		||||
use magical_bitcoin_wallet::blockchain::ElectrumBlockchain;
 | 
			
		||||
use magical_bitcoin_wallet::cli;
 | 
			
		||||
use magical_bitcoin_wallet::sled;
 | 
			
		||||
use magical_bitcoin_wallet::types::ScriptType;
 | 
			
		||||
use magical_bitcoin_wallet::{Client, Wallet};
 | 
			
		||||
 | 
			
		||||
fn prepare_home_dir() -> PathBuf {
 | 
			
		||||
@ -43,204 +32,14 @@ fn prepare_home_dir() -> PathBuf {
 | 
			
		||||
    dir
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parse_addressee(s: &str) -> Result<(Address, u64), String> {
 | 
			
		||||
    let parts: Vec<_> = s.split(":").collect();
 | 
			
		||||
    if parts.len() != 2 {
 | 
			
		||||
        return Err("Invalid format".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let addr = Address::from_str(&parts[0]);
 | 
			
		||||
    if let Err(e) = addr {
 | 
			
		||||
        return Err(format!("{:?}", e));
 | 
			
		||||
    }
 | 
			
		||||
    let val = u64::from_str(&parts[1]);
 | 
			
		||||
    if let Err(e) = val {
 | 
			
		||||
        return Err(format!("{:?}", e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok((addr.unwrap(), val.unwrap()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
 | 
			
		||||
    OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn addressee_validator(s: String) -> Result<(), String> {
 | 
			
		||||
    parse_addressee(&s).map(|_| ())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn outpoint_validator(s: String) -> Result<(), String> {
 | 
			
		||||
    parse_outpoint(&s).map(|_| ())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    env_logger::init();
 | 
			
		||||
 | 
			
		||||
    let app = App::new("Magical Bitcoin Wallet")
 | 
			
		||||
        .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"))
 | 
			
		||||
        .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or(""))
 | 
			
		||||
        .about("A modern, lightweight, descriptor-based wallet")
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("get_new_address").about("Generates a new external address"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server"))
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("get_balance").about("Returns the current wallet balance"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("create_tx")
 | 
			
		||||
                .about("Creates a new unsigned tranasaction")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("to")
 | 
			
		||||
                        .long("to")
 | 
			
		||||
                        .value_name("ADDRESS:SAT")
 | 
			
		||||
                        .help("Adds an addressee to the transaction")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(addressee_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("send_all")
 | 
			
		||||
                        .short("all")
 | 
			
		||||
                        .long("send_all")
 | 
			
		||||
                        .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("utxos")
 | 
			
		||||
                        .long("utxos")
 | 
			
		||||
                        .value_name("TXID:VOUT")
 | 
			
		||||
                        .help("Selects which utxos *must* be spent")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(outpoint_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("unspendable")
 | 
			
		||||
                        .long("unspendable")
 | 
			
		||||
                        .value_name("TXID:VOUT")
 | 
			
		||||
                        .help("Marks an utxo as unspendable")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(outpoint_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("fee_rate")
 | 
			
		||||
                        .short("fee")
 | 
			
		||||
                        .long("fee_rate")
 | 
			
		||||
                        .value_name("SATS_VBYTE")
 | 
			
		||||
                        .help("Fee rate to use in sat/vbyte")
 | 
			
		||||
                        .takes_value(true),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("policy")
 | 
			
		||||
                        .long("policy")
 | 
			
		||||
                        .value_name("POLICY")
 | 
			
		||||
                        .help("Selects which policy will be used to satisfy the descriptor")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1),
 | 
			
		||||
                ),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("policies")
 | 
			
		||||
                .about("Returns the available spending policies for the descriptor")
 | 
			
		||||
            )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("sign")
 | 
			
		||||
                .about("Signs and tries to finalize a PSBT")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("psbt")
 | 
			
		||||
                        .long("psbt")
 | 
			
		||||
                        .value_name("BASE64_PSBT")
 | 
			
		||||
                        .help("Sets the PSBT to sign")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("assume_height")
 | 
			
		||||
                        .long("assume_height")
 | 
			
		||||
                        .value_name("HEIGHT")
 | 
			
		||||
                        .help("Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(false),
 | 
			
		||||
                ))
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("broadcast")
 | 
			
		||||
                .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("psbt")
 | 
			
		||||
                        .long("psbt")
 | 
			
		||||
                        .value_name("BASE64_PSBT")
 | 
			
		||||
                        .help("Sets the PSBT to broadcast")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true),
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
    let app = cli::make_cli_subcommands();
 | 
			
		||||
    let mut repl_app = app.clone().setting(AppSettings::NoBinaryName);
 | 
			
		||||
 | 
			
		||||
    let app = app
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("network")
 | 
			
		||||
                .short("n")
 | 
			
		||||
                .long("network")
 | 
			
		||||
                .value_name("NETWORK")
 | 
			
		||||
                .help("Sets the network")
 | 
			
		||||
                .takes_value(true)
 | 
			
		||||
                .default_value("testnet")
 | 
			
		||||
                .possible_values(&["testnet", "regtest"]),
 | 
			
		||||
        )
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("wallet")
 | 
			
		||||
                .short("w")
 | 
			
		||||
                .long("wallet")
 | 
			
		||||
                .value_name("WALLET_NAME")
 | 
			
		||||
                .help("Selects the wallet to use")
 | 
			
		||||
                .takes_value(true)
 | 
			
		||||
                .default_value("main"),
 | 
			
		||||
        )
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("server")
 | 
			
		||||
                .short("s")
 | 
			
		||||
                .long("server")
 | 
			
		||||
                .value_name("SERVER:PORT")
 | 
			
		||||
                .help("Sets the Electrum server to use")
 | 
			
		||||
                .takes_value(true)
 | 
			
		||||
                .default_value("tn.not.fyi:55001"),
 | 
			
		||||
        )
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("descriptor")
 | 
			
		||||
                .short("d")
 | 
			
		||||
                .long("descriptor")
 | 
			
		||||
                .value_name("DESCRIPTOR")
 | 
			
		||||
                .help("Sets the descriptor to use for the external addresses")
 | 
			
		||||
                .required(true)
 | 
			
		||||
                .takes_value(true),
 | 
			
		||||
        )
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("change_descriptor")
 | 
			
		||||
                .short("c")
 | 
			
		||||
                .long("change_descriptor")
 | 
			
		||||
                .value_name("DESCRIPTOR")
 | 
			
		||||
                .help("Sets the descriptor to use for internal addresses")
 | 
			
		||||
                .takes_value(true),
 | 
			
		||||
        )
 | 
			
		||||
        .arg(
 | 
			
		||||
            Arg::with_name("v")
 | 
			
		||||
                .short("v")
 | 
			
		||||
                .multiple(true)
 | 
			
		||||
                .help("Sets the level of verbosity"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(SubCommand::with_name("repl").about("Opens an interactive shell"));
 | 
			
		||||
    let app = cli::add_global_flags(app);
 | 
			
		||||
 | 
			
		||||
    let matches = app.get_matches();
 | 
			
		||||
 | 
			
		||||
@ -280,86 +79,6 @@ async fn main() {
 | 
			
		||||
    .unwrap();
 | 
			
		||||
    let wallet = Arc::new(wallet);
 | 
			
		||||
 | 
			
		||||
    // TODO: print errors in a nice way
 | 
			
		||||
    async fn handle_matches<C, D>(wallet: Arc<Wallet<C, D>>, matches: ArgMatches<'_>)
 | 
			
		||||
    where
 | 
			
		||||
        C: magical_bitcoin_wallet::blockchain::OnlineBlockchain,
 | 
			
		||||
        D: magical_bitcoin_wallet::database::BatchDatabase,
 | 
			
		||||
    {
 | 
			
		||||
        if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") {
 | 
			
		||||
            println!("{}", wallet.get_new_address().unwrap().to_string());
 | 
			
		||||
        } else if let Some(_sub_matches) = matches.subcommand_matches("sync") {
 | 
			
		||||
            wallet.sync(None, None).await.unwrap();
 | 
			
		||||
        } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") {
 | 
			
		||||
            for utxo in wallet.list_unspent().unwrap() {
 | 
			
		||||
                println!("{} value {} SAT", utxo.outpoint, utxo.txout.value);
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") {
 | 
			
		||||
            println!("{} SAT", wallet.get_balance().unwrap());
 | 
			
		||||
        } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") {
 | 
			
		||||
            let addressees = sub_matches
 | 
			
		||||
                .values_of("to")
 | 
			
		||||
                .unwrap()
 | 
			
		||||
                .map(|s| parse_addressee(s).unwrap())
 | 
			
		||||
                .collect();
 | 
			
		||||
            let send_all = sub_matches.is_present("send_all");
 | 
			
		||||
            let fee_rate = sub_matches
 | 
			
		||||
                .value_of("fee_rate")
 | 
			
		||||
                .map(|s| f32::from_str(s).unwrap())
 | 
			
		||||
                .unwrap_or(1.0);
 | 
			
		||||
            let utxos = sub_matches
 | 
			
		||||
                .values_of("utxos")
 | 
			
		||||
                .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
 | 
			
		||||
            let unspendable = sub_matches
 | 
			
		||||
                .values_of("unspendable")
 | 
			
		||||
                .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
 | 
			
		||||
            let policy: Option<Vec<_>> = sub_matches
 | 
			
		||||
                .value_of("policy")
 | 
			
		||||
                .map(|s| serde_json::from_str::<Vec<Vec<usize>>>(&s).unwrap());
 | 
			
		||||
 | 
			
		||||
            let result = wallet
 | 
			
		||||
                .create_tx(
 | 
			
		||||
                    addressees,
 | 
			
		||||
                    send_all,
 | 
			
		||||
                    fee_rate * 1e-5,
 | 
			
		||||
                    policy,
 | 
			
		||||
                    utxos,
 | 
			
		||||
                    unspendable,
 | 
			
		||||
                )
 | 
			
		||||
                .unwrap();
 | 
			
		||||
            println!("{:#?}", result.1);
 | 
			
		||||
            println!("PSBT: {}", base64::encode(&serialize(&result.0)));
 | 
			
		||||
        } else if let Some(_sub_matches) = matches.subcommand_matches("policies") {
 | 
			
		||||
            println!(
 | 
			
		||||
                "External: {}",
 | 
			
		||||
                serde_json::to_string(&wallet.policies(ScriptType::External).unwrap()).unwrap()
 | 
			
		||||
            );
 | 
			
		||||
            println!(
 | 
			
		||||
                "Internal: {}",
 | 
			
		||||
                serde_json::to_string(&wallet.policies(ScriptType::Internal).unwrap()).unwrap()
 | 
			
		||||
            );
 | 
			
		||||
        } else if let Some(sub_matches) = matches.subcommand_matches("sign") {
 | 
			
		||||
            let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
 | 
			
		||||
            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
 | 
			
		||||
            let assume_height = sub_matches
 | 
			
		||||
                .value_of("assume_height")
 | 
			
		||||
                .and_then(|s| Some(s.parse().unwrap()));
 | 
			
		||||
            let (psbt, finalized) = wallet.sign(psbt, assume_height).unwrap();
 | 
			
		||||
 | 
			
		||||
            println!("PSBT: {}", base64::encode(&serialize(&psbt)));
 | 
			
		||||
            println!("Finalized: {}", finalized);
 | 
			
		||||
            if finalized {
 | 
			
		||||
                println!("Extracted: {}", serialize_hex(&psbt.extract_tx()));
 | 
			
		||||
            }
 | 
			
		||||
        } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
 | 
			
		||||
            let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
 | 
			
		||||
            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
 | 
			
		||||
            let (txid, _) = wallet.broadcast(psbt).await.unwrap();
 | 
			
		||||
 | 
			
		||||
            println!("TXID: {}", txid);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if let Some(_sub_matches) = matches.subcommand_matches("repl") {
 | 
			
		||||
        let mut rl = Editor::<()>::new();
 | 
			
		||||
 | 
			
		||||
@ -382,7 +101,7 @@ async fn main() {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    handle_matches(Arc::clone(&wallet), matches.unwrap()).await;
 | 
			
		||||
                    cli::handle_matches(&Arc::clone(&wallet), matches.unwrap()).await;
 | 
			
		||||
                }
 | 
			
		||||
                Err(ReadlineError::Interrupted) => continue,
 | 
			
		||||
                Err(ReadlineError::Eof) => break,
 | 
			
		||||
@ -395,6 +114,6 @@ async fn main() {
 | 
			
		||||
 | 
			
		||||
    // rl.save_history("history.txt").unwrap();
 | 
			
		||||
    } else {
 | 
			
		||||
        handle_matches(wallet, matches).await;
 | 
			
		||||
        cli::handle_matches(&wallet, matches).await;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										300
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,300 @@
 | 
			
		||||
use std::str::FromStr;
 | 
			
		||||
 | 
			
		||||
use clap::{App, Arg, ArgMatches, SubCommand};
 | 
			
		||||
 | 
			
		||||
#[allow(unused_imports)]
 | 
			
		||||
use log::{debug, error, info, trace, LevelFilter};
 | 
			
		||||
 | 
			
		||||
use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
 | 
			
		||||
use bitcoin::util::psbt::PartiallySignedTransaction;
 | 
			
		||||
use bitcoin::{Address, OutPoint};
 | 
			
		||||
 | 
			
		||||
use crate::error::Error;
 | 
			
		||||
use crate::types::ScriptType;
 | 
			
		||||
use crate::Wallet;
 | 
			
		||||
 | 
			
		||||
fn parse_addressee(s: &str) -> Result<(Address, u64), String> {
 | 
			
		||||
    let parts: Vec<_> = s.split(":").collect();
 | 
			
		||||
    if parts.len() != 2 {
 | 
			
		||||
        return Err("Invalid format".to_string());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let addr = Address::from_str(&parts[0]);
 | 
			
		||||
    if let Err(e) = addr {
 | 
			
		||||
        return Err(format!("{:?}", e));
 | 
			
		||||
    }
 | 
			
		||||
    let val = u64::from_str(&parts[1]);
 | 
			
		||||
    if let Err(e) = val {
 | 
			
		||||
        return Err(format!("{:?}", e));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok((addr.unwrap(), val.unwrap()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
 | 
			
		||||
    OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn addressee_validator(s: String) -> Result<(), String> {
 | 
			
		||||
    parse_addressee(&s).map(|_| ())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn outpoint_validator(s: String) -> Result<(), String> {
 | 
			
		||||
    parse_outpoint(&s).map(|_| ())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
 | 
			
		||||
    App::new("Magical Bitcoin Wallet")
 | 
			
		||||
        .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"))
 | 
			
		||||
        .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or(""))
 | 
			
		||||
        .about("A modern, lightweight, descriptor-based wallet")
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("get_new_address").about("Generates a new external address"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server"))
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("get_balance").about("Returns the current wallet balance"),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("create_tx")
 | 
			
		||||
                .about("Creates a new unsigned tranasaction")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("to")
 | 
			
		||||
                        .long("to")
 | 
			
		||||
                        .value_name("ADDRESS:SAT")
 | 
			
		||||
                        .help("Adds an addressee to the transaction")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(addressee_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("send_all")
 | 
			
		||||
                        .short("all")
 | 
			
		||||
                        .long("send_all")
 | 
			
		||||
                        .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("utxos")
 | 
			
		||||
                        .long("utxos")
 | 
			
		||||
                        .value_name("TXID:VOUT")
 | 
			
		||||
                        .help("Selects which utxos *must* be spent")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(outpoint_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("unspendable")
 | 
			
		||||
                        .long("unspendable")
 | 
			
		||||
                        .value_name("TXID:VOUT")
 | 
			
		||||
                        .help("Marks an utxo as unspendable")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .multiple(true)
 | 
			
		||||
                        .validator(outpoint_validator),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("fee_rate")
 | 
			
		||||
                        .short("fee")
 | 
			
		||||
                        .long("fee_rate")
 | 
			
		||||
                        .value_name("SATS_VBYTE")
 | 
			
		||||
                        .help("Fee rate to use in sat/vbyte")
 | 
			
		||||
                        .takes_value(true),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("policy")
 | 
			
		||||
                        .long("policy")
 | 
			
		||||
                        .value_name("POLICY")
 | 
			
		||||
                        .help("Selects which policy will be used to satisfy the descriptor")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1),
 | 
			
		||||
                ),
 | 
			
		||||
        )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("policies")
 | 
			
		||||
                .about("Returns the available spending policies for the descriptor")
 | 
			
		||||
            )
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("sign")
 | 
			
		||||
                .about("Signs and tries to finalize a PSBT")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("psbt")
 | 
			
		||||
                        .long("psbt")
 | 
			
		||||
                        .value_name("BASE64_PSBT")
 | 
			
		||||
                        .help("Sets the PSBT to sign")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true),
 | 
			
		||||
                )
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("assume_height")
 | 
			
		||||
                        .long("assume_height")
 | 
			
		||||
                        .value_name("HEIGHT")
 | 
			
		||||
                        .help("Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(false),
 | 
			
		||||
                ))
 | 
			
		||||
        .subcommand(
 | 
			
		||||
            SubCommand::with_name("broadcast")
 | 
			
		||||
                .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network")
 | 
			
		||||
                .arg(
 | 
			
		||||
                    Arg::with_name("psbt")
 | 
			
		||||
                        .long("psbt")
 | 
			
		||||
                        .value_name("BASE64_PSBT")
 | 
			
		||||
                        .help("Sets the PSBT to broadcast")
 | 
			
		||||
                        .takes_value(true)
 | 
			
		||||
                        .number_of_values(1)
 | 
			
		||||
                        .required(true),
 | 
			
		||||
                ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
 | 
			
		||||
    app.arg(
 | 
			
		||||
        Arg::with_name("network")
 | 
			
		||||
            .short("n")
 | 
			
		||||
            .long("network")
 | 
			
		||||
            .value_name("NETWORK")
 | 
			
		||||
            .help("Sets the network")
 | 
			
		||||
            .takes_value(true)
 | 
			
		||||
            .default_value("testnet")
 | 
			
		||||
            .possible_values(&["testnet", "regtest"]),
 | 
			
		||||
    )
 | 
			
		||||
    .arg(
 | 
			
		||||
        Arg::with_name("wallet")
 | 
			
		||||
            .short("w")
 | 
			
		||||
            .long("wallet")
 | 
			
		||||
            .value_name("WALLET_NAME")
 | 
			
		||||
            .help("Selects the wallet to use")
 | 
			
		||||
            .takes_value(true)
 | 
			
		||||
            .default_value("main"),
 | 
			
		||||
    )
 | 
			
		||||
    .arg(
 | 
			
		||||
        Arg::with_name("server")
 | 
			
		||||
            .short("s")
 | 
			
		||||
            .long("server")
 | 
			
		||||
            .value_name("SERVER:PORT")
 | 
			
		||||
            .help("Sets the Electrum server to use")
 | 
			
		||||
            .takes_value(true)
 | 
			
		||||
            .default_value("tn.not.fyi:55001"),
 | 
			
		||||
    )
 | 
			
		||||
    .arg(
 | 
			
		||||
        Arg::with_name("descriptor")
 | 
			
		||||
            .short("d")
 | 
			
		||||
            .long("descriptor")
 | 
			
		||||
            .value_name("DESCRIPTOR")
 | 
			
		||||
            .help("Sets the descriptor to use for the external addresses")
 | 
			
		||||
            .required(true)
 | 
			
		||||
            .takes_value(true),
 | 
			
		||||
    )
 | 
			
		||||
    .arg(
 | 
			
		||||
        Arg::with_name("change_descriptor")
 | 
			
		||||
            .short("c")
 | 
			
		||||
            .long("change_descriptor")
 | 
			
		||||
            .value_name("DESCRIPTOR")
 | 
			
		||||
            .help("Sets the descriptor to use for internal addresses")
 | 
			
		||||
            .takes_value(true),
 | 
			
		||||
    )
 | 
			
		||||
    .arg(
 | 
			
		||||
        Arg::with_name("v")
 | 
			
		||||
            .short("v")
 | 
			
		||||
            .multiple(true)
 | 
			
		||||
            .help("Sets the level of verbosity"),
 | 
			
		||||
    )
 | 
			
		||||
    .subcommand(SubCommand::with_name("repl").about("Opens an interactive shell"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn handle_matches<C, D>(
 | 
			
		||||
    wallet: &Wallet<C, D>,
 | 
			
		||||
    matches: ArgMatches<'_>,
 | 
			
		||||
) -> Result<Option<String>, Error>
 | 
			
		||||
where
 | 
			
		||||
    C: crate::blockchain::OnlineBlockchain,
 | 
			
		||||
    D: crate::database::BatchDatabase,
 | 
			
		||||
{
 | 
			
		||||
    if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") {
 | 
			
		||||
        Ok(Some(format!("{}", wallet.get_new_address()?)))
 | 
			
		||||
    } else if let Some(_sub_matches) = matches.subcommand_matches("sync") {
 | 
			
		||||
        wallet.sync(None, None).await?;
 | 
			
		||||
        Ok(None)
 | 
			
		||||
    } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") {
 | 
			
		||||
        let mut res = String::new();
 | 
			
		||||
        for utxo in wallet.list_unspent()? {
 | 
			
		||||
            res += &format!("{} value {} SAT\n", utxo.outpoint, utxo.txout.value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Some(res))
 | 
			
		||||
    } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") {
 | 
			
		||||
        Ok(Some(format!("{} SAT", wallet.get_balance()?)))
 | 
			
		||||
    } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") {
 | 
			
		||||
        let addressees = sub_matches
 | 
			
		||||
            .values_of("to")
 | 
			
		||||
            .unwrap()
 | 
			
		||||
            .map(|s| parse_addressee(s).unwrap())
 | 
			
		||||
            .collect();
 | 
			
		||||
        let send_all = sub_matches.is_present("send_all");
 | 
			
		||||
        let fee_rate = sub_matches
 | 
			
		||||
            .value_of("fee_rate")
 | 
			
		||||
            .map(|s| f32::from_str(s).unwrap())
 | 
			
		||||
            .unwrap_or(1.0);
 | 
			
		||||
        let utxos = sub_matches
 | 
			
		||||
            .values_of("utxos")
 | 
			
		||||
            .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
 | 
			
		||||
        let unspendable = sub_matches
 | 
			
		||||
            .values_of("unspendable")
 | 
			
		||||
            .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect());
 | 
			
		||||
        let policy: Option<Vec<_>> = sub_matches
 | 
			
		||||
            .value_of("policy")
 | 
			
		||||
            .map(|s| serde_json::from_str::<Vec<Vec<usize>>>(&s).unwrap());
 | 
			
		||||
 | 
			
		||||
        let result = wallet.create_tx(
 | 
			
		||||
            addressees,
 | 
			
		||||
            send_all,
 | 
			
		||||
            fee_rate * 1e-5,
 | 
			
		||||
            policy,
 | 
			
		||||
            utxos,
 | 
			
		||||
            unspendable,
 | 
			
		||||
        )?;
 | 
			
		||||
        Ok(Some(format!(
 | 
			
		||||
            "{:#?}\nPSBT: {}",
 | 
			
		||||
            result.1,
 | 
			
		||||
            base64::encode(&serialize(&result.0))
 | 
			
		||||
        )))
 | 
			
		||||
    } else if let Some(_sub_matches) = matches.subcommand_matches("policies") {
 | 
			
		||||
        Ok(Some(format!(
 | 
			
		||||
            "External: {}\nInternal:{}",
 | 
			
		||||
            serde_json::to_string(&wallet.policies(ScriptType::External)?).unwrap(),
 | 
			
		||||
            serde_json::to_string(&wallet.policies(ScriptType::Internal)?).unwrap(),
 | 
			
		||||
        )))
 | 
			
		||||
    } else if let Some(sub_matches) = matches.subcommand_matches("sign") {
 | 
			
		||||
        let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
 | 
			
		||||
        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
 | 
			
		||||
        let assume_height = sub_matches
 | 
			
		||||
            .value_of("assume_height")
 | 
			
		||||
            .and_then(|s| Some(s.parse().unwrap()));
 | 
			
		||||
        let (psbt, finalized) = wallet.sign(psbt, assume_height)?;
 | 
			
		||||
 | 
			
		||||
        let mut res = String::new();
 | 
			
		||||
 | 
			
		||||
        res += &format!("PSBT: {}\n", base64::encode(&serialize(&psbt)));
 | 
			
		||||
        res += &format!("Finalized: {}", finalized);
 | 
			
		||||
        if finalized {
 | 
			
		||||
            res += &format!("\nExtracted: {}", serialize_hex(&psbt.extract_tx()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(Some(res))
 | 
			
		||||
    } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
 | 
			
		||||
        let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
 | 
			
		||||
        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
 | 
			
		||||
        let (txid, _) = wallet.broadcast(psbt).await?;
 | 
			
		||||
 | 
			
		||||
        Ok(Some(format!("TXID: {}", txid)))
 | 
			
		||||
    } else {
 | 
			
		||||
        Ok(None)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -25,6 +25,9 @@ pub use blockchain::esplora::EsploraBlockchain;
 | 
			
		||||
#[cfg(feature = "key-value-db")]
 | 
			
		||||
pub extern crate sled;
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "cli-utils")]
 | 
			
		||||
pub mod cli;
 | 
			
		||||
 | 
			
		||||
#[macro_use]
 | 
			
		||||
pub mod error;
 | 
			
		||||
pub mod blockchain;
 | 
			
		||||
 | 
			
		||||
@ -498,6 +498,7 @@ where
 | 
			
		||||
 | 
			
		||||
    // Internals
 | 
			
		||||
 | 
			
		||||
    #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
    fn get_timestamp() -> u64 {
 | 
			
		||||
        SystemTime::now()
 | 
			
		||||
            .duration_since(UNIX_EPOCH)
 | 
			
		||||
@ -505,6 +506,11 @@ where
 | 
			
		||||
            .as_secs()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(target_arch = "wasm32")]
 | 
			
		||||
    fn get_timestamp() -> u64 {
 | 
			
		||||
        0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_descriptor_for(&self, script_type: ScriptType) -> &ExtendedDescriptor {
 | 
			
		||||
        let desc = match script_type {
 | 
			
		||||
            ScriptType::External => &self.descriptor,
 | 
			
		||||
@ -646,6 +652,7 @@ where
 | 
			
		||||
            // safe to run only on the descriptor because we assume the change descriptor also has
 | 
			
		||||
            // the same structure
 | 
			
		||||
            let desc = self.descriptor.derive_from_psbt_input(psbt, n);
 | 
			
		||||
            debug!("{:?}", psbt.inputs[n].hd_keypaths);
 | 
			
		||||
            debug!("reconstructed descriptor is {:?}", desc);
 | 
			
		||||
 | 
			
		||||
            let desc = match desc {
 | 
			
		||||
@ -765,6 +772,7 @@ where
 | 
			
		||||
        // cache a few of our addresses
 | 
			
		||||
        if last_addr.is_none() {
 | 
			
		||||
            let mut address_batch = self.database.borrow().begin_batch();
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            let start = Instant::now();
 | 
			
		||||
 | 
			
		||||
            for i in 0..=max_address {
 | 
			
		||||
@ -790,6 +798,7 @@ where
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            #[cfg(not(target_arch = "wasm32"))]
 | 
			
		||||
            info!(
 | 
			
		||||
                "derivation of {} addresses, took {} ms",
 | 
			
		||||
                max_address,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user