diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 95197545f..3cc604b7e 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -43,6 +43,7 @@ pub mod coin_selection; pub mod export; pub mod signer; pub mod tx_builder; +pub mod tx_builder_2; pub(crate) mod utils; #[cfg(feature = "hardware-signer")] @@ -348,6 +349,14 @@ impl Wallet { .collect() } + pub fn list_unspent_by_keychain(&self) -> BTreeMap> { + let mut map = BTreeMap::default(); + for utxo in self.list_unspent() { + map.entry(utxo.keychain).or_insert(Vec::new()).push(utxo); + } + map + } + /// Get all the checkpoints the wallet is currently storing indexed by height. pub fn checkpoints(&self) -> &BTreeMap { self.keychain_tracker.chain().checkpoints() diff --git a/crates/bdk/src/wallet/tx_builder_2.rs b/crates/bdk/src/wallet/tx_builder_2.rs new file mode 100644 index 000000000..6cded1714 --- /dev/null +++ b/crates/bdk/src/wallet/tx_builder_2.rs @@ -0,0 +1,235 @@ +// Bitcoin Dev Kit +// Written in 2023 by The Bitcoin Dev Kit Developers +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use crate::collections::{HashMap, HashSet}; +use alloc::{boxed::Box, vec::Vec}; +use bdk_chain::ConfirmationTime; +use bitcoin::util::psbt::Input as PsbtInput; +use bitcoin::{OutPoint, Script, TxOut}; +use miniscript::{Descriptor, DescriptorPublicKey}; + +// TODO: replace with the miniscript one +// Clone is useful for tests, idk if it's going to be here irl +#[derive(Clone)] +pub struct Asset {} + +pub struct Plan {} + +// TODO: the generic for the plan_id might be overkill here +pub enum Utxo

{ + Planned { + outpoint: OutPoint, + txout: TxOut, + confirmation_time: ConfirmationTime, + plan_id: Option

, + }, + PsbtInput { + outpoint: OutPoint, + satisfaction_weight: u32, + psbt_input: PsbtInput, + }, +} + +#[derive(Default)] +pub struct CoinGroup

{ + utxos: HashMap>, + unspendable: HashSet, +} + +// Step 1 +pub struct CoinControl { + // group to utxos + utxos: HashMap>, + plans: HashMap, + #[allow(clippy::type_complexity)] + grouping_fn: Box) -> G>, +} + +impl

