diff --git a/Cargo.toml b/Cargo.toml index f880b91162..d5d73fc56e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightn bdk_chain = { version = "0.23.3", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.2", default-features = false, features = ["async-https-rustls", "tokio"], optional = true } bdk_electrum = { version = "0.24.0", default-features = false, features = ["use-rustls-ring"], optional = true } -bdk_wallet = { version = "3.1.0", default-features = false, features = ["std", "keys-bip39"]} +bdk_wallet = { version = "3.2.0", default-features = false, features = ["std", "keys-bip39"]} bitreq = { version = "0.3", default-features = false, features = ["async-https", "json-using-serde"] } rustls = { version = "0.23", default-features = false } @@ -118,7 +118,7 @@ tokio = { version = "1.39", default-features = false, features = [ "rt-multi-thr tokio-util = { version = "0.7.10", default-features = false, features = ["rt"] } esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"], optional = true } ldk-esplora-client = { package = "esplora-client", version = "0.13", default-features = false, features = ["tokio", "async-https-rustls"], optional = true } -electrum-client = { version = "0.25", default-features = false, features = ["proxy", "use-rustls-ring"], optional = true } +electrum-client = { version = "0.25.1", default-features = false, features = ["proxy", "use-rustls-ring"], optional = true } libc = "0.2" uniffi = { version = "0.29.5", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } diff --git a/src/builder.rs b/src/builder.rs index ddf9104cc2..21f15508b4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -17,6 +17,8 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; +use bdk_wallet::descriptor::IntoWalletDescriptor; +use bdk_wallet::signer::SignersContainer; use bdk_wallet::template::Bip84; use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; use bitcoin::bip32::{ChildNumber, Xpriv}; @@ -1758,17 +1760,30 @@ fn build_with_store_internal( BuildError::WalletSetupFailed })?; - let descriptor = Bip84(xprv, KeychainKind::External); - let change_descriptor = Bip84(xprv, KeychainKind::Internal); + let secp_ctx = Secp256k1::new(); + let (descriptor, keymap) = Bip84(xprv, KeychainKind::External) + .into_wallet_descriptor(&secp_ctx, config.network.into()) + .map_err(|e| { + log_error!(logger, "Failed to derive external wallet descriptor: {}", e); + BuildError::WalletSetupFailed + })?; + let (change_descriptor, change_keymap) = Bip84(xprv, KeychainKind::Internal) + .into_wallet_descriptor(&secp_ctx, config.network.into()) + .map_err(|e| { + log_error!(logger, "Failed to derive internal wallet descriptor: {}", e); + BuildError::WalletSetupFailed + })?; + let signers = [ + SignersContainer::build(keymap, &descriptor, &secp_ctx), + SignersContainer::build(change_keymap, &change_descriptor, &secp_ctx), + ]; let mut wallet_persister = KVStoreWalletPersister::new(Arc::clone(&kv_store), Arc::clone(&logger)); - #[allow(deprecated)] let wallet_opt = runtime .block_on(async { BdkWallet::load() .descriptor(KeychainKind::External, Some(descriptor.clone())) .descriptor(KeychainKind::Internal, Some(change_descriptor.clone())) - .extract_keys() .check_network(config.network) .load_wallet_async(&mut wallet_persister) .await @@ -1957,6 +1972,7 @@ fn build_with_store_internal( let wallet = Arc::new(Wallet::new( bdk_wallet, + signers, wallet_persister, persisted_pool_indices, Arc::clone(&tx_broadcaster), diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 86025998e8..c19861e739 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -919,14 +919,98 @@ impl Filter for ElectrumRuntimeClient { #[cfg(test)] mod tests { + use std::io::{BufRead, BufReader, Write}; + use std::net::TcpListener; use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::mpsc; + use std::sync::{mpsc, Barrier}; use std::thread; use bitcoin::blockdata::constants::genesis_block; use super::*; + #[test] + fn clean_eof_wakes_concurrent_electrum_requests() { + const REQUEST_COUNT: usize = 8; + for _ in 0..10 { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let server_address = listener.local_addr().unwrap(); + let server = thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + let mut reader = BufReader::new(stream); + let mut version_request = String::new(); + assert!(reader.read_line(&mut version_request).unwrap() > 0); + let version_request: serde_json::Value = + serde_json::from_str(&version_request).unwrap(); + let version_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": version_request["id"], + "result": ["test-server", "1.4"], + }); + writeln!(reader.get_mut(), "{version_response}").unwrap(); + reader.get_mut().flush().unwrap(); + reader.get_mut().set_read_timeout(Some(Duration::from_millis(250))).unwrap(); + + let mut requests_received = 0; + while requests_received < 2 { + let mut request = String::new(); + match reader.read_line(&mut request) { + Ok(0) => break, + Ok(_) => requests_received += 1, + Err(e) + if matches!( + e.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + break + }, + Err(e) => panic!("failed to read Electrum request: {e}"), + } + } + if requests_received > 1 { + thread::sleep(Duration::from_millis(50)); + } + requests_received + }); + + let config = ElectrumConfigBuilder::new().retry(0).build(); + let client = Arc::new( + ElectrumClient::from_config(&format!("tcp://{server_address}"), config).unwrap(), + ); + let start = Arc::new(Barrier::new(REQUEST_COUNT + 1)); + let (finished_sender, finished_receiver) = mpsc::channel(); + let mut requests = Vec::new(); + for _ in 0..REQUEST_COUNT { + let client = Arc::clone(&client); + let start = Arc::clone(&start); + let finished_sender = finished_sender.clone(); + requests.push(thread::spawn(move || { + start.wait(); + let result = + client.raw_call("server.ping", Vec::::new()); + finished_sender.send(result.is_err()).unwrap(); + })); + } + start.wait(); + let requests_received = server.join().unwrap(); + + for _ in 0..REQUEST_COUNT { + let failed_as_expected = finished_receiver + .recv_timeout(Duration::from_secs(5)) + .expect("clean EOF must wake every concurrent Electrum request"); + assert!(failed_as_expected); + } + for request in requests { + request.join().unwrap(); + } + if requests_received > 1 { + return; + } + } + panic!("failed to issue concurrent Electrum requests"); + } + struct RecordingConfirm { calls: AtomicUsize, relevant_txid: Txid, diff --git a/src/error.rs b/src/error.rs index d187c68200..202cb38878 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,7 +11,6 @@ use bdk_chain::bitcoin::psbt::ExtractTxError as BdkExtractTxError; use bdk_chain::local_chain::CannotConnectError as BdkChainConnectionError; use bdk_chain::tx_graph::CalculateFeeError as BdkChainCalculateFeeError; use bdk_wallet::error::CreateTxError as BdkCreateTxError; -#[allow(deprecated)] use bdk_wallet::signer::SignerError as BdkSignerError; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -255,7 +254,6 @@ impl fmt::Display for Error { impl std::error::Error for Error {} -#[allow(deprecated)] impl From for Error { fn from(_: BdkSignerError) -> Self { Self::OnchainTxSigningFailed diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index b883ea918a..b7e2e095f6 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -15,9 +15,11 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; use bdk_chain::ChainPosition; use bdk_wallet::descriptor::ExtendedDescriptor; use bdk_wallet::error::{BuildFeeBumpError, CreateTxError}; -#[allow(deprecated)] -use bdk_wallet::SignOptions; -use bdk_wallet::{Balance, KeychainKind, LocalOutput, PersistedWallet, Update, WalletEvent}; +use bdk_wallet::signer::SignersContainer; +use bdk_wallet::{ + Balance, FinalizeInputOutcome, KeychainKind, LocalOutput, PersistedWallet, SignOptions, Update, + WalletEvent, +}; use bitcoin::address::NetworkUnchecked; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -160,6 +162,8 @@ impl AddressPool { pub(crate) struct Wallet { // A BDK on-chain wallet. inner: Mutex>, + // External and internal signing keys are kept outside the BDK wallet. + signers: [SignersContainer; 2], persister: tokio::sync::Mutex, address_pool: Mutex, // Serializes refill runs so concurrent pops never over-reveal. @@ -186,10 +190,11 @@ pub(crate) struct Wallet { impl Wallet { pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, - wallet_persister: KVStoreWalletPersister, persisted_pool_indices: Vec, - broadcaster: Arc, fee_estimator: Arc, - chain_source: Arc, payment_store: Arc, runtime: Arc, - config: Arc, logger: Arc, pending_payment_store: Arc, + signers: [SignersContainer; 2], wallet_persister: KVStoreWalletPersister, + persisted_pool_indices: Vec, broadcaster: Arc, + fee_estimator: Arc, chain_source: Arc, + payment_store: Arc, runtime: Arc, config: Arc, + logger: Arc, pending_payment_store: Arc, channel_tx_facts_store: Arc, ) -> Self { let address_pool = Mutex::new(AddressPool::new(persisted_pool_indices, &wallet, &logger)); @@ -198,6 +203,7 @@ impl Wallet { let address_pool_refill_lock = tokio::sync::Mutex::new(()); Self { inner, + signers, persister, address_pool, address_pool_refill_lock, @@ -1700,7 +1706,6 @@ impl Wallet { Ok(()) } - #[allow(deprecated)] pub(crate) async fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, fee_rate: Option, @@ -1725,7 +1730,11 @@ impl Wallet { }, }; - match locked_wallet.sign(&mut psbt, SignOptions::default()) { + match locked_wallet.sign_with_signers( + &mut psbt, + &self.signers.each_ref(), + SignOptions::default(), + ) { Ok(finalized) => { if !finalized { return Err(Error::OnchainTxCreationFailed); @@ -2166,7 +2175,6 @@ impl Wallet { .map_err(|_| Error::InvalidAddress) } - #[allow(deprecated)] pub(crate) async fn send_to_address( &self, address: &bitcoin::Address, send_amount: OnchainSendAmount, fee_rate: Option, @@ -2288,7 +2296,11 @@ impl Wallet { _ => {}, } - match locked_wallet.sign(&mut psbt, SignOptions::default()) { + match locked_wallet.sign_with_signers( + &mut psbt, + &self.signers.each_ref(), + SignOptions::default(), + ) { Ok(finalized) => { if !finalized { return Err(Error::OnchainTxCreationFailed); @@ -2527,7 +2539,6 @@ impl Wallet { Ok(utxos) } - #[allow(deprecated)] async fn get_change_script_inner(&self) -> Result { let mut locked_persister = self.persister.lock().await; let (address_info, change_set) = { @@ -2541,7 +2552,6 @@ impl Wallet { Ok(address_info.address.script_pubkey()) } - #[allow(deprecated)] pub(crate) fn sign_owned_inputs(&self, unsigned_tx: Transaction) -> Result { let locked_wallet = self.inner.lock().expect("lock"); @@ -2564,13 +2574,14 @@ impl Wallet { let mut sign_options = SignOptions::default(); sign_options.trust_witness_utxo = true; - match locked_wallet.sign(&mut psbt, sign_options) { + match locked_wallet.sign_with_signers(&mut psbt, &self.signers.each_ref(), sign_options) { Ok(finalized) => debug_assert!(!finalized), Err(e) => { log_error!(self.logger, "Failed to sign owned inputs: {}", e); return Err(()); }, } + self.finalize_wallet_inputs(&locked_wallet, &mut psbt)?; match psbt.extract_tx() { Ok(tx) => Ok(tx), @@ -2582,7 +2593,6 @@ impl Wallet { } } - #[allow(deprecated)] fn sign_psbt_inner(&self, mut psbt: Psbt) -> Result { let locked_wallet = self.inner.lock().expect("lock"); @@ -2592,17 +2602,12 @@ impl Wallet { let mut sign_options = SignOptions::default(); sign_options.trust_witness_utxo = true; - match locked_wallet.sign(&mut psbt, sign_options) { - Ok(_finalized) => { - // BDK will fail to finalize for all LDK-provided inputs of the PSBT. Unfortunately - // we can't check more fine grained if it succeeded for all the other inputs here, - // so we just ignore the returned `finalized` bool. - }, - Err(err) => { + locked_wallet + .sign_with_signers(&mut psbt, &self.signers.each_ref(), sign_options) + .map_err(|err| { log_error!(self.logger, "Failed to sign transaction: {}", err); - return Err(()); - }, - } + })?; + self.finalize_wallet_inputs(&locked_wallet, &mut psbt)?; let tx = psbt.extract_tx().map_err(|e| { log_error!(self.logger, "Failed to extract transaction: {}", e); @@ -2612,6 +2617,23 @@ impl Wallet { Ok(tx) } + fn finalize_wallet_inputs( + &self, wallet: &bdk_wallet::Wallet, psbt: &mut Psbt, + ) -> Result<(), ()> { + let outcome = wallet.try_finalize_psbt(psbt).map_err(|e| { + log_error!(self.logger, "Failed to finalize transaction: {}", e); + })?; + for (index, input_outcome) in outcome.into_outcomes() { + // MissingDescriptor is expected for LDK's inputs, which BDK cannot finalize. + // A descriptor that BDK recognizes must be satisfied before we return the transaction. + if let FinalizeInputOutcome::CouldNotSatisfy(e) = input_outcome { + log_error!(self.logger, "Failed to finalize wallet input {}: {}", index, e); + return Err(()); + } + } + Ok(()) + } + /// Builds this node's share of the `active` round of an interactive funding whose negotiated /// history is `candidates`, and the per-candidate figures of that history, for recording the /// round under `payment_id`. Returns `None` when there is nothing to record: no local @@ -3367,7 +3389,6 @@ impl Wallet { Ok(FundingStatusUpdate::Applied) } - #[allow(deprecated)] pub(crate) async fn bump_fee_rbf( &self, payment_id: PaymentId, fee_rate: Option, cur_anchor_reserve_sats: u64, ) -> Result { @@ -3618,7 +3639,11 @@ impl Wallet { return Err(Error::InsufficientFunds); } - match locked_wallet.sign(&mut psbt, SignOptions::default()) { + match locked_wallet.sign_with_signers( + &mut psbt, + &self.signers.each_ref(), + SignOptions::default(), + ) { Ok(finalized) => { if !finalized { log_error!(self.logger, "Failed to finalize signing for fee bump of {}", txid); @@ -4176,6 +4201,7 @@ mod tests { use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use bdk_chain::{BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate}; + use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::Wallet as BdkWallet; use bitcoin::hashes::Hash; use bitcoin::Network; @@ -4384,19 +4410,26 @@ mod tests { let mut wallet_persister = KVStoreWalletPersister::new(Arc::clone(&store), Arc::clone(&logger)); - #[allow(deprecated)] + let secp_ctx = Secp256k1::new(); + let (descriptor, keymap) = + EXTERNAL_DESCRIPTOR.into_wallet_descriptor(&secp_ctx, config.network.into()).unwrap(); + let (change_descriptor, change_keymap) = + INTERNAL_DESCRIPTOR.into_wallet_descriptor(&secp_ctx, config.network.into()).unwrap(); + let signers = [ + SignersContainer::build(keymap, &descriptor, &secp_ctx), + SignersContainer::build(change_keymap, &change_descriptor, &secp_ctx), + ]; let bdk_wallet = if load_existing { BdkWallet::load() - .descriptor(KeychainKind::External, Some(EXTERNAL_DESCRIPTOR)) - .descriptor(KeychainKind::Internal, Some(INTERNAL_DESCRIPTOR)) - .extract_keys() + .descriptor(KeychainKind::External, Some(descriptor)) + .descriptor(KeychainKind::Internal, Some(change_descriptor)) .check_network(Network::Regtest) .load_wallet_async(&mut wallet_persister) .await .unwrap() .unwrap() } else { - BdkWallet::create(EXTERNAL_DESCRIPTOR, INTERNAL_DESCRIPTOR) + BdkWallet::create(descriptor, change_descriptor) .network(Network::Regtest) .create_wallet_async(&mut wallet_persister) .await @@ -4460,6 +4493,7 @@ mod tests { Arc::new(Wallet::new( bdk_wallet, + signers, wallet_persister, persisted_pool_indices, broadcaster, @@ -4493,6 +4527,132 @@ mod tests { ) } + #[tokio::test] + async fn sign_both_keychains_before_and_after_reload() { + let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); + let mut wallet = new_test_wallet(Arc::clone(&store), false).await; + let secp_ctx = Secp256k1::new(); + + for reload in [false, true] { + if reload { + wallet = new_test_wallet(Arc::clone(&store), true).await; + } + + let prevouts = { + let locked_wallet = wallet.inner.lock().unwrap(); + [KeychainKind::External, KeychainKind::Internal].map(|keychain| TxOut { + value: Amount::from_sat(100_000), + script_pubkey: locked_wallet.peek_address(keychain, 0).address.script_pubkey(), + }) + }; + let unsigned_tx = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: LockTime::ZERO, + input: (0..2) + .map(|vout| bitcoin::TxIn { + previous_output: OutPoint { txid: Txid::from_byte_array([42; 32]), vout }, + ..Default::default() + }) + .collect(), + output: vec![TxOut { + value: Amount::from_sat(199_000), + script_pubkey: prevouts[0].script_pubkey.clone(), + }], + }; + let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).unwrap(); + for (input, prevout) in psbt.inputs.iter_mut().zip(&prevouts) { + // LDK supplies witness UTXOs without the full previous transactions. + input.witness_utxo = Some(prevout.clone()); + } + + let tx = wallet.sign_psbt_inner(psbt).unwrap(); + let mut sighash_cache = bitcoin::sighash::SighashCache::new(&tx); + for (index, (input, prevout)) in tx.input.iter().zip(&prevouts).enumerate() { + assert_eq!(input.witness.len(), 2, "both keychains must be finalized"); + let signature = bitcoin::ecdsa::Signature::from_slice(&input.witness[0]).unwrap(); + let public_key = bitcoin::PublicKey::from_slice(&input.witness[1]).unwrap(); + assert_eq!( + ScriptBuf::new_p2wpkh(&public_key.wpubkey_hash().unwrap()), + prevout.script_pubkey + ); + assert_eq!(signature.sighash_type, bitcoin::sighash::EcdsaSighashType::All); + assert!(input.witness[0].len() <= 71, "ECDSA signatures must use low R"); + let sighash = sighash_cache + .p2wpkh_signature_hash( + index, + &prevout.script_pubkey, + prevout.value, + signature.sighash_type, + ) + .unwrap(); + secp_ctx + .verify_ecdsa( + &bitcoin::secp256k1::Message::from_digest(sighash.to_byte_array()), + &signature.signature, + &public_key.inner, + ) + .unwrap(); + } + } + } + + #[tokio::test] + async fn sign_owned_inputs_requires_wallet_finalization() { + let (mut wallet, psbt) = mixed_input_psbt().await; + let tx = wallet.sign_owned_inputs(psbt.unsigned_tx.clone()).unwrap(); + assert_eq!(tx.input[0].witness.len(), 2); + assert!(tx.input[1].witness.is_empty(), "foreign inputs may remain unsigned"); + + Arc::get_mut(&mut wallet).unwrap().signers = Default::default(); + assert!( + wallet.sign_owned_inputs(psbt.unsigned_tx).is_err(), + "an unfinished wallet input must fail signing" + ); + } + + #[tokio::test] + async fn sign_psbt_requires_wallet_finalization() { + let (mut wallet, psbt) = mixed_input_psbt().await; + let tx = wallet.sign_psbt_inner(psbt.clone()).unwrap(); + assert_eq!(tx.input[0].witness.len(), 2); + assert!(tx.input[1].witness.is_empty(), "foreign inputs may remain unsigned"); + + Arc::get_mut(&mut wallet).unwrap().signers = Default::default(); + assert!( + wallet.sign_psbt_inner(psbt).is_err(), + "an unfinished wallet input must fail signing" + ); + } + + /// A PSBT spending one wallet output and one foreign output awaiting its owner's signature. + async fn mixed_input_psbt() -> (Arc, Psbt) { + let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); + let wallet = new_test_wallet(store, false).await; + let funding_tx = wallet_paying_tx(&wallet, 42); + insert_confirmed_tx(&wallet, funding_tx.clone(), 1); + let foreign_output = + TxOut { value: Amount::from_sat(90_000), script_pubkey: ScriptBuf::new().to_p2wsh() }; + let unsigned_tx = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: LockTime::ZERO, + input: [ + OutPoint { txid: funding_tx.compute_txid(), vout: 0 }, + OutPoint { txid: Txid::from_byte_array([43; 32]), vout: 0 }, + ] + .into_iter() + .map(|previous_output| bitcoin::TxIn { previous_output, ..Default::default() }) + .collect(), + output: vec![TxOut { + value: Amount::from_sat(179_000), + script_pubkey: foreign_output.script_pubkey.clone(), + }], + }; + let mut psbt = Psbt::from_unsigned_tx(unsigned_tx).unwrap(); + psbt.inputs[0].witness_utxo = Some(funding_tx.output[0].clone()); + psbt.inputs[1].witness_utxo = Some(foreign_output); + (wallet, psbt) + } + #[tokio::test] async fn refill_publishes_addresses_only_after_their_reveal_is_persisted() { let fail_store = FailSwitchStore::new(); @@ -7983,6 +8143,44 @@ mod tests { .is_none()); } + #[tokio::test] + async fn max_funding_waits_for_truc_output_confirmation() { + let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new())); + let wallet = new_test_wallet(store, false).await; + let change_script = wallet + .inner + .lock() + .unwrap() + .reveal_next_address(KeychainKind::Internal) + .address + .script_pubkey(); + let fee_rate = FeeRate::from_sat_per_kwu(250); + + // Both outputs are trusted change, but our v2 funding transactions can only spend + // the v2 output while the v3 (TRUC) parent remains unconfirmed. + let mut regular_tx = wallet_paying_tx(&wallet, 42); + regular_tx.output[0].script_pubkey = change_script.clone(); + insert_unconfirmed_tx(&wallet, regular_tx); + let max_without_truc = wallet.get_max_funding_amount(0, fee_rate).unwrap(); + assert!(max_without_truc > 0); + + let mut truc_tx = wallet_paying_tx(&wallet, 43); + truc_tx.version = bitcoin::transaction::Version(3); + truc_tx.output[0].script_pubkey = change_script; + insert_unconfirmed_tx(&wallet, truc_tx.clone()); + assert_eq!( + wallet.get_max_funding_amount(0, fee_rate).unwrap(), + max_without_truc, + "unconfirmed TRUC outputs must not increase the v2 funding amount" + ); + + insert_confirmed_tx(&wallet, truc_tx, 1); + assert!( + wallet.get_max_funding_amount(0, fee_rate).unwrap() > max_without_truc, + "confirmed TRUC outputs must become available for funding" + ); + } + #[tokio::test] async fn max_funding_estimate_keeps_reserved_change_address_used() { let store: Arc = Arc::new(DynStoreWrapper(InMemoryStore::new()));