[examples_redesign] Introduce example_cli
package
This is the equivalent of `keychain_tracker_example_cli` that works with the redesigned structures.
This commit is contained in:
parent
2e3cee4bd0
commit
f55974a64b
@ -4,6 +4,7 @@ members = [
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/electrum",
|
||||
"example-crates/example_cli",
|
||||
"example-crates/keychain_tracker_electrum",
|
||||
"example-crates/keychain_tracker_esplora",
|
||||
"example-crates/keychain_tracker_example_cli",
|
||||
|
@ -16,6 +16,13 @@ pub enum ObservedAs<A> {
|
||||
Unconfirmed(u64),
|
||||
}
|
||||
|
||||
impl<A> ObservedAs<A> {
|
||||
/// Returns whether [`ObservedAs`] is confirmed or not.
|
||||
pub fn is_confirmed(&self) -> bool {
|
||||
matches!(self, Self::Confirmed(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone> ObservedAs<&A> {
|
||||
pub fn cloned(self) -> ObservedAs<A> {
|
||||
match self {
|
||||
|
@ -19,4 +19,7 @@ pub trait ChainOracle {
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error>;
|
||||
|
||||
/// Get the best chain's chain tip.
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error>;
|
||||
}
|
||||
|
@ -203,6 +203,15 @@ impl<A: Anchor, IA: Append> Append for IndexedAdditions<A, IA> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, IA: Default> From<Additions<A>> for IndexedAdditions<A, IA> {
|
||||
fn from(graph_additions: Additions<A>) -> Self {
|
||||
Self {
|
||||
graph_additions,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a structure that can index transaction data.
|
||||
pub trait Indexer {
|
||||
/// The resultant "additions" when new transaction data is indexed.
|
||||
|
@ -34,6 +34,10 @@ impl ChainOracle for LocalChain {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
|
||||
Ok(self.tip())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
|
@ -130,7 +130,7 @@ impl ElectrumExt for Client {
|
||||
let mut scanned_spk_iter = scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone()));
|
||||
match populate_with_spks::<K, _, _>(
|
||||
match populate_with_spks::<_, _>(
|
||||
self,
|
||||
&mut update,
|
||||
&mut scanned_spk_iter,
|
||||
@ -143,7 +143,7 @@ impl ElectrumExt for Client {
|
||||
};
|
||||
}
|
||||
for (keychain, keychain_spks) in &mut request_spks {
|
||||
match populate_with_spks::<K, u32, _>(
|
||||
match populate_with_spks::<u32, _>(
|
||||
self,
|
||||
&mut update,
|
||||
keychain_spks,
|
||||
@ -529,7 +529,7 @@ fn populate_with_txids(
|
||||
|
||||
/// Populate an update [`SparseChain`] with transactions (and associated block positions) from
|
||||
/// the transaction history of the provided `spk`s.
|
||||
fn populate_with_spks<K, I, S>(
|
||||
fn populate_with_spks<I, S>(
|
||||
client: &Client,
|
||||
update: &mut SparseChain,
|
||||
spks: &mut S,
|
||||
@ -537,7 +537,6 @@ fn populate_with_spks<K, I, S>(
|
||||
batch_size: usize,
|
||||
) -> Result<BTreeMap<I, (Script, bool)>, InternalError>
|
||||
where
|
||||
K: Ord + Clone,
|
||||
I: Ord + Clone,
|
||||
S: Iterator<Item = (I, Script)>,
|
||||
{
|
||||
|
17
example-crates/example_cli/Cargo.toml
Normal file
17
example-crates/example_cli/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "example_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
|
||||
bdk_coin_select = { path = "../../nursery/coin_select" }
|
||||
|
||||
clap = { version = "3.2.23", features = ["derive", "env"] }
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
775
example-crates/example_cli/src/lib.rs
Normal file
775
example-crates/example_cli/src/lib.rs
Normal file
@ -0,0 +1,775 @@
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
use bdk_file_store::Store;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
psbt::Prevouts,
|
||||
secp256k1::{self, Secp256k1},
|
||||
util::sighash::SighashCache,
|
||||
Address, LockTime, Network, Script, Sequence, Transaction, TxIn, TxOut,
|
||||
},
|
||||
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
|
||||
keychain::{DerivationAdditions, KeychainTxOutIndex},
|
||||
miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, ObservedAs, Persist, PersistBackend,
|
||||
};
|
||||
pub use bdk_file_store;
|
||||
pub use clap;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
|
||||
pub type Database<'m, A, X> = Persist<Store<'m, ChangeSet<A, X>>, ChangeSet<A, X>>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(bound(
|
||||
deserialize = "A: Ord + serde::Deserialize<'de>, X: serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + serde::Serialize, X: serde::Serialize",
|
||||
))]
|
||||
pub struct ChangeSet<A, X> {
|
||||
pub indexed_additions: IndexedAdditions<A, DerivationAdditions<Keychain>>,
|
||||
pub extension: X,
|
||||
}
|
||||
|
||||
impl<A, X: Default> Default for ChangeSet<A, X> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indexed_additions: Default::default(),
|
||||
extension: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, X: Append> Append for ChangeSet<A, X> {
|
||||
fn append(&mut self, other: Self) {
|
||||
Append::append(&mut self.indexed_additions, other.indexed_additions);
|
||||
Append::append(&mut self.extension, other.extension)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct Args<C: clap::Subcommand> {
|
||||
#[clap(env = "DESCRIPTOR")]
|
||||
pub descriptor: String,
|
||||
#[clap(env = "CHANGE_DESCRIPTOR")]
|
||||
pub change_descriptor: Option<String>,
|
||||
|
||||
#[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
|
||||
pub network: Network,
|
||||
|
||||
#[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
|
||||
pub db_path: PathBuf,
|
||||
|
||||
#[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
|
||||
pub cp_limit: usize,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands<C>,
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<C: clap::Subcommand> {
|
||||
#[clap(flatten)]
|
||||
ChainSpecific(C),
|
||||
/// Address generation and inspection.
|
||||
Address {
|
||||
#[clap(subcommand)]
|
||||
addr_cmd: AddressCmd,
|
||||
},
|
||||
/// Get the wallet balance.
|
||||
Balance,
|
||||
/// TxOut related commands.
|
||||
#[clap(name = "txout")]
|
||||
TxOut {
|
||||
#[clap(subcommand)]
|
||||
txout_cmd: TxOutCmd,
|
||||
},
|
||||
/// Send coins to an address.
|
||||
Send {
|
||||
value: u64,
|
||||
address: Address,
|
||||
#[clap(short, default_value = "largest-first")]
|
||||
coin_select: CoinSelectionAlgo,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CoinSelectionAlgo {
|
||||
LargestFirst,
|
||||
SmallestFirst,
|
||||
OldestFirst,
|
||||
NewestFirst,
|
||||
BranchAndBound,
|
||||
}
|
||||
|
||||
impl Default for CoinSelectionAlgo {
|
||||
fn default() -> Self {
|
||||
Self::LargestFirst
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for CoinSelectionAlgo {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use CoinSelectionAlgo::*;
|
||||
Ok(match s {
|
||||
"largest-first" => LargestFirst,
|
||||
"smallest-first" => SmallestFirst,
|
||||
"oldest-first" => OldestFirst,
|
||||
"newest-first" => NewestFirst,
|
||||
"bnb" => BranchAndBound,
|
||||
unknown => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unknown coin selection algorithm '{}'",
|
||||
unknown
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CoinSelectionAlgo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use CoinSelectionAlgo::*;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
LargestFirst => "largest-first",
|
||||
SmallestFirst => "smallest-first",
|
||||
OldestFirst => "oldest-first",
|
||||
NewestFirst => "newest-first",
|
||||
BranchAndBound => "bnb",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address.
|
||||
Next,
|
||||
/// Get a new address regardless of the existing unused addresses.
|
||||
New,
|
||||
/// List all addresses
|
||||
List {
|
||||
#[clap(long)]
|
||||
change: bool,
|
||||
},
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum TxOutCmd {
|
||||
List {
|
||||
/// Return only spent outputs.
|
||||
#[clap(short, long)]
|
||||
spent: bool,
|
||||
/// Return only unspent outputs.
|
||||
#[clap(short, long)]
|
||||
unspent: bool,
|
||||
/// Return only confirmed outputs.
|
||||
#[clap(long)]
|
||||
confirmed: bool,
|
||||
/// Return only unconfirmed outputs.
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
pub enum Keychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Keychain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Keychain::External => write!(f, "external"),
|
||||
Keychain::Internal => write!(f, "internal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_address_cmd<A, X>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
db: &Mutex<Database<'_, A, X>>,
|
||||
network: Network,
|
||||
cmd: AddressCmd,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize,
|
||||
{
|
||||
let process_spk = |spk_i: u32, spk: &Script, index_additions: DerivationAdditions<Keychain>| {
|
||||
if !index_additions.is_empty() {
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage(ChangeSet {
|
||||
indexed_additions: IndexedAdditions {
|
||||
index_additions,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
db.commit()?;
|
||||
}
|
||||
let addr = Address::from_script(spk, network).context("failed to derive address")?;
|
||||
println!("[address @ {}] {}", spk_i, addr);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let index = &mut graph.index;
|
||||
|
||||
match cmd {
|
||||
AddressCmd::Next => {
|
||||
let ((spk_i, spk), index_additions) = index.next_unused_spk(&Keychain::External);
|
||||
process_spk(spk_i, spk, index_additions)
|
||||
}
|
||||
AddressCmd::New => {
|
||||
let ((spk_i, spk), index_additions) = index.reveal_next_spk(&Keychain::External);
|
||||
process_spk(spk_i, spk, index_additions)
|
||||
}
|
||||
AddressCmd::Index => {
|
||||
for (keychain, derivation_index) in index.last_revealed_indices() {
|
||||
println!("{:?}: {}", keychain, derivation_index);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
AddressCmd::List { change } => {
|
||||
let target_keychain = match change {
|
||||
true => Keychain::Internal,
|
||||
false => Keychain::External,
|
||||
};
|
||||
for (spk_i, spk) in index.revealed_spks_of_keychain(&target_keychain) {
|
||||
let address = Address::from_script(spk, network)
|
||||
.expect("should always be able to derive address");
|
||||
println!(
|
||||
"{:?} {} used:{}",
|
||||
spk_i,
|
||||
address,
|
||||
index.is_used(&(target_keychain, spk_i))
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_balance_cmd<A: Anchor, O: ChainOracle>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
) -> Result<(), O::Error> {
|
||||
let balance = graph.graph().try_balance(
|
||||
chain,
|
||||
chain.get_chain_tip()?.unwrap_or_default(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)?;
|
||||
|
||||
let confirmed_total = balance.confirmed + balance.immature;
|
||||
let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
|
||||
|
||||
println!("[confirmed]");
|
||||
println!(" total = {}sats", confirmed_total);
|
||||
println!(" spendable = {}sats", balance.confirmed);
|
||||
println!(" immature = {}sats", balance.immature);
|
||||
|
||||
println!("[unconfirmed]");
|
||||
println!(" total = {}sats", unconfirmed_total,);
|
||||
println!(" trusted = {}sats", balance.trusted_pending);
|
||||
println!(" untrusted = {}sats", balance.untrusted_pending);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_txo_cmd<A: Anchor, O: ChainOracle>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
network: Network,
|
||||
cmd: TxOutCmd,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
match cmd {
|
||||
TxOutCmd::List {
|
||||
spent,
|
||||
unspent,
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
} => {
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.try_filter_chain_txouts(chain, chain_tip, outpoints)
|
||||
.filter(|r| match r {
|
||||
Ok((_, full_txo)) => match (spent, unspent) {
|
||||
(true, false) => full_txo.spent_by.is_some(),
|
||||
(false, true) => full_txo.spent_by.is_none(),
|
||||
_ => true,
|
||||
},
|
||||
// always keep errored items
|
||||
Err(_) => true,
|
||||
})
|
||||
.filter(|r| match r {
|
||||
Ok((_, full_txo)) => match (confirmed, unconfirmed) {
|
||||
(true, false) => full_txo.chain_position.is_confirmed(),
|
||||
(false, true) => !full_txo.chain_position.is_confirmed(),
|
||||
_ => true,
|
||||
},
|
||||
// always keep errored items
|
||||
Err(_) => true,
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for (spk_i, full_txo) in txouts {
|
||||
let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
|
||||
println!(
|
||||
"{:?} {} {} {} spent:{:?}",
|
||||
spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_send_cmd<A: Anchor, O: ChainOracle, X>(
|
||||
graph: &Mutex<KeychainTxGraph<A>>,
|
||||
db: &Mutex<Database<'_, A, X>>,
|
||||
chain: &O,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize,
|
||||
{
|
||||
let (transaction, change_index) = {
|
||||
let graph = &mut *graph.lock().unwrap();
|
||||
// take mutable ref to construct tx -- it is only open for a short time while building it.
|
||||
let (tx, change_info) = create_tx(graph, chain, keymap, cs_algorithm, address, value)?;
|
||||
|
||||
if let Some((index_additions, (change_keychain, index))) = change_info {
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this, then we don't want to broadcast.
|
||||
db.lock().unwrap().stage(ChangeSet {
|
||||
indexed_additions: IndexedAdditions {
|
||||
index_additions,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// We don't want other callers/threads to use this address while we're using it
|
||||
// but we also don't want to scan the tx we just created because it's not
|
||||
// technically in the blockchain yet.
|
||||
graph.index.mark_used(&change_keychain, index);
|
||||
(tx, Some((change_keychain, index)))
|
||||
} else {
|
||||
(tx, None)
|
||||
}
|
||||
};
|
||||
|
||||
match (broadcast)(&transaction) {
|
||||
Ok(_) => {
|
||||
println!("Broadcasted Tx : {}", transaction.txid());
|
||||
|
||||
let indexed_additions = graph.lock().unwrap().insert_tx(&transaction, None, None);
|
||||
|
||||
// We know the tx is at least unconfirmed now. Note if persisting here fails,
|
||||
// it's not a big deal since we can always find it again form
|
||||
// blockchain.
|
||||
db.lock().unwrap().stage(ChangeSet {
|
||||
indexed_additions,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some((keychain, index)) = change_index {
|
||||
// We failed to broadcast, so allow our change address to be used in the future
|
||||
graph.lock().unwrap().index.unmark_used(&keychain, index);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
) -> anyhow::Result<(
|
||||
Transaction,
|
||||
Option<(DerivationAdditions<Keychain>, (Keychain, u32))>,
|
||||
)>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut additions = DerivationAdditions::default();
|
||||
|
||||
let assets = bdk_tmp_plan::Assets {
|
||||
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// TODO use planning module
|
||||
let mut candidates = planned_utxos(graph, chain, &assets)?;
|
||||
|
||||
// apply coin selection algorithm
|
||||
match cs_algorithm {
|
||||
CoinSelectionAlgo::LargestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
|
||||
}
|
||||
CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
|
||||
CoinSelectionAlgo::OldestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
|
||||
}
|
||||
CoinSelectionAlgo::NewestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
|
||||
}
|
||||
CoinSelectionAlgo::BranchAndBound => {}
|
||||
}
|
||||
|
||||
// turn the txos we chose into weight and value
|
||||
let wv_candidates = candidates
|
||||
.iter()
|
||||
.map(|(plan, utxo)| {
|
||||
WeightedValue::new(
|
||||
utxo.txout.value,
|
||||
plan.expected_weight() as _,
|
||||
plan.witness_version().is_some(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut outputs = vec![TxOut {
|
||||
value,
|
||||
script_pubkey: address.script_pubkey(),
|
||||
}];
|
||||
|
||||
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
|
||||
Keychain::Internal
|
||||
} else {
|
||||
Keychain::External
|
||||
};
|
||||
|
||||
let ((change_index, change_script), change_additions) =
|
||||
graph.index.next_unused_spk(&internal_keychain);
|
||||
additions.append(change_additions);
|
||||
|
||||
// Clone to drop the immutable reference.
|
||||
let change_script = change_script.clone();
|
||||
|
||||
let change_plan = bdk_tmp_plan::plan_satisfaction(
|
||||
&graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.at_derivation_index(change_index),
|
||||
&assets,
|
||||
)
|
||||
.expect("failed to obtain change plan");
|
||||
|
||||
let mut change_output = TxOut {
|
||||
value: 0,
|
||||
script_pubkey: change_script,
|
||||
};
|
||||
|
||||
let cs_opts = CoinSelectorOpt {
|
||||
target_feerate: 0.5,
|
||||
min_drain_value: graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.dust_value(),
|
||||
..CoinSelectorOpt::fund_outputs(
|
||||
&outputs,
|
||||
&change_output,
|
||||
change_plan.expected_weight() as u32,
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: How can we make it easy to shuffle in order of inputs and outputs here?
|
||||
// apply coin selection by saying we need to fund these outputs
|
||||
let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
|
||||
|
||||
// just select coins in the order provided until we have enough
|
||||
// only use the first result (least waste)
|
||||
let selection = match cs_algorithm {
|
||||
CoinSelectionAlgo::BranchAndBound => {
|
||||
coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
|
||||
.map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
|
||||
}
|
||||
_ => coin_selector.select_until_finished()?,
|
||||
};
|
||||
let (_, selection_meta) = selection.best_strategy();
|
||||
|
||||
// get the selected utxos
|
||||
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
||||
|
||||
if let Some(drain_value) = selection_meta.drain_value {
|
||||
change_output.value = drain_value;
|
||||
// if the selection tells us to use change and the change value is sufficient, we add it as an output
|
||||
outputs.push(change_output)
|
||||
}
|
||||
|
||||
let mut transaction = Transaction {
|
||||
version: 0x02,
|
||||
lock_time: chain
|
||||
.get_chain_tip()?
|
||||
.and_then(|block_id| LockTime::from_height(block_id.height).ok())
|
||||
.unwrap_or(LockTime::ZERO)
|
||||
.into(),
|
||||
input: selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| TxIn {
|
||||
previous_output: utxo.outpoint,
|
||||
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
output: outputs,
|
||||
};
|
||||
|
||||
let prevouts = selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| utxo.txout.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let sighash_prevouts = Prevouts::All(&prevouts);
|
||||
|
||||
// first, set tx values for the plan so that we don't change them while signing
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
if let Some(sequence) = plan.required_sequence() {
|
||||
transaction.input[i].sequence = sequence
|
||||
}
|
||||
}
|
||||
|
||||
// create a short lived transaction
|
||||
let _sighash_tx = transaction.clone();
|
||||
let mut sighash_cache = SighashCache::new(&_sighash_tx);
|
||||
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
let requirements = plan.requirements();
|
||||
let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
|
||||
assert!(
|
||||
!requirements.requires_hash_preimages(),
|
||||
"can't have hash pre-images since we didn't provide any."
|
||||
);
|
||||
assert!(
|
||||
requirements.signatures.sign_with_keymap(
|
||||
i,
|
||||
keymap,
|
||||
&sighash_prevouts,
|
||||
None,
|
||||
None,
|
||||
&mut sighash_cache,
|
||||
&mut auth_data,
|
||||
&Secp256k1::default(),
|
||||
)?,
|
||||
"we should have signed with this input."
|
||||
);
|
||||
|
||||
match plan.try_complete(&auth_data) {
|
||||
bdk_tmp_plan::PlanState::Complete {
|
||||
final_script_sig,
|
||||
final_script_witness,
|
||||
} => {
|
||||
if let Some(witness) = final_script_witness {
|
||||
transaction.input[i].witness = witness;
|
||||
}
|
||||
|
||||
if let Some(script_sig) = final_script_sig {
|
||||
transaction.input[i].script_sig = script_sig;
|
||||
}
|
||||
}
|
||||
bdk_tmp_plan::PlanState::Incomplete(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"we weren't able to complete the plan with our keys."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some((additions, (internal_keychain, change_index)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>)>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?.unwrap_or_default();
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
.filter_map(
|
||||
#[allow(clippy::type_complexity)]
|
||||
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<ObservedAs<A>>), _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i);
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn handle_commands<C: clap::Subcommand, A: Anchor, O: ChainOracle, X>(
|
||||
graph: &Mutex<KeychainTxGraph<A>>,
|
||||
db: &Mutex<Database<A, X>>,
|
||||
chain: &O,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
network: Network,
|
||||
broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>,
|
||||
cmd: Commands<C>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
ChangeSet<A, X>: Default + Append + DeserializeOwned + Serialize,
|
||||
{
|
||||
match cmd {
|
||||
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
|
||||
Commands::Address { addr_cmd } => {
|
||||
let graph = &mut *graph.lock().unwrap();
|
||||
run_address_cmd(graph, db, network, addr_cmd)
|
||||
}
|
||||
Commands::Balance => {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
run_balance_cmd(graph, chain).map_err(anyhow::Error::from)
|
||||
}
|
||||
Commands::TxOut { txout_cmd } => {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
run_txo_cmd(graph, chain, network, txout_cmd)
|
||||
}
|
||||
Commands::Send {
|
||||
value,
|
||||
address,
|
||||
coin_select,
|
||||
} => run_send_cmd(
|
||||
graph,
|
||||
db,
|
||||
chain,
|
||||
keymap,
|
||||
coin_select,
|
||||
address,
|
||||
value,
|
||||
broadcast,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_index<C: clap::Subcommand, SC: secp256k1::Signing>(
|
||||
args: &Args<C>,
|
||||
secp: &Secp256k1<SC>,
|
||||
) -> anyhow::Result<(KeychainTxOutIndex<Keychain>, KeyMap)> {
|
||||
let mut index = KeychainTxOutIndex::<Keychain>::default();
|
||||
|
||||
let (descriptor, mut keymap) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, &args.descriptor)?;
|
||||
index.add_keychain(Keychain::External, descriptor);
|
||||
|
||||
if let Some((internal_descriptor, internal_keymap)) = args
|
||||
.change_descriptor
|
||||
.as_ref()
|
||||
.map(|desc_str| Descriptor::<DescriptorPublicKey>::parse_descriptor(secp, desc_str))
|
||||
.transpose()?
|
||||
{
|
||||
keymap.extend(internal_keymap);
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
Ok((index, keymap))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn init<'m, S: clap::Subcommand, A: Anchor, X>(
|
||||
db_magic: &'m [u8],
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<(
|
||||
Args<S>,
|
||||
KeyMap,
|
||||
Mutex<KeychainTxGraph<A>>,
|
||||
Mutex<Database<'m, A, X>>,
|
||||
X,
|
||||
)>
|
||||
where
|
||||
ChangeSet<A, X>: Default + Append + Serialize + DeserializeOwned,
|
||||
{
|
||||
if std::env::var("BDK_DB_PATH").is_err() {
|
||||
std::env::set_var("BDK_DB_PATH", db_default_path);
|
||||
}
|
||||
let args = Args::<S>::parse();
|
||||
let secp = Secp256k1::default();
|
||||
let (index, keymap) = prepare_index(&args, &secp)?;
|
||||
|
||||
let mut indexed_graph = IndexedTxGraph::<A, KeychainTxOutIndex<Keychain>>::new(index);
|
||||
|
||||
let mut db_backend =
|
||||
match Store::<'m, ChangeSet<A, X>>::new_from_path(db_magic, args.db_path.as_path()) {
|
||||
Ok(db_backend) => db_backend,
|
||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||
};
|
||||
|
||||
let ChangeSet {
|
||||
indexed_additions,
|
||||
extension,
|
||||
} = db_backend.load_from_persistence()?;
|
||||
indexed_graph.apply_additions(indexed_additions);
|
||||
|
||||
Ok((
|
||||
args,
|
||||
keymap,
|
||||
Mutex::new(indexed_graph),
|
||||
Mutex::new(Database::new(db_backend)),
|
||||
extension,
|
||||
))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user