Default for CoinControl { + fn default() -> Self { + CoinControl { + grouping_fn: Box::new(|utxo| { + match utxo { + Utxo::Planned { txout, .. } => txout.script_pubkey.clone(), + Utxo::PsbtInput { + psbt_input, + outpoint, + .. + } => { + match (&psbt_input.witness_utxo, &psbt_input.non_witness_utxo) { + (Some(txout), _) => txout.script_pubkey.clone(), + (_, Some(tx)) => { + tx.output[outpoint.vout as usize].script_pubkey.clone() + } + (None, None) => { + // The user didn't give us enough info for us to figure out + // the spk + // We should add this coin to a fake group composed of only + // this coin + // I can probably do this with some weird hack, creating a fake + // spk from the hash of the outpoint, but honestly I don't love it + // and I'm looking for better solutions + todo!(); + } + } + } + } + }), + utxos: HashMap::default(), + plans: HashMap::default(), + } + } +} + +impl CoinControl { + pub fn add_planned_utxos( + &mut self, + utxos: impl IntoIterator, + _desc: Descriptor, + _assets: impl IntoIterator, + unspendable: HashSet, + ) { + let plan = Plan {}; + // TODO: if you can't obtain the plan, insert all into the unspendable list + let plan_id = self.plans.len(); + self.plans.insert(plan_id, plan); + for (outpoint, txout, confirmation_time) in utxos { + let utxo = Utxo::Planned { + outpoint, + txout, + confirmation_time, + plan_id: Some(plan_id), + }; + let group_id = (self.grouping_fn)(&utxo); + let group = &mut self.utxos.entry(group_id).or_insert(CoinGroup::default()); + group.utxos.insert(outpoint, utxo); + if unspendable.contains(&outpoint) { + group.unspendable.insert(outpoint); + } + } + } + + pub fn add_psbt_inputs( + &mut self, + utxos: impl IntoIterator, + unspendable: HashSet, + ) { + for (outpoint, satisfaction_weight, psbt_input) in utxos { + let utxo = Utxo::PsbtInput { + outpoint, + satisfaction_weight, + psbt_input, + }; + let group_id = (self.grouping_fn)(&utxo); + let group = &mut self.utxos.entry(group_id).or_insert(CoinGroup::default()); + group.utxos.insert(outpoint, utxo); + if unspendable.contains(&outpoint) { + group.unspendable.insert(outpoint); + } + } + } + + pub fn as_candidates(&self, _include_partial_groups: bool) -> Vec> { + todo!() + } +} + +pub struct UtxoGroupCandidate { + pub group_id: G, + pub cs_candidate: Candidate, +} + +impl AsRef for UtxoGroupCandidate { + fn as_ref(&self) -> &Candidate { + &self.cs_candidate + } +} + +/// A `Candidate` represents an input candidate for `CoinSelector`. This can either be a +/// single UTXO, or a group of UTXOs that should be spent together. +#[derive(Debug, Clone, Copy)] +pub struct Candidate { + /// Total value of the UTXO(s) that this [`Candidate`] represents. + pub value: u64, + /// Total weight of including this/these UTXO(s). + /// `txin` fields: `prevout`, `nSequence`, `scriptSigLen`, `scriptSig`, `scriptWitnessLen`, + /// `scriptWitness` should all be included. + pub weight: u32, + /// Total number of inputs; so we can calculate extra `varint` weight due to `vin` len changes. + pub input_count: usize, + /// Whether this [`Candidate`] contains at least one segwit spend. + pub is_segwit: bool, +} + +impl AsRef for Candidate { + fn as_ref(&self) -> &Candidate { + self + } +} + +// This is step 0 which will probably be reworked 10 more times, so I'm commenting it +/* +pub struct TxParams { + pub recipients: Vec<(Script, AmountOrDrain)>, + pub fee_policy: FeePolicy, + pub change_keychain: K, + pub sighash: psbt::PsbtSighashType, + pub locktime: LockTime, + pub rbf: RbfValue, + pub version: Version, + pub current_height: Option, +} + +pub enum AmountOrDrain { + Amount(Amount), + // TODO: maybe drain could have a percentage, so you can drain 60% + // to A and 40% to B. + // At the moment if you put multiple drains the amount will be split + // equally between the drain recipients. + Drain, +} + +/// Transaction version +/// +/// Has a default value of `1` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub struct Version(i32); + +impl Default for Version { + fn default() -> Self { + Version(1) + } +} + +/// RBF nSequence value +/// +/// Has a default value of `0xFFFFFFFD` +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] +pub enum RbfValue { + Default, + Value(Sequence), +} + +impl RbfValue { + pub(crate) fn get_value(&self) -> Sequence { + match self { + RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME, + RbfValue::Value(v) => *v, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum FeePolicy { + FeeRate(FeeRate), + FeeAmount(u64), +} +*/ diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 94c5ad1e3..c630ae1db 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -3326,3 +3326,25 @@ fn test_tx_cancellation() { .unwrap(); assert_eq!(change_derivation_4, (KeychainKind::Internal, 2)); } + +// TODO: this is not really a test right now, just a showcase of the API +#[test] +fn test_new_tx_builder() { + use bdk::wallet::tx_builder_2::*; + use bdk_chain::collections::HashSet; + + let (wallet, _) = + get_funded_wallet_with_change(get_test_wpkh(), Some(get_test_tr_single_sig_xprv())); + + let descriptors = wallet.keychains(); + let coins_by_keychain = wallet.list_unspent_by_keychain(); + let mut coin_control = CoinControl::default(); + let assets = vec![Asset {}]; + for (keychain, coins) in coins_by_keychain { + let desc = descriptors.get(&keychain).unwrap(); + let coins = coins + .into_iter() + .map(|lu| (lu.outpoint, lu.txout, lu.confirmation_time)); + coin_control.add_planned_utxos(coins, desc.clone(), assets.clone(), HashSet::default()); + } +}