11use std:: { iter:: Sum , ops:: Add } ;
22
33use bdk_coin_select:: { Target , TargetFee , TargetOutputs } ;
4+ use bitcoin:: Amount ;
45use log:: debug;
56
67use crate :: {
@@ -44,12 +45,12 @@ fn piecewise_linear(x: f64, points: &[(f64, f64)]) -> f64 {
4445/// An Action a wallet can perform
4546#[ derive( Debug ) ]
4647pub ( crate ) enum Action {
47- /// Spend a payment obligation unilaterally with pre-selected inputs
48- UnilateralPayments ( Vec < PaymentObligationId > , Vec < Outpoint > ) ,
48+ /// Spend a payment obligation unilaterally with pre-selected inputs and pre-computed change
49+ UnilateralPayments ( Vec < PaymentObligationId > , Vec < Outpoint > , Vec < Amount > ) ,
4950 /// Accept a cospend invitation
5051 AcceptCospendProposal ( ( MessageId , BulletinBoardId ) ) ,
51- /// Contribute outputs to a cospend session that is waiting for them
52- ContributeOutputsToSession ( BulletinBoardId , Vec < PaymentObligationId > ) ,
52+ /// Contribute outputs to a cospend session that is waiting for them, with pre-computed change
53+ ContributeOutputsToSession ( BulletinBoardId , Vec < PaymentObligationId > , Vec < Amount > ) ,
5354 /// Continue to participate in a multi-party payjoin
5455 ContinueParticipateInCospend ( BulletinBoardId ) ,
5556 /// Taker records non-committal interest in cospending with each orderbook UTXO
@@ -163,8 +164,8 @@ fn simulate_one_action(wallet_handle: &WalletHandleMut, action: &Action) -> Pred
163164
164165 // POs handled: derived from action since confirmation is deferred to block
165166 let payment_obligations_handled: Vec < PaymentObligationId > = match action {
166- Action :: UnilateralPayments ( po_ids, _) => po_ids. clone ( ) ,
167- Action :: ContributeOutputsToSession ( _, po_ids) => po_ids. clone ( ) ,
167+ Action :: UnilateralPayments ( po_ids, _, _ ) => po_ids. clone ( ) ,
168+ Action :: ContributeOutputsToSession ( _, po_ids, _ ) => po_ids. clone ( ) ,
168169 _ => vec ! [ ] ,
169170 } ;
170171
@@ -296,6 +297,36 @@ fn target_for_obligations(pos: &[PaymentObligationData], wallet: &WalletHandleMu
296297 }
297298}
298299
300+ /// Compute pre-selected change outputs for a `ContributeOutputsToSession` action.
301+ /// If the session has pre-selected inputs (from the aggregator), uses those exactly.
302+ /// Otherwise falls back to full BNB / spend-all selection over all wallet UTXOs.
303+ fn change_for_session_contribution (
304+ bb_id : & BulletinBoardId ,
305+ pos : & [ PaymentObligationData ] ,
306+ wallet : & WalletHandleMut ,
307+ ) -> Vec < Amount > {
308+ let session = wallet
309+ . info ( )
310+ . active_multi_party_payjoins
311+ . get ( bb_id)
312+ . unwrap ( ) ;
313+ let session_input_outpoints: Vec < Outpoint > =
314+ session. inputs . iter ( ) . map ( |i| i. outpoint ) . collect ( ) ;
315+ let target = target_for_obligations ( pos, wallet) ;
316+ if session_input_outpoints. is_empty ( ) {
317+ let candidates = wallet. handle ( ) . coin_candidates ( ) ;
318+ if let Some ( ( _, change) ) = select_bnb ( & candidates, target) {
319+ return change;
320+ }
321+ select_all ( & candidates, target) . 1
322+ } else {
323+ let candidates = wallet
324+ . handle ( )
325+ . coin_candidates_for ( & session_input_outpoints) ;
326+ select_all ( & candidates, target) . 1
327+ }
328+ }
329+
299330#[ derive( Debug , Clone ) ]
300331pub ( crate ) struct UnilateralSpender ;
301332
@@ -315,12 +346,12 @@ impl Strategy for UnilateralSpender {
315346 let mut actions = vec ! [ ] ;
316347 for po in state. payment_obligations . iter ( ) {
317348 let target = target_for_obligations ( std:: slice:: from_ref ( po) , wallet) ;
318- if let Some ( inputs) = select_bnb ( & candidates, target) {
319- actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , inputs) ) ;
349+ if let Some ( ( inputs, change ) ) = select_bnb ( & candidates, target) {
350+ actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , inputs, change ) ) ;
320351 }
321- let all_inputs = select_all ( & candidates) ;
352+ let ( all_inputs, change ) = select_all ( & candidates, target ) ;
322353 if !all_inputs. is_empty ( ) {
323- actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , all_inputs) ) ;
354+ actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , all_inputs, change ) ) ;
324355 }
325356 }
326357 if actions. is_empty ( ) {
@@ -346,11 +377,12 @@ impl Strategy for Consolidator {
346377 wallet : & WalletHandleMut ,
347378 ) -> Vec < Action > {
348379 let candidates = wallet. handle ( ) . coin_candidates ( ) ;
349- let all_inputs = select_all ( & candidates) ;
350380 let mut actions = Vec :: new ( ) ;
351381 for po in state. payment_obligations . iter ( ) {
382+ let target = target_for_obligations ( std:: slice:: from_ref ( po) , wallet) ;
383+ let ( all_inputs, change) = select_all ( & candidates, target) ;
352384 if !all_inputs. is_empty ( ) {
353- actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , all_inputs. clone ( ) ) ) ;
385+ actions. push ( Action :: UnilateralPayments ( vec ! [ po. id] , all_inputs, change ) ) ;
354386 }
355387 }
356388 actions. push ( Action :: Wait ) ;
@@ -380,12 +412,12 @@ impl Strategy for BatchSpender {
380412 let target = target_for_obligations ( & state. payment_obligations , wallet) ;
381413 let candidates = wallet. handle ( ) . coin_candidates ( ) ;
382414 let mut actions = vec ! [ ] ;
383- if let Some ( inputs) = select_bnb ( & candidates, target) {
384- actions. push ( Action :: UnilateralPayments ( po_ids. clone ( ) , inputs) ) ;
415+ if let Some ( ( inputs, change ) ) = select_bnb ( & candidates, target) {
416+ actions. push ( Action :: UnilateralPayments ( po_ids. clone ( ) , inputs, change ) ) ;
385417 }
386- let all_inputs = select_all ( & candidates) ;
418+ let ( all_inputs, change ) = select_all ( & candidates, target ) ;
387419 if !all_inputs. is_empty ( ) {
388- actions. push ( Action :: UnilateralPayments ( po_ids, all_inputs) ) ;
420+ actions. push ( Action :: UnilateralPayments ( po_ids, all_inputs, change ) ) ;
389421 }
390422 if actions. is_empty ( ) {
391423 actions. push ( Action :: Wait ) ;
@@ -424,11 +456,17 @@ impl Strategy for MakerStrategy {
424456 }
425457 }
426458
427- // Contribute outputs to sessions that are waiting for them (SentInputs state)
459+ // Contribute outputs to sessions that are waiting for them (AcceptedProposal state)
428460 for ( bb_id, session) in wallet. info ( ) . active_multi_party_payjoins . iter ( ) {
429461 if session. state == TxConstructionState :: AcceptedProposal {
430462 for po in state. payment_obligations . iter ( ) {
431- actions. push ( Action :: ContributeOutputsToSession ( * bb_id, vec ! [ po. id] ) ) ;
463+ let change =
464+ change_for_session_contribution ( bb_id, std:: slice:: from_ref ( po) , wallet) ;
465+ actions. push ( Action :: ContributeOutputsToSession (
466+ * bb_id,
467+ vec ! [ po. id] ,
468+ change,
469+ ) ) ;
432470 }
433471 }
434472 }
@@ -446,11 +484,11 @@ impl Strategy for MakerStrategy {
446484 } else {
447485 vec ! [ ]
448486 } ;
449- // Selected inputs are already embedded in each action — no simulation needed.
487+ // Selected inputs are already embedded in each action i.e no simulation needed.
450488 let per_action_inputs: Vec < std:: collections:: HashSet < Outpoint > > = unilateral_actions
451489 . iter ( )
452490 . filter_map ( |a| match a {
453- Action :: UnilateralPayments ( _, inputs) => Some ( inputs. iter ( ) . copied ( ) . collect ( ) ) ,
491+ Action :: UnilateralPayments ( _, inputs, _ ) => Some ( inputs. iter ( ) . copied ( ) . collect ( ) ) ,
454492 _ => None ,
455493 } )
456494 . collect ( ) ;
@@ -490,11 +528,17 @@ impl Strategy for TakerStrategy {
490528 ) -> Vec < Action > {
491529 let mut actions = vec ! [ ] ;
492530
493- // Contribute outputs to sessions awaiting them (SentInputs state)
531+ // Contribute outputs to sessions awaiting them (AcceptedProposal state)
494532 for ( bb_id, session) in wallet. info ( ) . active_multi_party_payjoins . iter ( ) {
495533 if session. state == TxConstructionState :: AcceptedProposal {
496534 for po in state. payment_obligations . iter ( ) {
497- actions. push ( Action :: ContributeOutputsToSession ( * bb_id, vec ! [ po. id] ) ) ;
535+ let change =
536+ change_for_session_contribution ( bb_id, std:: slice:: from_ref ( po) , wallet) ;
537+ actions. push ( Action :: ContributeOutputsToSession (
538+ * bb_id,
539+ vec ! [ po. id] ,
540+ change,
541+ ) ) ;
498542 }
499543 }
500544 }
@@ -724,7 +768,7 @@ mod tests {
724768 assert ! ( actions. iter( ) . any( |a| matches!( a, Action :: Wait ) ) ) ;
725769 assert ! ( !actions
726770 . iter( )
727- . any( |a| matches!( a, Action :: UnilateralPayments ( _, _) ) ) ) ;
771+ . any( |a| matches!( a, Action :: UnilateralPayments ( _, _, _ ) ) ) ) ;
728772 }
729773
730774 #[ test]
@@ -752,13 +796,16 @@ mod tests {
752796
753797 let actions = strategy. enumerate_candidate_actions ( & view, & wallet) ;
754798
755- // No UTXOs — coin selection produces nothing, falls back to Wait.
799+ // No UTXOs coin selection produces nothing, falls back to Wait.
756800 assert_eq ! ( actions. len( ) , 1 ) ;
757801 assert ! ( matches!( actions[ 0 ] , Action :: Wait ) ) ;
758802 }
759803
760804 #[ test]
761805 fn test_composite_strategy_combines_actions ( ) {
806+ // TODO: this test is kinda useless, we need to add UTXOs to the sim and test the composite strategy.
807+ // Otherwise we are just testing that both strategies fall back to Wait when there are no UTXOs.
808+ // This is bc coin selection uses `wallet.handle().coin_candidates();` not `state.utxos`.
762809 let mut sim = test_sim ( ) ;
763810 let wallet = WalletId ( 0 ) . with_mut ( & mut sim) ;
764811 let composite = CompositeStrategy {
@@ -785,24 +832,9 @@ mod tests {
785832
786833 let actions = composite. enumerate_candidate_actions ( & view, & wallet) ;
787834
788- // Should include actions from both strategies
789- // UnilateralSpender: 2 actions (one per obligation, single-PO each)
790- // BatchSpender: 1 action (all obligations in one tx)
791- assert_eq ! ( actions. len( ) , 3 ) ;
792-
793- let single_po_count = actions
794- . iter ( )
795- . filter ( |a| matches ! ( a, Action :: UnilateralPayments ( ids, _) if ids. len( ) == 1 ) )
796- . count ( ) ;
797- assert_eq ! ( single_po_count, 2 ) ;
798-
799- let batch_count = actions
800- . iter ( )
801- . filter ( |a| matches ! ( a, Action :: UnilateralPayments ( ids, _) if ids. len( ) == 2 ) )
802- . count ( ) ;
803- assert_eq ! ( batch_count, 1 ) ;
804- // No UTXOs both strategies fall back to Wait, composite collects both.
805- assert_eq ! ( actions. len( ) , 3 ) ;
835+ // Wallet has no UTXOs in the sim, both strategies fall back to Wait.
836+ // Composite collects one Wait from each strategy.
837+ assert_eq ! ( actions. len( ) , 2 ) ;
806838 assert ! ( actions. iter( ) . all( |a| matches!( a, Action :: Wait ) ) ) ;
807839 }
808840
@@ -820,7 +852,7 @@ mod tests {
820852 from : WalletId ( 0 ) ,
821853 to : WalletId ( 1 ) ,
822854 } ;
823- // No orderbook UTXOs — taker has nothing to propose to
855+ // No orderbook UTXOs, taker has nothing to propose to
824856 let view = create_test_wallet_view ( vec ! [ po] ) ;
825857
826858 let actions = strategy. enumerate_candidate_actions ( & view, & wallet) ;
@@ -982,7 +1014,7 @@ mod tests {
9821014
9831015 assert ! ( actions
9841016 . iter( )
985- . any( |a| matches!( a, Action :: ContributeOutputsToSession ( id, ids) if * id == bb_id && ids. len( ) == 1 ) ) ) ;
1017+ . any( |a| matches!( a, Action :: ContributeOutputsToSession ( id, ids, _ ) if * id == bb_id && ids. len( ) == 1 ) ) ) ;
9861018 // Should NOT emit ContinueParticipateInCospend for this session
9871019 assert ! ( !actions
9881020 . iter( )
0 commit comments