From 7b3d4eb5cf9bcec84148d475ffecb4a1ba9e906a Mon Sep 17 00:00:00 2001 From: Tibo-lg Date: Wed, 20 Sep 2023 13:14:12 +0900 Subject: [PATCH] Ensure that a change output is generated --- bitcoin-rpc-provider/src/lib.rs | 16 ++- dlc-manager/src/contract_updater.rs | 52 +++++--- dlc-manager/src/lib.rs | 10 +- dlc-manager/src/utils.rs | 30 ++++- dlc-manager/test_inputs/offer_contract2.json | 120 +++++++++++++++++ dlc/Cargo.toml | 1 + dlc/src/lib.rs | 31 ++--- dlc/src/util.rs | 30 ++++- mocks/src/mock_wallet.rs | 60 ++++----- simple-wallet/src/lib.rs | 130 ++++++++++--------- 10 files changed, 338 insertions(+), 142 deletions(-) create mode 100644 dlc-manager/test_inputs/offer_contract2.json diff --git a/bitcoin-rpc-provider/src/lib.rs b/bitcoin-rpc-provider/src/lib.rs index 43b2e217..095a1c2f 100644 --- a/bitcoin-rpc-provider/src/lib.rs +++ b/bitcoin-rpc-provider/src/lib.rs @@ -14,7 +14,7 @@ use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::{ consensus::Decodable, network::constants::Network, Amount, PrivateKey, Transaction, Txid, }; -use bitcoin::{Address, OutPoint, ScriptBuf, TxOut}; +use bitcoin::{Address, OutPoint, Script, ScriptBuf, TxOut}; use bitcoincore_rpc::jsonrpc::serde_json; use bitcoincore_rpc::jsonrpc::serde_json::Value; use bitcoincore_rpc::{json, Auth, Client, RpcApi}; @@ -285,6 +285,7 @@ impl Wallet for BitcoinCoreProvider { amount: u64, _fee_rate: u64, lock_utxos: bool, + _change_script: &Script, ) -> Result, ManagerError> { let client = self.client.lock().unwrap(); let utxo_res = client @@ -391,11 +392,18 @@ impl Wallet for BitcoinCoreProvider { } fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), ManagerError> { - match self.client.lock().unwrap().unlock_unspent(outpoints).map_err(rpc_err_to_manager_err)? { + match self + .client + .lock() + .unwrap() + .unlock_unspent(outpoints) + .map_err(rpc_err_to_manager_err)? + { true => Ok(()), - false => Err(ManagerError::StorageError(format!("Failed to unlock utxos: {outpoints:?}"))) + false => Err(ManagerError::StorageError(format!( + "Failed to unlock utxos: {outpoints:?}" + ))), } - } } diff --git a/dlc-manager/src/contract_updater.rs b/dlc-manager/src/contract_updater.rs index 2f42a49c..87f186d9 100644 --- a/dlc-manager/src/contract_updater.rs +++ b/dlc-manager/src/contract_updater.rs @@ -756,38 +756,52 @@ where #[cfg(test)] mod tests { - use std::rc::Rc; - - use mocks::dlc_manager::contract::offered_contract::OfferedContract; + use dlc_messages::OfferDlc; + use mocks::{ + dlc_manager::contract::offered_contract::OfferedContract, mock_wallet::MockWallet, + }; use secp256k1_zkp::PublicKey; - #[test] - fn accept_contract_test() { - let offer_dlc = - serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap(); + fn fee_computation_test_common(offer_dlc: OfferDlc, utxo_values: &[u64]) -> MockWallet { let dummy_pubkey: PublicKey = "02e6642fd69bd211f93f7f1f36ca51a26a5290eb2dd1b0d8279a87bb0d480c8443" .parse() .unwrap(); let offered_contract = OfferedContract::try_from_offer_dlc(&offer_dlc, dummy_pubkey, [0; 32]).unwrap(); - let blockchain = Rc::new(mocks::mock_blockchain::MockBlockchain::new()); - let fee_rate: u64 = offered_contract.fee_rate_per_vb; - let utxo_value: u64 = offered_contract.total_collateral - - offered_contract.offer_params.collateral - + crate::utils::get_half_common_fee(fee_rate).unwrap(); - let wallet = Rc::new(mocks::mock_wallet::MockWallet::new( - &blockchain, - &[utxo_value, 10000], - )); + let blockchain = mocks::mock_blockchain::MockBlockchain::new(); + let wallet = MockWallet::new(&blockchain, utxo_values); mocks::dlc_manager::contract_updater::accept_contract( secp256k1_zkp::SECP256K1, &offered_contract, - &wallet, - &wallet, - &blockchain, + &&wallet, + &&wallet, + &&blockchain, ) .expect("Not to fail"); + wallet + } + + #[test] + fn with_exact_value_utxo_doesnt_fail() { + let offer_dlc: OfferDlc = + serde_json::from_str(include_str!("../test_inputs/offer_contract.json")).unwrap(); + let fee_rate: u64 = offer_dlc.fee_rate_per_vb; + let utxo_value: u64 = offer_dlc.contract_info.get_total_collateral() + - offer_dlc.offer_collateral + + crate::utils::get_half_common_fee(fee_rate).unwrap(); + fee_computation_test_common(offer_dlc, &[utxo_value, 10000]); + } + + #[test] + fn with_no_change_utxo_enforce_change_output() { + let offer_dlc: OfferDlc = + serde_json::from_str(include_str!("../test_inputs/offer_contract2.json")).unwrap(); + let wallet = fee_computation_test_common(offer_dlc, &[136015, 40000]); + let utxos = wallet.utxos.lock().unwrap(); + for utxo in utxos.iter() { + assert!(utxo.reserved); + } } } diff --git a/dlc-manager/src/lib.rs b/dlc-manager/src/lib.rs index 524c6951..276a5d82 100644 --- a/dlc-manager/src/lib.rs +++ b/dlc-manager/src/lib.rs @@ -37,7 +37,7 @@ pub mod payout_curve; mod utils; use bitcoin::psbt::PartiallySignedTransaction; -use bitcoin::{Address, Block, OutPoint, ScriptBuf, Transaction, TxOut, Txid}; +use bitcoin::{Address, Block, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid}; use chain_monitor::ChainMonitor; use channel::offered_channel::OfferedChannel; use channel::signed_channel::{SignedChannel, SignedChannelStateType}; @@ -152,12 +152,18 @@ pub trait Wallet { fn get_new_address(&self) -> Result; /// Returns a new (unused) change address. fn get_new_change_address(&self) -> Result; - /// Get a set of UTXOs to fund the given amount. + /// Get a set of UTXOs to fund the given amount. The implementation is expected to take into + /// account the cost of the inputs that are selected. For the protocol to be secure, it is + /// required that each party has a change output on the funding transaction to be able to bump + /// the fee in case of network congestion. If the total value of the returned UTXO is to small + /// to have a change output, the library will call the method again with a higer `amount` to + /// ensure having a change output. fn get_utxos_for_amount( &self, amount: u64, fee_rate: u64, lock_utxos: bool, + change_script: &Script, ) -> Result, Error>; /// Import the provided address. fn import_address(&self, address: &Address) -> Result<(), Error>; diff --git a/dlc-manager/src/utils.rs b/dlc-manager/src/utils.rs index ec9934a5..d8473014 100644 --- a/dlc-manager/src/utils.rs +++ b/dlc-manager/src/utils.rs @@ -2,7 +2,10 @@ use std::ops::Deref; use bitcoin::{consensus::Encodable, Txid}; -use dlc::{PartyParams, TxInputInfo}; +use dlc::{ + util::{get_inputs_and_change_weight, weight_to_fee}, + PartyParams, TxInputInfo, +}; use dlc_messages::{ oracle_msgs::{OracleAnnouncement, OracleAttestation}, FundingInput, @@ -83,7 +86,30 @@ where // Add base cost of fund tx + CET / 2 and a CET output to the collateral. let appr_required_amount = own_collateral + get_half_common_fee(fee_rate)? + dlc::util::weight_to_fee(124, fee_rate)?; - let utxos = wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true)?; + let mut utxos = + wallet.get_utxos_for_amount(appr_required_amount, fee_rate, true, &change_spk)?; + let total_value: u64 = utxos.iter().map(|x| x.tx_out.value).sum(); + let min_change_value = change_addr.script_pubkey().dust_value().to_sat(); + let (inputs_weight, change_weight) = get_inputs_and_change_weight( + &utxos + .iter() + .map(|x| (x.tx_out.script_pubkey.as_ref(), 107)) + .collect::>(), + &change_spk, + )?; + let inputs_fee = weight_to_fee(inputs_weight, fee_rate)?; + let change_fee = weight_to_fee(change_weight, fee_rate)?; + // We need to have a change output, if we didn't on first try, we request an amount which + // includes minimum value for the change output as well as the fee for it. + if total_value < appr_required_amount + min_change_value + inputs_fee + change_fee { + wallet.unreserve_utxos(&utxos.iter().map(|x| x.outpoint).collect::>())?; + utxos = wallet.get_utxos_for_amount( + appr_required_amount + min_change_value + change_fee, + fee_rate, + true, + &change_spk, + )?; + } let mut funding_inputs: Vec = Vec::new(); let mut funding_tx_info: Vec = Vec::new(); diff --git a/dlc-manager/test_inputs/offer_contract2.json b/dlc-manager/test_inputs/offer_contract2.json new file mode 100644 index 00000000..8acf783a --- /dev/null +++ b/dlc-manager/test_inputs/offer_contract2.json @@ -0,0 +1,120 @@ +{ + "protocolVersion":1, + "contractFlags":0, + "chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "temporaryContractId":"f6a1b2841c93db06e94200b227bb4bdea83068efa557d68e14775237cbaab56a", + "contractInfo":{ + "singleContractInfo":{ + "totalCollateral":120000, + "contractInfo":{ + "contractDescriptor":{ + "numericOutcomeContractDescriptor":{ + "numDigits":10, + "payoutFunction":{ + "payoutFunctionPieces":[ + { + "endPoint":{ + "eventOutcome":0, + "outcomePayout":0, + "extraPrecision":0 + }, + "payoutCurvePiece":{ + "polynomialPayoutCurvePiece":{ + "payoutPoints":[ + { + "eventOutcome":3, + "outcomePayout":70000, + "extraPrecision":0 + } + ] + } + } + }, + { + "endPoint":{ + "eventOutcome":5, + "outcomePayout":120000, + "extraPrecision":0 + }, + "payoutCurvePiece":{ + "polynomialPayoutCurvePiece":{ + "payoutPoints":[ + + ] + } + } + } + ], + "lastEndpoint":{ + "eventOutcome":1023, + "outcomePayout":120000, + "extraPrecision":0 + } + }, + "roundingIntervals":{ + "intervals":[ + { + "beginInterval":0, + "roundingMod":1 + } + ] + } + } + }, + "oracleInfo":{ + "single":{ + "oracleAnnouncement":{ + "announcementSignature":"18e18de8b3547e210addd32589db9520286f55c0c18510c67bb6f8ea66b05154b84c6ec0075e3623f886b7e2bc623b7df25e1bc25d1cc87c622b28f0ae526664", + "oraclePublicKey":"1d524d2753a36ebe340af67370f78219b4dbb6f56d2f96b3b21eaabec6f4a114", + "oracleEvent":{ + "oracleNonces":[ + "bc927a2c8bf43c9d208e679848ffaf95d178fdbd2e29d1c66668f21dd75149e8", + "9ed74e19c1d532f5127829b7d9f183e0738ad084485428b53a7fe0c50f2efe5e", + "f44733d1129d0cd9253124749f8cff2c7e7eecd79888a4a015d3e3ad153ef282", + "f4f39e5733bfc5ca18530eb444419b31d9dc0ec938502615c33f2b0b7c05ac71", + "930991374fbf6b9a49e5e16fa3c5c39638af58f5a4c55682a93b2b940502e7bf", + "e3af3b59907c349d627e3f4f20125bdc1e979cac41ee82ef0a184000c79e904b", + "0b95d4335713752329a1791b963d526c0a49873bbbfcad9e1c03881508b2a801", + "48776cc1e3b8f3ff7fd6226ea2df5607787913468a1c0faad4ff315b7cf3b41d", + "0b39b0e1a14f5f50cb05f0a6d8e7c082f75e9fe386006727af933ce4d273a76f", + "479a38e13c1622bfd53299ee67680d7a0edd3fed92223e3a878c8d010fcc1a2d" + ], + "eventMaturityEpoch":1623133104, + "eventDescriptor":{ + "digitDecompositionEvent":{ + "base":2, + "isSigned":false, + "unit":"sats/sec", + "precision":0, + "nbDigits":10 + } + }, + "eventId":"Test" + } + } + } + } + } + } + }, + "fundingPubkey":"02556021f6abda2ae7a74d38a4e4a3b00c1dd648db96397dcd8642c3d0b0b139d1", + "payoutSpk":"0014430af74f2f9dc88729fd02eaeb946fc161e2be1e", + "payoutSerialId":8165863461276958928, + "offerCollateral":60000, + "fundingInputs":[ + { + "inputSerialId":11632658032743242199, + "prevTx":"02000000000101e79f7a30bb35206060eb09a99b6956bcdc7a1767b310c8dfde3595c69246a60e0000000000feffffff0200c2eb0b000000001600142a416c1e5f5e78bc6c518294fd1dd86b40eed2d77caf953e000000001600148e56705661334df89b2c1c7c4e41da9cef9eb38e0247304402201491f05ebe196b333420cbab3e7e7f3e431bfe91a42730cef9c6e64b0e8ff62302202c5fc79abbdb0a1c8ad422dbb97a54693feedc580f0cb7a62bdadaecbfc4f9430121035f57172a38f35f29f4357dcc2d24ea8e72638cf43190e4fdcb3f0ace215cfd5602020000", + "prevTxVout":0, + "sequence":4294967295, + "maxWitnessLen":107, + "redeemScript":"" + } + ], + "changeSpk":"001441ca183be469eab996f34ed31197a96b57f6050e", + "changeSerialId":16919534260907952016, + "fundOutputSerialId":5054305248376932341, + "feeRatePerVb":400, + "cetLocktime":1623133103, + "refundLocktime":1623737904 +} diff --git a/dlc/Cargo.toml b/dlc/Cargo.toml index ff79451a..ee7560da 100644 --- a/dlc/Cargo.toml +++ b/dlc/Cargo.toml @@ -1,6 +1,7 @@ [package] authors = ["Crypto Garage"] description = "Creation, signing and verification of Discreet Log Contracts (DLC) transactions." +edition = "2018" homepage = "https://github.com/p2pderivatives/rust-dlc" license-file = "../LICENSE" name = "dlc" diff --git a/dlc/src/lib.rs b/dlc/src/lib.rs index b29d1f7a..7f5a41d1 100644 --- a/dlc/src/lib.rs +++ b/dlc/src/lib.rs @@ -80,6 +80,10 @@ macro_rules! checked_add { }; } +pub(crate) use checked_add; + +use crate::util::get_inputs_and_change_weight; + /// Represents the payouts for a unique contract outcome. Offer party represents /// the initiator of the contract while accept party represents the party /// accepting the contract. @@ -282,25 +286,14 @@ impl PartyParams { fee_rate_per_vb: u64, extra_fee: u64, ) -> Result<(TxOut, u64, u64), Error> { - let mut inputs_weight: usize = 0; - - for w in &self.inputs { - let script_weight = util::redeem_script_to_script_sig(&w.redeem_script) - .len() - .checked_mul(4) - .ok_or(Error::InvalidArgument)?; - inputs_weight = checked_add!( - inputs_weight, - TX_INPUT_BASE_WEIGHT, - script_weight, - w.max_witness_len - )?; - } - - // Value size + script length var_int + ouput script pubkey size - let change_size = self.change_script_pubkey.len(); - // Change size is scaled by 4 from vBytes to weight units - let change_weight = change_size.checked_mul(4).ok_or(Error::InvalidArgument)?; + let (inputs_weight, change_weight) = get_inputs_and_change_weight( + &self + .inputs + .iter() + .map(|x| (x.redeem_script.as_ref(), x.max_witness_len)) + .collect::>(), + &self.change_script_pubkey, + )?; // Base weight (nLocktime, nVersion, ...) is distributed among parties // independently of inputs contributed diff --git a/dlc/src/util.rs b/dlc/src/util.rs index bd910926..e715b70c 100644 --- a/dlc/src/util.rs +++ b/dlc/src/util.rs @@ -9,7 +9,7 @@ use bitcoin::{ use bitcoin::{ScriptBuf, Sequence, Witness}; use secp256k1_zkp::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey, Signing}; -use crate::Error; +use crate::{checked_add, Error}; // Setting the nSequence for every input of a transaction to this value disables // both RBF and nLockTime usage. @@ -256,3 +256,31 @@ pub fn validate_fee_rate(fee_rate_per_vb: u64) -> Result<(), Error> { Ok(()) } + +/// Computes the total weight of the given inputs and change. +pub fn get_inputs_and_change_weight( + inputs: &[(&Script, usize)], + change_spk: &Script, +) -> Result<(usize, usize), Error> { + let mut inputs_weight: usize = 0; + + for w in inputs { + let script_weight = redeem_script_to_script_sig(w.0) + .len() + .checked_mul(4) + .ok_or(Error::InvalidArgument)?; + inputs_weight = checked_add!( + inputs_weight, + crate::TX_INPUT_BASE_WEIGHT, + script_weight, + w.1 + )?; + } + + // Value size + script length var_int + ouput script pubkey size + let change_size = change_spk.len(); + // Change size is scaled by 4 from vBytes to weight units + let change_weight = change_size.checked_mul(4).ok_or(Error::InvalidArgument)?; + + Ok((inputs_weight, change_weight)) +} diff --git a/mocks/src/mock_wallet.rs b/mocks/src/mock_wallet.rs index 01ac6e63..dd194ee0 100644 --- a/mocks/src/mock_wallet.rs +++ b/mocks/src/mock_wallet.rs @@ -1,18 +1,18 @@ -use std::rc::Rc; +use std::sync::Mutex; use bitcoin::psbt::PartiallySignedTransaction; -use bitcoin::{absolute::LockTime, Address, OutPoint, ScriptBuf, Transaction, TxOut}; +use bitcoin::{absolute::LockTime, Address, OutPoint, Script, ScriptBuf, Transaction, TxOut}; use dlc_manager::{error::Error, Blockchain, ContractSignerProvider, SimpleSigner, Utxo, Wallet}; -use secp256k1_zkp::{rand::seq::SliceRandom, PublicKey, SecretKey}; +use secp256k1_zkp::{PublicKey, SecretKey}; use crate::mock_blockchain::MockBlockchain; pub struct MockWallet { - utxos: Vec, + pub utxos: Mutex>, } impl MockWallet { - pub fn new(blockchain: &Rc, utxo_values: &[u64]) -> Self { + pub fn new(blockchain: &MockBlockchain, utxo_values: &[u64]) -> Self { let mut utxos = Vec::with_capacity(utxo_values.len()); for utxo_value in utxo_values { @@ -41,7 +41,9 @@ impl MockWallet { utxos.push(utxo); } - Self { utxos } + Self { + utxos: Mutex::new(utxos), + } } } @@ -77,34 +79,22 @@ impl Wallet for MockWallet { fn get_utxos_for_amount( &self, amount: u64, - _fee_rate: u64, - _lock_utxos: bool, + fee_rate: u64, + lock_utxos: bool, + change_spk: &Script, ) -> Result, Error> { - let mut utxo_pool = self.utxos.clone(); - let seed = 1; - utxo_pool.shuffle(&mut secp256k1_zkp::rand::rngs::mock::StepRng::new( - seed, seed, - )); - - let mut sum = 0; - - let res = utxo_pool - .iter() - .take_while(|x| { - if sum >= amount { - return false; - } - sum += x.tx_out.value; - true - }) - .cloned() - .collect(); - - if sum >= amount { - return Ok(res); + let mut utxos = self.utxos.lock().unwrap(); + let res = simple_wallet::select_coins(&utxos, fee_rate, amount, change_spk)?; + if lock_utxos { + for s in &res { + utxos + .iter_mut() + .find(|x| x.tx_out == s.tx_out && x.outpoint == s.outpoint) + .unwrap() + .reserved = true; + } } - - Err(Error::InvalidParameters("Not enought UTXOs".to_string())) + Ok(res) } fn import_address(&self, _address: &Address) -> Result<(), dlc_manager::error::Error> { @@ -115,7 +105,11 @@ impl Wallet for MockWallet { Ok(()) } - fn unreserve_utxos(&self, _outpoints: &[OutPoint]) -> Result<(), Error> { + fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), Error> { + let mut pool = self.utxos.lock().unwrap(); + for s in outpoints { + pool.iter_mut().find(|x| &x.outpoint == s).unwrap().reserved = false; + } Ok(()) } } diff --git a/simple-wallet/src/lib.rs b/simple-wallet/src/lib.rs index 73761641..02a74f9c 100644 --- a/simple-wallet/src/lib.rs +++ b/simple-wallet/src/lib.rs @@ -5,11 +5,10 @@ use bdk::{ wallet::coin_selection::{BranchAndBoundCoinSelection, CoinSelectionAlgorithm}, FeeRate, KeychainKind, LocalUtxo, Utxo as BdkUtxo, WeightedUtxo, }; +use bitcoin::{psbt::PartiallySignedTransaction, ScriptBuf}; use bitcoin::{ - hashes::Hash, Address, Network, OutPoint, Script, Sequence, Transaction, TxIn, TxOut, Txid, - Witness, + Address, Network, OutPoint, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, }; -use bitcoin::{psbt::PartiallySignedTransaction, ScriptBuf}; use dlc_manager::{ error::Error, Blockchain, ContractSignerProvider, KeysId, SimpleSigner, Utxo, Wallet, }; @@ -244,51 +243,18 @@ where amount: u64, fee_rate: u64, lock_utxos: bool, + change_script: &Script, ) -> Result> { let org_utxos = self.storage.get_utxos()?; - let utxos = org_utxos - .iter() - .filter(|x| !x.reserved) - .map(|x| WeightedUtxo { - utxo: BdkUtxo::Local(LocalUtxo { - outpoint: x.outpoint, - txout: x.tx_out.clone(), - keychain: KeychainKind::External, - is_spent: false, - }), - satisfaction_weight: 107, - }) - .collect::>(); - let coin_selection = BranchAndBoundCoinSelection::default(); - let dummy_pubkey: PublicKey = - "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" - .parse() - .unwrap(); - let dummy_drain = - ScriptBuf::new_v0_p2wpkh(&bitcoin::WPubkeyHash::hash(&dummy_pubkey.serialize())); - let fee_rate = FeeRate::from_sat_per_vb(fee_rate as f32); - let selection = coin_selection - .coin_select(self, Vec::new(), utxos, fee_rate, amount, &dummy_drain) - .map_err(|e| Error::WalletError(Box::new(e)))?; - let mut res = Vec::new(); - for utxo in selection.selected { - let local_utxo = if let BdkUtxo::Local(l) = utxo { - l - } else { - panic!(); - }; - let org = org_utxos - .iter() - .find(|x| x.tx_out == local_utxo.txout && x.outpoint == local_utxo.outpoint) - .unwrap(); - if lock_utxos { - let updated = Utxo { + + let res = select_coins(&org_utxos, fee_rate, amount, change_script)?; + if lock_utxos { + for r in &res { + self.storage.upsert_utxo(&Utxo { reserved: true, - ..org.clone() - }; - self.storage.upsert_utxo(&updated)?; + ..r.clone() + })?; } - res.push(org.clone()); } Ok(res) } @@ -297,14 +263,6 @@ where Ok(()) } - fn unreserve_utxos(&self, outputs: &[OutPoint]) -> std::result::Result<(), Error> { - for outpoint in outputs { - self.storage.unreserve_utxo(&outpoint.txid, outpoint.vout)?; - } - - Ok(()) - } - fn sign_psbt_input( &self, psbt: &mut PartiallySignedTransaction, @@ -350,13 +308,65 @@ where psbt.inputs[input_index].final_script_witness = Some(tx_input.witness); Ok(()) } + + fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<()> { + for outpoint in outpoints { + self.storage.unreserve_utxo(&outpoint.txid, outpoint.vout)?; + } + Ok(()) + } } -impl BatchOperations for SimpleWallet -where - B::Target: WalletBlockchainProvider, - W::Target: WalletStorage, -{ +pub fn select_coins( + utxos: &[Utxo], + fee_rate: u64, + target_amount: u64, + change_script: &Script, +) -> Result> { + let weighted_utxos = utxos + .iter() + .filter(|x| !x.reserved) + .map(|x| WeightedUtxo { + utxo: BdkUtxo::Local(LocalUtxo { + outpoint: x.outpoint, + txout: x.tx_out.clone(), + keychain: KeychainKind::External, + is_spent: false, + }), + satisfaction_weight: 107, + }) + .collect::>(); + let coin_selection = BranchAndBoundCoinSelection::default(); + let fee_rate = FeeRate::from_sat_per_vb(fee_rate as f32); + let selection = coin_selection + .coin_select( + &DummyDB {}, + Vec::new(), + weighted_utxos, + fee_rate, + target_amount, + change_script, + ) + .map_err(|e| Error::WalletError(Box::new(e)))?; + let mut res = Vec::new(); + for utxo in selection.selected { + let local_utxo = if let BdkUtxo::Local(l) = utxo { + l + } else { + panic!(); + }; + let org = utxos + .iter() + .find(|x| x.tx_out == local_utxo.txout && x.outpoint == local_utxo.outpoint) + .unwrap(); + res.push(org.clone()); + } + Ok(res) +} + +struct DummyDB {} + +impl BatchOperations for DummyDB { fn set_script_pubkey( &mut self, _: &Script, @@ -438,11 +448,7 @@ where } } -impl Database for SimpleWallet -where - B::Target: WalletBlockchainProvider, - W::Target: WalletStorage, -{ +impl Database for DummyDB { fn check_descriptor_checksum>( &mut self, _: bdk::KeychainKind,