Wallet logic
This commit is contained in:
parent
d01e4369df
commit
1a4e1bd96c
@ -7,8 +7,11 @@ before_script:
|
||||
- rustup component add rustfmt
|
||||
script:
|
||||
- cargo fmt -- --check --verbose
|
||||
- cargo build --verbose --all
|
||||
- cargo test --verbose --all
|
||||
- cargo build --verbose --all
|
||||
- cargo build --verbose --no-default-features --features=minimal
|
||||
- cargo build --verbose --no-default-features --features=key-value-db
|
||||
- cargo build --verbose --no-default-features --features=electrum
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
11
Cargo.toml
11
Cargo.toml
@ -13,10 +13,17 @@ base64 = "^0.11"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.31.0", optional = true }
|
||||
electrum-client = { version = "0.1.0-beta.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["sled"]
|
||||
minimal = []
|
||||
default = ["sled", "electrum-client"]
|
||||
electrum = ["electrum-client"]
|
||||
key-value-db = ["sled"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
lazy_static = "1.4"
|
||||
rustyline = "5.0" # newer version requires 2018 edition
|
||||
clap = "2.33"
|
||||
dirs = "2.0"
|
||||
env_logger = "0.7"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -235,7 +235,7 @@ impl BatchOperations for Batch {
|
||||
}
|
||||
|
||||
impl Database for Tree {
|
||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Vec<Result<Script, Error>> {
|
||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error> {
|
||||
let key = SledKey::Path((script_type, None)).as_sled_key();
|
||||
self.scan_prefix(key)
|
||||
.map(|x| -> Result<_, Error> {
|
||||
@ -245,7 +245,7 @@ impl Database for Tree {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn iter_utxos(&self) -> Vec<Result<UTXO, Error>> {
|
||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error> {
|
||||
let key = SledKey::UTXO(None).as_sled_key();
|
||||
self.scan_prefix(key)
|
||||
.map(|x| -> Result<_, Error> {
|
||||
@ -257,7 +257,7 @@ impl Database for Tree {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn iter_raw_txs(&self) -> Vec<Result<Transaction, Error>> {
|
||||
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error> {
|
||||
let key = SledKey::RawTx(None).as_sled_key();
|
||||
self.scan_prefix(key)
|
||||
.map(|x| -> Result<_, Error> {
|
||||
@ -267,8 +267,8 @@ impl Database for Tree {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn iter_txs(&self, include_raw: bool) -> Vec<Result<TransactionDetails, Error>> {
|
||||
let key = SledKey::RawTx(None).as_sled_key();
|
||||
fn iter_txs(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
|
||||
let key = SledKey::Transaction(None).as_sled_key();
|
||||
self.scan_prefix(key)
|
||||
.map(|x| -> Result<_, Error> {
|
||||
let (k, v) = x?;
|
||||
@ -516,7 +516,7 @@ mod test {
|
||||
|
||||
tree.set_script_pubkey(&script, script_type, &path).unwrap();
|
||||
|
||||
assert_eq!(tree.iter_script_pubkeys(None).len(), 1);
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -530,11 +530,11 @@ mod test {
|
||||
let script_type = ScriptType::External;
|
||||
|
||||
tree.set_script_pubkey(&script, script_type, &path).unwrap();
|
||||
assert_eq!(tree.iter_script_pubkeys(None).len(), 1);
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1);
|
||||
|
||||
tree.del_script_pubkey_from_path(script_type, &path)
|
||||
.unwrap();
|
||||
assert_eq!(tree.iter_script_pubkeys(None).len(), 0);
|
||||
assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -40,10 +40,10 @@ pub trait BatchOperations {
|
||||
}
|
||||
|
||||
pub trait Database: BatchOperations {
|
||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Vec<Result<Script, Error>>;
|
||||
fn iter_utxos(&self) -> Vec<Result<UTXO, Error>>;
|
||||
fn iter_raw_txs(&self) -> Vec<Result<Transaction, Error>>;
|
||||
fn iter_txs(&self, include_raw: bool) -> Vec<Result<TransactionDetails, Error>>;
|
||||
fn iter_script_pubkeys(&self, script_type: Option<ScriptType>) -> Result<Vec<Script>, Error>;
|
||||
fn iter_utxos(&self) -> Result<Vec<UTXO>, Error>;
|
||||
fn iter_raw_txs(&self) -> Result<Vec<Transaction>, Error>;
|
||||
fn iter_txs(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error>;
|
||||
|
||||
fn get_script_pubkey_from_path<P: AsRef<[ChildNumber]>>(
|
||||
&self,
|
||||
|
@ -16,9 +16,15 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod error;
|
||||
pub mod extended_key;
|
||||
pub mod policy;
|
||||
|
||||
pub use self::error::Error;
|
||||
pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey};
|
||||
pub use self::policy::{ExtractPolicy, Policy};
|
||||
|
||||
trait MiniscriptExtractPolicy {
|
||||
fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)]
|
||||
struct DummyKey();
|
||||
@ -86,6 +92,7 @@ where
|
||||
fn psbt_witness_script(&self) -> Option<Script> {
|
||||
match self {
|
||||
Descriptor::Wsh(ref script) => Some(script.encode()),
|
||||
Descriptor::ShWsh(ref script) => Some(script.encode()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -261,20 +268,18 @@ impl ExtendedDescriptor {
|
||||
Ok(self.internal.translate_pk(translatefpk, translatefpkh)?)
|
||||
}
|
||||
|
||||
pub fn get_xprv(&self) -> Vec<ExtendedPrivKey> {
|
||||
pub fn get_xprv(&self) -> impl IntoIterator<Item = ExtendedPrivKey> + '_ {
|
||||
self.keys
|
||||
.iter()
|
||||
.filter(|(_, v)| v.xprv().is_some())
|
||||
.map(|(_, v)| v.xprv().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_secret_keys(&self) -> Vec<PrivateKey> {
|
||||
pub fn get_secret_keys(&self) -> impl IntoIterator<Item = PrivateKey> + '_ {
|
||||
self.keys
|
||||
.iter()
|
||||
.filter(|(_, v)| v.as_secret_key().is_some())
|
||||
.map(|(_, v)| v.as_secret_key().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_hd_keypaths(
|
||||
@ -317,6 +322,12 @@ impl ExtendedDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtractPolicy for ExtendedDescriptor {
|
||||
fn extract_policy(&self) -> Option<Policy> {
|
||||
self.internal.extract_policy(&self.keys)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ExtendedDescriptor {
|
||||
type Error = Error;
|
||||
|
||||
|
399
src/descriptor/policy.rs
Normal file
399
src/descriptor/policy.rs
Normal file
@ -0,0 +1,399 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use bitcoin::hashes::*;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::PublicKey;
|
||||
|
||||
use miniscript::{Descriptor, Miniscript, Terminal};
|
||||
|
||||
use descriptor::{Key, MiniscriptExtractPolicy};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PKOrF {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pubkey: Option<PublicKey>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fingerprint: Option<Fingerprint>,
|
||||
}
|
||||
|
||||
impl PKOrF {
|
||||
fn from_key(k: &Box<dyn Key>) -> Self {
|
||||
let secp = Secp256k1::gen_new();
|
||||
|
||||
if let Some(fing) = k.fingerprint(&secp) {
|
||||
PKOrF {
|
||||
fingerprint: Some(fing),
|
||||
pubkey: None,
|
||||
}
|
||||
} else {
|
||||
PKOrF {
|
||||
fingerprint: None,
|
||||
pubkey: Some(k.as_public_key(&secp, None).unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "UPPERCASE")]
|
||||
pub enum SatisfiableItem {
|
||||
// Leaves
|
||||
Signature(PKOrF),
|
||||
SignatureKey {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fingerprint: Option<Fingerprint>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pubkey_hash: Option<hash160::Hash>,
|
||||
},
|
||||
SHA256Preimage {
|
||||
hash: sha256::Hash,
|
||||
},
|
||||
HASH256Preimage {
|
||||
hash: sha256d::Hash,
|
||||
},
|
||||
RIPEMD160Preimage {
|
||||
hash: ripemd160::Hash,
|
||||
},
|
||||
HASH160Preimage {
|
||||
hash: hash160::Hash,
|
||||
},
|
||||
AbsoluteTimelock {
|
||||
height: u32,
|
||||
},
|
||||
RelativeTimelock {
|
||||
blocks: u32,
|
||||
},
|
||||
|
||||
// Complex item
|
||||
Thresh {
|
||||
items: Vec<Policy>,
|
||||
threshold: usize,
|
||||
},
|
||||
Multisig {
|
||||
keys: Vec<PKOrF>,
|
||||
threshold: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl SatisfiableItem {
|
||||
pub fn is_leaf(&self) -> bool {
|
||||
match self {
|
||||
SatisfiableItem::Thresh {
|
||||
items: _,
|
||||
threshold: _,
|
||||
} => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum ItemSatisfier {
|
||||
Us,
|
||||
Other(Option<Fingerprint>),
|
||||
Timelock(Option<u32>), // remaining blocks. TODO: time-based timelocks
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Policy {
|
||||
#[serde(flatten)]
|
||||
item: SatisfiableItem,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
satisfier: Option<ItemSatisfier>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PathRequirements {
|
||||
pub csv: Option<u32>,
|
||||
pub timelock: Option<u32>,
|
||||
}
|
||||
|
||||
impl PathRequirements {
|
||||
pub fn merge(&mut self, other: &Self) -> Result<(), PolicyError> {
|
||||
if other.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match (self.csv, other.csv) {
|
||||
(Some(old), Some(new)) if old != new => Err(PolicyError::DifferentCSV(old, new)),
|
||||
_ => {
|
||||
self.csv = self.csv.or(other.csv);
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
match (self.timelock, other.timelock) {
|
||||
(Some(old), Some(new)) if old != new => Err(PolicyError::DifferentTimelock(old, new)),
|
||||
_ => {
|
||||
self.timelock = self.timelock.or(other.timelock);
|
||||
Ok(())
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.csv.is_none() && self.timelock.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PolicyError {
|
||||
NotEnoughItemsSelected(usize),
|
||||
TooManyItemsSelected(usize),
|
||||
IndexOutOfRange(usize, usize),
|
||||
DifferentCSV(u32, u32),
|
||||
DifferentTimelock(u32, u32),
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn new(item: SatisfiableItem) -> Self {
|
||||
Policy {
|
||||
item,
|
||||
satisfier: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_and(a: Option<Policy>, b: Option<Policy>) -> Option<Policy> {
|
||||
match (a, b) {
|
||||
(None, None) => None,
|
||||
(Some(x), None) | (None, Some(x)) => Some(x),
|
||||
(Some(a), Some(b)) => Some(
|
||||
SatisfiableItem::Thresh {
|
||||
items: vec![a, b],
|
||||
threshold: 2,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_or(a: Option<Policy>, b: Option<Policy>) -> Option<Policy> {
|
||||
match (a, b) {
|
||||
(None, None) => None,
|
||||
(Some(x), None) | (None, Some(x)) => Some(x),
|
||||
(Some(a), Some(b)) => Some(
|
||||
SatisfiableItem::Thresh {
|
||||
items: vec![a, b],
|
||||
threshold: 1,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_thresh(items: Vec<Policy>, mut threshold: usize) -> Option<Policy> {
|
||||
if threshold == 0 {
|
||||
return None;
|
||||
}
|
||||
if threshold > items.len() {
|
||||
threshold = items.len();
|
||||
}
|
||||
|
||||
Some(SatisfiableItem::Thresh { items, threshold }.into())
|
||||
}
|
||||
|
||||
fn make_multisig(pubkeys: Vec<Option<&Box<dyn Key>>>, threshold: usize) -> Option<Policy> {
|
||||
let keys = pubkeys
|
||||
.into_iter()
|
||||
.map(|k| PKOrF::from_key(k.unwrap()))
|
||||
.collect();
|
||||
Some(SatisfiableItem::Multisig { keys, threshold }.into())
|
||||
}
|
||||
|
||||
pub fn requires_path(&self) -> bool {
|
||||
self.get_requirements(&vec![]).is_err()
|
||||
}
|
||||
|
||||
pub fn get_requirements(
|
||||
&self,
|
||||
path: &Vec<Vec<usize>>,
|
||||
) -> Result<PathRequirements, PolicyError> {
|
||||
self.recursive_get_requirements(path, 0)
|
||||
}
|
||||
|
||||
fn recursive_get_requirements(
|
||||
&self,
|
||||
path: &Vec<Vec<usize>>,
|
||||
index: usize,
|
||||
) -> Result<PathRequirements, PolicyError> {
|
||||
// if items.len() == threshold, selected can be omitted and we take all of them by default
|
||||
let default = match &self.item {
|
||||
SatisfiableItem::Thresh { items, threshold } if items.len() == *threshold => {
|
||||
(0..*threshold).into_iter().collect()
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
let selected = match path.get(index) {
|
||||
_ if !default.is_empty() => &default,
|
||||
Some(arr) => arr,
|
||||
_ => &default,
|
||||
};
|
||||
|
||||
match &self.item {
|
||||
SatisfiableItem::Thresh { items, threshold } => {
|
||||
let mapped_req = items
|
||||
.iter()
|
||||
.map(|i| i.recursive_get_requirements(path, index + 1))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// if all the requirements are null we don't care about `selected` because there
|
||||
// are no requirements
|
||||
if mapped_req.iter().all(PathRequirements::is_null) {
|
||||
return Ok(PathRequirements::default());
|
||||
}
|
||||
|
||||
// if we have something, make sure we have enough items. note that the user can set
|
||||
// an empty value for this step in case of n-of-n, because `selected` is set to all
|
||||
// the elements above
|
||||
if selected.len() < *threshold {
|
||||
return Err(PolicyError::NotEnoughItemsSelected(index));
|
||||
}
|
||||
|
||||
// check the selected items, see if there are conflicting requirements
|
||||
let mut requirements = PathRequirements::default();
|
||||
for item_index in selected {
|
||||
requirements.merge(
|
||||
mapped_req
|
||||
.get(*item_index)
|
||||
.ok_or(PolicyError::IndexOutOfRange(*item_index, index))?,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(requirements)
|
||||
}
|
||||
_ if !selected.is_empty() => Err(PolicyError::TooManyItemsSelected(index)),
|
||||
SatisfiableItem::AbsoluteTimelock { height } => Ok(PathRequirements {
|
||||
csv: None,
|
||||
timelock: Some(*height),
|
||||
}),
|
||||
SatisfiableItem::RelativeTimelock { blocks } => Ok(PathRequirements {
|
||||
csv: Some(*blocks),
|
||||
timelock: None,
|
||||
}),
|
||||
_ => Ok(PathRequirements::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SatisfiableItem> for Policy {
|
||||
fn from(other: SatisfiableItem) -> Self {
|
||||
Self::new(other)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtractPolicy {
|
||||
fn extract_policy(&self) -> Option<Policy>;
|
||||
}
|
||||
|
||||
fn signature_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
||||
key.map(|k| SatisfiableItem::Signature(PKOrF::from_key(k)).into())
|
||||
}
|
||||
|
||||
fn signature_key_from_string(key: Option<&Box<dyn Key>>) -> Option<Policy> {
|
||||
let secp = Secp256k1::gen_new();
|
||||
|
||||
key.map(|k| {
|
||||
if let Some(fing) = k.fingerprint(&secp) {
|
||||
SatisfiableItem::SignatureKey {
|
||||
fingerprint: Some(fing),
|
||||
pubkey_hash: None,
|
||||
}
|
||||
} else {
|
||||
SatisfiableItem::SignatureKey {
|
||||
fingerprint: None,
|
||||
pubkey_hash: Some(hash160::Hash::hash(
|
||||
&k.as_public_key(&secp, None).unwrap().to_bytes(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
impl MiniscriptExtractPolicy for Miniscript<String> {
|
||||
fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy> {
|
||||
match &self.node {
|
||||
// Leaves
|
||||
Terminal::True | Terminal::False => None,
|
||||
Terminal::Pk(pubkey) => signature_from_string(lookup_map.get(pubkey)),
|
||||
Terminal::PkH(pubkey_hash) => signature_key_from_string(lookup_map.get(pubkey_hash)),
|
||||
Terminal::After(height) => {
|
||||
Some(SatisfiableItem::AbsoluteTimelock { height: *height }.into())
|
||||
}
|
||||
Terminal::Older(blocks) => {
|
||||
Some(SatisfiableItem::RelativeTimelock { blocks: *blocks }.into())
|
||||
}
|
||||
Terminal::Sha256(hash) => Some(SatisfiableItem::SHA256Preimage { hash: *hash }.into()),
|
||||
Terminal::Hash256(hash) => {
|
||||
Some(SatisfiableItem::HASH256Preimage { hash: *hash }.into())
|
||||
}
|
||||
Terminal::Ripemd160(hash) => {
|
||||
Some(SatisfiableItem::RIPEMD160Preimage { hash: *hash }.into())
|
||||
}
|
||||
Terminal::Hash160(hash) => {
|
||||
Some(SatisfiableItem::HASH160Preimage { hash: *hash }.into())
|
||||
}
|
||||
// Identities
|
||||
Terminal::Alt(inner)
|
||||
| Terminal::Swap(inner)
|
||||
| Terminal::Check(inner)
|
||||
| Terminal::DupIf(inner)
|
||||
| Terminal::Verify(inner)
|
||||
| Terminal::NonZero(inner)
|
||||
| Terminal::ZeroNotEqual(inner) => inner.extract_policy(lookup_map),
|
||||
// Complex policies
|
||||
Terminal::AndV(a, b) | Terminal::AndB(a, b) => {
|
||||
Policy::make_and(a.extract_policy(lookup_map), b.extract_policy(lookup_map))
|
||||
}
|
||||
Terminal::AndOr(x, y, z) => Policy::make_or(
|
||||
Policy::make_and(x.extract_policy(lookup_map), y.extract_policy(lookup_map)),
|
||||
z.extract_policy(lookup_map),
|
||||
),
|
||||
Terminal::OrB(a, b)
|
||||
| Terminal::OrD(a, b)
|
||||
| Terminal::OrC(a, b)
|
||||
| Terminal::OrI(a, b) => {
|
||||
Policy::make_or(a.extract_policy(lookup_map), b.extract_policy(lookup_map))
|
||||
}
|
||||
Terminal::Thresh(k, nodes) => {
|
||||
let mut threshold = *k;
|
||||
let mapped: Vec<_> = nodes
|
||||
.iter()
|
||||
.filter_map(|n| n.extract_policy(lookup_map))
|
||||
.collect();
|
||||
|
||||
if mapped.len() < nodes.len() {
|
||||
threshold = match threshold.checked_sub(nodes.len() - mapped.len()) {
|
||||
None => return None,
|
||||
Some(x) => x,
|
||||
};
|
||||
}
|
||||
|
||||
Policy::make_thresh(mapped, threshold)
|
||||
}
|
||||
Terminal::ThreshM(k, pks) => {
|
||||
Policy::make_multisig(pks.iter().map(|s| lookup_map.get(s)).collect(), *k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MiniscriptExtractPolicy for Descriptor<String> {
|
||||
fn extract_policy(&self, lookup_map: &BTreeMap<String, Box<dyn Key>>) -> Option<Policy> {
|
||||
match self {
|
||||
Descriptor::Pk(pubkey)
|
||||
| Descriptor::Pkh(pubkey)
|
||||
| Descriptor::Wpkh(pubkey)
|
||||
| Descriptor::ShWpkh(pubkey) => signature_from_string(lookup_map.get(pubkey)),
|
||||
Descriptor::Bare(inner)
|
||||
| Descriptor::Sh(inner)
|
||||
| Descriptor::Wsh(inner)
|
||||
| Descriptor::ShWsh(inner) => inner.extract_policy(lookup_map),
|
||||
}
|
||||
}
|
||||
}
|
35
src/error.rs
35
src/error.rs
@ -1,15 +1,40 @@
|
||||
use bitcoin::{OutPoint, Script, Txid};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
|
||||
MissingInputUTXO(usize),
|
||||
InvalidU32Bytes(Vec<u8>),
|
||||
Generic(String),
|
||||
ScriptDoesntHaveAddressForm,
|
||||
SendAllMultipleOutputs,
|
||||
OutputBelowDustLimit(usize),
|
||||
InsufficientFunds,
|
||||
UnknownUTXO,
|
||||
DifferentTransactions,
|
||||
|
||||
SpendingPolicyRequired,
|
||||
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
|
||||
|
||||
// Signing errors (expected, received)
|
||||
InputTxidMismatch((Txid, OutPoint)),
|
||||
InputRedeemScriptMismatch((Script, Script)), // scriptPubKey, redeemScript
|
||||
InputWitnessScriptMismatch((Script, Script)), // scriptPubKey, redeemScript
|
||||
InputUnknownSegwitScript(Script),
|
||||
InputMissingWitnessScript(usize),
|
||||
MissingUTXO,
|
||||
|
||||
Descriptor(crate::descriptor::error::Error),
|
||||
|
||||
Encode(bitcoin::consensus::encode::Error),
|
||||
BIP32(bitcoin::util::bip32::Error),
|
||||
Secp256k1(bitcoin::secp256k1::Error),
|
||||
JSON(serde_json::Error),
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
PSBT(bitcoin::util::psbt::Error),
|
||||
|
||||
#[cfg(any(feature = "electrum", feature = "default"))]
|
||||
Electrum(electrum_client::Error),
|
||||
#[cfg(any(feature = "key-value-db", feature = "default"))]
|
||||
Sled(sled::Error),
|
||||
}
|
||||
@ -24,10 +49,20 @@ macro_rules! impl_error {
|
||||
};
|
||||
}
|
||||
|
||||
impl_error!(crate::descriptor::error::Error, Descriptor);
|
||||
impl_error!(
|
||||
crate::descriptor::policy::PolicyError,
|
||||
InvalidPolicyPathError
|
||||
);
|
||||
|
||||
impl_error!(bitcoin::consensus::encode::Error, Encode);
|
||||
impl_error!(bitcoin::util::bip32::Error, BIP32);
|
||||
impl_error!(bitcoin::secp256k1::Error, Secp256k1);
|
||||
impl_error!(serde_json::Error, JSON);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||
impl_error!(bitcoin::util::psbt::Error, PSBT);
|
||||
|
||||
#[cfg(any(feature = "electrum", feature = "default"))]
|
||||
impl_error!(electrum_client::Error, Electrum);
|
||||
#[cfg(any(feature = "key-value-db", feature = "default"))]
|
||||
impl_error!(sled::Error, Sled);
|
||||
|
10
src/lib.rs
10
src/lib.rs
@ -9,8 +9,12 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(any(feature = "electrum", feature = "default"))]
|
||||
pub extern crate electrum_client;
|
||||
#[cfg(any(feature = "electrum", feature = "default"))]
|
||||
pub use electrum_client::client::Client;
|
||||
#[cfg(any(feature = "key-value-db", feature = "default"))]
|
||||
extern crate sled;
|
||||
pub extern crate sled;
|
||||
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
@ -19,3 +23,7 @@ pub mod descriptor;
|
||||
pub mod psbt;
|
||||
pub mod signer;
|
||||
pub mod types;
|
||||
pub mod wallet;
|
||||
|
||||
pub use descriptor::ExtendedDescriptor;
|
||||
pub use wallet::Wallet;
|
||||
|
78
src/psbt.rs
78
src/psbt.rs
@ -1,6 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::hashes::{hash160, Hash};
|
||||
use bitcoin::util::bip143::SighashComponents;
|
||||
use bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||
use bitcoin::util::psbt;
|
||||
@ -8,7 +8,10 @@ use bitcoin::{PrivateKey, PublicKey, Script, SigHashType, Transaction};
|
||||
|
||||
use bitcoin::secp256k1::{self, All, Message, Secp256k1};
|
||||
|
||||
use miniscript::{BitcoinSig, Satisfier};
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use miniscript::{BitcoinSig, MiniscriptKey, Satisfier};
|
||||
|
||||
use crate::descriptor::ExtendedDescriptor;
|
||||
use crate::error::Error;
|
||||
@ -34,29 +37,70 @@ impl<'a> PSBTSatisfier<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PSBTSatisfier<'a> {
|
||||
fn parse_sig(rawsig: &Vec<u8>) -> Option<BitcoinSig> {
|
||||
let (flag, sig) = rawsig.split_last().unwrap();
|
||||
let flag = bitcoin::SigHashType::from_u32(*flag as u32);
|
||||
let sig = match secp256k1::Signature::from_der(sig) {
|
||||
Ok(sig) => sig,
|
||||
Err(..) => return None,
|
||||
};
|
||||
Some((sig, flag))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: also support hash preimages through the "unknown" section of PSBT
|
||||
impl<'a> Satisfier<bitcoin::PublicKey> for PSBTSatisfier<'a> {
|
||||
// from https://docs.rs/miniscript/0.12.0/src/miniscript/psbt/mod.rs.html#96
|
||||
fn lookup_sig(&self, pk: &bitcoin::PublicKey) -> Option<BitcoinSig> {
|
||||
debug!("lookup_sig: {}", pk);
|
||||
|
||||
if let Some(rawsig) = self.input.partial_sigs.get(pk) {
|
||||
let (flag, sig) = rawsig.split_last().unwrap();
|
||||
let flag = bitcoin::SigHashType::from_u32(*flag as u32);
|
||||
let sig = match secp256k1::Signature::from_der(sig) {
|
||||
Ok(sig) => sig,
|
||||
Err(..) => return None,
|
||||
};
|
||||
Some((sig, flag))
|
||||
Self::parse_sig(&rawsig)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_pkh_pk(&self, hash: &hash160::Hash) -> Option<bitcoin::PublicKey> {
|
||||
debug!("lookup_pkh_pk: {}", hash);
|
||||
|
||||
for (pk, _) in &self.input.partial_sigs {
|
||||
if &pk.to_pubkeyhash() == hash {
|
||||
return Some(*pk);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn lookup_pkh_sig(&self, hash: &hash160::Hash) -> Option<(bitcoin::PublicKey, BitcoinSig)> {
|
||||
debug!("lookup_pkh_sig: {}", hash);
|
||||
|
||||
for (pk, sig) in &self.input.partial_sigs {
|
||||
if &pk.to_pubkeyhash() == hash {
|
||||
return match Self::parse_sig(&sig) {
|
||||
Some(bitcoinsig) => Some((*pk, bitcoinsig)),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn check_older(&self, height: u32) -> bool {
|
||||
// TODO: also check if `nSequence` right
|
||||
debug!("check_older: {}", height);
|
||||
|
||||
// TODO: test >= / >
|
||||
self.current_height.unwrap_or(0) >= self.create_height.unwrap_or(0) + height
|
||||
}
|
||||
|
||||
fn check_after(&self, height: u32) -> bool {
|
||||
// TODO: also check if `nLockTime` is right
|
||||
debug!("check_older: {}", height);
|
||||
|
||||
self.current_height.unwrap_or(0) > height
|
||||
}
|
||||
}
|
||||
@ -94,6 +138,22 @@ impl<'a> PSBTSigner<'a> {
|
||||
private_keys,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, mut other: PSBTSigner) -> Result<(), Error> {
|
||||
if self.tx.txid() != other.tx.txid() {
|
||||
return Err(Error::DifferentTransactions);
|
||||
}
|
||||
|
||||
self.extended_keys.append(&mut other.extended_keys);
|
||||
self.private_keys.append(&mut other.private_keys);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: temporary
|
||||
pub fn all_public_keys(&self) -> impl IntoIterator<Item = &PublicKey> {
|
||||
self.private_keys.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Signer for PSBTSigner<'a> {
|
||||
|
@ -36,7 +36,7 @@ pub struct UTXO {
|
||||
pub txout: TxOut,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TransactionDetails {
|
||||
pub transaction: Option<Transaction>,
|
||||
pub txid: Txid,
|
||||
|
1039
src/wallet/mod.rs
Normal file
1039
src/wallet/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
52
src/wallet/offline_stream.rs
Normal file
52
src/wallet/offline_stream.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use std::io::{self, Error, ErrorKind, Read, Write};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OfflineStream {}
|
||||
|
||||
impl Read for OfflineStream {
|
||||
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
|
||||
Err(Error::new(
|
||||
ErrorKind::NotConnected,
|
||||
"Trying to read from an OfflineStream",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for OfflineStream {
|
||||
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
|
||||
Err(Error::new(
|
||||
ErrorKind::NotConnected,
|
||||
"Trying to read from an OfflineStream",
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Err(Error::new(
|
||||
ErrorKind::NotConnected,
|
||||
"Trying to read from an OfflineStream",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(any(feature = "electrum", feature = "default"))]
|
||||
// use electrum_client::Client;
|
||||
//
|
||||
// #[cfg(any(feature = "electrum", feature = "default"))]
|
||||
// impl OfflineStream {
|
||||
// fn new_client() -> {
|
||||
// use std::io::bufreader;
|
||||
//
|
||||
// let stream = OfflineStream{};
|
||||
// let buf_reader = BufReader::new(stream.clone());
|
||||
//
|
||||
// Client {
|
||||
// stream,
|
||||
// buf_reader,
|
||||
// headers: VecDeque::new(),
|
||||
// script_notifications: BTreeMap::new(),
|
||||
//
|
||||
// #[cfg(feature = "debug-calls")]
|
||||
// calls: 0,
|
||||
// }
|
||||
// }
|
||||
// }
|
48
src/wallet/utils.rs
Normal file
48
src/wallet/utils.rs
Normal file
@ -0,0 +1,48 @@
|
||||
// De-facto standard "dust limit" (even though it should change based on the output type)
|
||||
const DUST_LIMIT_SATOSHI: u64 = 546;
|
||||
|
||||
// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
|
||||
// instead of a <= etc. The constant value for the dust limit is not public on purpose, to
|
||||
// encourage the usage of this trait.
|
||||
pub trait IsDust {
|
||||
fn is_dust(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsDust for u64 {
|
||||
fn is_dust(&self) -> bool {
|
||||
*self <= DUST_LIMIT_SATOSHI
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChunksIterator<I: Iterator> {
|
||||
iter: I,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl<I: Iterator> ChunksIterator<I> {
|
||||
pub fn new(iter: I, size: usize) -> Self {
|
||||
ChunksIterator { iter, size }
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator> Iterator for ChunksIterator<I> {
|
||||
type Item = Vec<<I as std::iter::Iterator>::Item>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..self.size {
|
||||
let e = self.iter.next();
|
||||
|
||||
match e {
|
||||
None => break,
|
||||
Some(val) => v.push(val),
|
||||
}
|
||||
}
|
||||
|
||||
if v.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(v)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user