Wallet logic
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
extern crate magical_bitcoin_wallet;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -6,12 +7,12 @@ use magical_bitcoin_wallet::bitcoin::*;
|
||||
use magical_bitcoin_wallet::descriptor::*;
|
||||
|
||||
fn main() {
|
||||
let desc = "sh(wsh(or_d(\
|
||||
let desc = "wsh(or_d(\
|
||||
thresh_m(\
|
||||
2,[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/*\
|
||||
),\
|
||||
and_v(vc:pk_h(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy),older(1000))\
|
||||
)))";
|
||||
))";
|
||||
|
||||
let extended_desc = ExtendedDescriptor::from_str(desc).unwrap();
|
||||
println!("{:?}", extended_desc);
|
||||
@@ -19,6 +20,10 @@ fn main() {
|
||||
let derived_desc = extended_desc.derive(42).unwrap();
|
||||
println!("{:?}", derived_desc);
|
||||
|
||||
if let Descriptor::Wsh(x) = &derived_desc {
|
||||
println!("{}", serde_json::to_string(&x.extract_policy()).unwrap());
|
||||
}
|
||||
|
||||
let addr = derived_desc.address(Network::Testnet).unwrap();
|
||||
println!("{}", addr);
|
||||
|
||||
|
||||
358
examples/repl.rs
Normal file
358
examples/repl.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
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 clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::Editor;
|
||||
|
||||
#[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 magical_bitcoin_wallet::bitcoin;
|
||||
use magical_bitcoin_wallet::sled;
|
||||
use magical_bitcoin_wallet::types::ScriptType;
|
||||
use magical_bitcoin_wallet::{Client, ExtendedDescriptor, Wallet};
|
||||
|
||||
fn prepare_home_dir() -> PathBuf {
|
||||
let mut dir = PathBuf::new();
|
||||
dir.push(&dirs::home_dir().unwrap());
|
||||
dir.push(".magical-bitcoin");
|
||||
|
||||
if !dir.exists() {
|
||||
info!("Creating home directory {}", dir.as_path().display());
|
||||
fs::create_dir(&dir).unwrap();
|
||||
}
|
||||
|
||||
dir.push("database.sled");
|
||||
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(|_| ())
|
||||
}
|
||||
|
||||
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),
|
||||
));
|
||||
|
||||
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 matches = app.get_matches();
|
||||
|
||||
// TODO
|
||||
// let level = match matches.occurrences_of("v") {
|
||||
// 0 => LevelFilter::Info,
|
||||
// 1 => LevelFilter::Debug,
|
||||
// _ => LevelFilter::Trace,
|
||||
// };
|
||||
|
||||
let network = match matches.value_of("network") {
|
||||
Some("regtest") => Network::Regtest,
|
||||
Some("testnet") | _ => Network::Testnet,
|
||||
};
|
||||
|
||||
let descriptor = matches
|
||||
.value_of("descriptor")
|
||||
.map(|x| ExtendedDescriptor::from_str(x).unwrap())
|
||||
.unwrap();
|
||||
let change_descriptor = matches
|
||||
.value_of("change_descriptor")
|
||||
.map(|x| ExtendedDescriptor::from_str(x).unwrap());
|
||||
debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
|
||||
|
||||
let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
|
||||
let tree = database
|
||||
.open_tree(matches.value_of("wallet").unwrap())
|
||||
.unwrap();
|
||||
debug!("database opened successfully");
|
||||
|
||||
let client = Client::new(matches.value_of("server").unwrap()).unwrap();
|
||||
let wallet = Wallet::new(descriptor, change_descriptor, network, tree, client);
|
||||
|
||||
// TODO: print errors in a nice way
|
||||
let handle_matches = |matches: ArgMatches<'_>| {
|
||||
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).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 (psbt, finalized) = wallet.sign(psbt).unwrap();
|
||||
|
||||
println!("Finalized: {}", finalized);
|
||||
if finalized {
|
||||
println!("Extracted: {}", serialize_hex(&psbt.extract_tx()));
|
||||
} else {
|
||||
println!("PSBT: {}", base64::encode(&serialize(&psbt)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(_sub_matches) = matches.subcommand_matches("repl") {
|
||||
let mut rl = Editor::<()>::new();
|
||||
|
||||
// if rl.load_history("history.txt").is_err() {
|
||||
// println!("No previous history.");
|
||||
// }
|
||||
|
||||
loop {
|
||||
let readline = rl.readline(">> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
if line.trim() == "" {
|
||||
continue;
|
||||
}
|
||||
|
||||
rl.add_history_entry(line.as_str());
|
||||
let matches = repl_app.get_matches_from_safe_borrow(line.split(" "));
|
||||
if let Err(err) = matches {
|
||||
println!("{}", err.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
handle_matches(matches.unwrap());
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => continue,
|
||||
Err(ReadlineError::Eof) => break,
|
||||
Err(err) => {
|
||||
println!("{:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rl.save_history("history.txt").unwrap();
|
||||
} else {
|
||||
handle_matches(matches);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user