1
1
pub use anyhow;
2
2
use anyhow:: Context ;
3
- use bdk_coin_select:: { coin_select_bnb , CoinSelector , CoinSelectorOpt , WeightedValue } ;
3
+ use bdk_coin_select:: { Candidate , CoinSelector } ;
4
4
use bdk_file_store:: Store ;
5
5
use serde:: { de:: DeserializeOwned , Serialize } ;
6
- use std:: { cmp:: Reverse , collections:: HashMap , path:: PathBuf , sync:: Mutex , time :: Duration } ;
6
+ use std:: { cmp:: Reverse , collections:: HashMap , path:: PathBuf , sync:: Mutex } ;
7
7
8
8
use bdk_chain:: {
9
9
bitcoin:: {
@@ -16,7 +16,7 @@ use bdk_chain::{
16
16
descriptor:: { DescriptorSecretKey , KeyMap } ,
17
17
Descriptor , DescriptorPublicKey ,
18
18
} ,
19
- Anchor , Append , ChainOracle , DescriptorExt , FullTxOut , Persist , PersistBackend ,
19
+ Anchor , Append , ChainOracle , FullTxOut , Persist , PersistBackend ,
20
20
} ;
21
21
pub use bdk_file_store;
22
22
pub use clap;
@@ -412,39 +412,18 @@ where
412
412
} ;
413
413
414
414
// TODO use planning module
415
- let mut candidates = planned_utxos ( graph, chain, & assets) ?;
416
-
417
- // apply coin selection algorithm
418
- match cs_algorithm {
419
- CoinSelectionAlgo :: LargestFirst => {
420
- candidates. sort_by_key ( |( _, utxo) | Reverse ( utxo. txout . value ) )
421
- }
422
- CoinSelectionAlgo :: SmallestFirst => candidates. sort_by_key ( |( _, utxo) | utxo. txout . value ) ,
423
- CoinSelectionAlgo :: OldestFirst => {
424
- candidates. sort_by_key ( |( _, utxo) | utxo. chain_position . clone ( ) )
425
- }
426
- CoinSelectionAlgo :: NewestFirst => {
427
- candidates. sort_by_key ( |( _, utxo) | Reverse ( utxo. chain_position . clone ( ) ) )
428
- }
429
- CoinSelectionAlgo :: BranchAndBound => { }
430
- }
431
-
415
+ let raw_candidates = planned_utxos ( graph, chain, & assets) ?;
432
416
// turn the txos we chose into weight and value
433
- let wv_candidates = candidates
417
+ let candidates = raw_candidates
434
418
. iter ( )
435
419
. map ( |( plan, utxo) | {
436
- WeightedValue :: new (
420
+ Candidate :: new (
437
421
utxo. txout . value ,
438
422
plan. expected_weight ( ) as _ ,
439
423
plan. witness_version ( ) . is_some ( ) ,
440
424
)
441
425
} )
442
- . collect ( ) ;
443
-
444
- let mut outputs = vec ! [ TxOut {
445
- value,
446
- script_pubkey: address. script_pubkey( ) ,
447
- } ] ;
426
+ . collect :: < Vec < _ > > ( ) ;
448
427
449
428
let internal_keychain = if graph. index . keychains ( ) . get ( & Keychain :: Internal ) . is_some ( ) {
450
429
Keychain :: Internal
@@ -457,7 +436,7 @@ where
457
436
additions. append ( change_additions) ;
458
437
459
438
// Clone to drop the immutable reference.
460
- let change_script = change_script. into ( ) ;
439
+ let change_script = change_script. to_owned ( ) ;
461
440
462
441
let change_plan = bdk_tmp_plan:: plan_satisfaction (
463
442
& graph
@@ -471,68 +450,113 @@ where
471
450
)
472
451
. expect ( "failed to obtain change plan" ) ;
473
452
474
- let mut change_output = TxOut {
475
- value : 0 ,
476
- script_pubkey : change_script,
453
+ let mut transaction = Transaction {
454
+ version : 0x02 ,
455
+ // because the temporary planning module does not support timelocks, we can use the chain
456
+ // tip as the `lock_time` for anti-fee-sniping purposes
457
+ lock_time : chain
458
+ . get_chain_tip ( ) ?
459
+ . and_then ( |block_id| absolute:: LockTime :: from_height ( block_id. height ) . ok ( ) )
460
+ . unwrap_or ( absolute:: LockTime :: ZERO ) ,
461
+ input : vec ! [ ] ,
462
+ output : vec ! [ TxOut {
463
+ value,
464
+ script_pubkey: address. script_pubkey( ) ,
465
+ } ] ,
477
466
} ;
478
467
479
- let cs_opts = CoinSelectorOpt {
480
- target_feerate : 0.5 ,
481
- min_drain_value : graph
482
- . index
483
- . keychains ( )
484
- . get ( & internal_keychain)
485
- . expect ( "must exist" )
486
- . dust_value ( ) ,
487
- ..CoinSelectorOpt :: fund_outputs (
488
- & outputs,
489
- & change_output,
490
- change_plan. expected_weight ( ) as u32 ,
491
- )
468
+ let target = bdk_coin_select:: Target {
469
+ feerate : bdk_coin_select:: FeeRate :: from_sat_per_vb ( 1.0 ) ,
470
+ min_fee : 0 ,
471
+ value : transaction. output . iter ( ) . map ( |txo| txo. value ) . sum ( ) ,
492
472
} ;
493
473
494
- // TODO: How can we make it easy to shuffle in order of inputs and outputs here?
495
- // apply coin selection by saying we need to fund these outputs
496
- let mut coin_selector = CoinSelector :: new ( & wv_candidates, & cs_opts) ;
474
+ let drain = bdk_coin_select:: Drain {
475
+ weight : {
476
+ // we calculate the weight difference of including the drain output in the base tx
477
+ // this method will detect varint size changes of txout count
478
+ let tx_weight = transaction. weight ( ) ;
479
+ let tx_weight_with_drain = {
480
+ let mut tx = transaction. clone ( ) ;
481
+ tx. output . push ( TxOut {
482
+ script_pubkey : change_script. clone ( ) ,
483
+ ..Default :: default ( )
484
+ } ) ;
485
+ tx. weight ( )
486
+ } ;
487
+ ( tx_weight_with_drain - tx_weight) . to_wu ( ) as u32 - 1
488
+ } ,
489
+ value : 0 ,
490
+ spend_weight : change_plan. expected_weight ( ) as u32 ,
491
+ } ;
492
+ let long_term_feerate = bdk_coin_select:: FeeRate :: from_sat_per_wu ( 0.25 ) ;
493
+ let drain_policy = bdk_coin_select:: change_policy:: min_waste ( drain, long_term_feerate) ;
497
494
498
- // just select coins in the order provided until we have enough
499
- // only use the first result (least waste)
500
- let selection = match cs_algorithm {
495
+ let mut selector = CoinSelector :: new ( & candidates, transaction. weight ( ) . to_wu ( ) as u32 ) ;
496
+ match cs_algorithm {
501
497
CoinSelectionAlgo :: BranchAndBound => {
502
- coin_select_bnb ( Duration :: from_secs ( 10 ) , coin_selector. clone ( ) )
503
- . map_or_else ( || coin_selector. select_until_finished ( ) , |cs| cs. finish ( ) ) ?
498
+ let metric = bdk_coin_select:: metrics:: Waste {
499
+ target,
500
+ long_term_feerate,
501
+ change_policy : & drain_policy,
502
+ } ;
503
+ let ( final_selection, _score) = selector
504
+ . branch_and_bound ( metric)
505
+ . take ( 50_000 )
506
+ // we only process viable solutions
507
+ . flatten ( )
508
+ . reduce ( |( best_sol, best_score) , ( curr_sol, curr_score) | {
509
+ // we are reducing waste
510
+ if curr_score < best_score {
511
+ ( curr_sol, curr_score)
512
+ } else {
513
+ ( best_sol, best_score)
514
+ }
515
+ } )
516
+ . ok_or ( anyhow:: format_err!( "no bnb solution found" ) ) ?;
517
+ selector = final_selection;
518
+ }
519
+ cs_algorithm => {
520
+ match cs_algorithm {
521
+ CoinSelectionAlgo :: LargestFirst => {
522
+ selector. sort_candidates_by_key ( |( _, c) | Reverse ( c. value ) )
523
+ }
524
+ CoinSelectionAlgo :: SmallestFirst => {
525
+ selector. sort_candidates_by_key ( |( _, c) | c. value )
526
+ }
527
+ CoinSelectionAlgo :: OldestFirst => selector
528
+ . sort_candidates_by_key ( |( i, _) | raw_candidates[ i] . 1 . chain_position . clone ( ) ) ,
529
+ CoinSelectionAlgo :: NewestFirst => selector. sort_candidates_by_key ( |( i, _) | {
530
+ Reverse ( raw_candidates[ i] . 1 . chain_position . clone ( ) )
531
+ } ) ,
532
+ CoinSelectionAlgo :: BranchAndBound => unreachable ! ( "bnb variant is matched already" ) ,
533
+ }
534
+ selector. select_until_target_met ( target, drain) ?
504
535
}
505
- _ => coin_selector. select_until_finished ( ) ?,
506
536
} ;
507
- let ( _, selection_meta) = selection. best_strategy ( ) ;
508
537
509
538
// get the selected utxos
510
- let selected_txos = selection. apply_selection ( & candidates) . collect :: < Vec < _ > > ( ) ;
539
+ let selected_txos = selector
540
+ . apply_selection ( & raw_candidates)
541
+ . collect :: < Vec < _ > > ( ) ;
511
542
512
- if let Some ( drain_value) = selection_meta. drain_value {
513
- change_output. value = drain_value;
514
- // if the selection tells us to use change and the change value is sufficient, we add it as an output
515
- outputs. push ( change_output)
543
+ let drain = drain_policy ( & selector, target) ;
544
+ if drain. is_some ( ) {
545
+ transaction. output . push ( TxOut {
546
+ value : drain. value ,
547
+ script_pubkey : change_script,
548
+ } ) ;
516
549
}
517
550
518
- let mut transaction = Transaction {
519
- version : 0x02 ,
520
- // because the temporary planning module does not support timelocks, we can use the chain
521
- // tip as the `lock_time` for anti-fee-sniping purposes
522
- lock_time : chain
523
- . get_chain_tip ( ) ?
524
- . and_then ( |block_id| absolute:: LockTime :: from_height ( block_id. height ) . ok ( ) )
525
- . unwrap_or ( absolute:: LockTime :: ZERO ) ,
526
- input : selected_txos
527
- . iter ( )
528
- . map ( |( _, utxo) | TxIn {
529
- previous_output : utxo. outpoint ,
530
- sequence : Sequence :: ENABLE_RBF_NO_LOCKTIME ,
531
- ..Default :: default ( )
532
- } )
533
- . collect ( ) ,
534
- output : outputs,
535
- } ;
551
+ // fill transaction inputs
552
+ transaction. input = selected_txos
553
+ . iter ( )
554
+ . map ( |( _, utxo) | TxIn {
555
+ previous_output : utxo. outpoint ,
556
+ sequence : Sequence :: ENABLE_RBF_NO_LOCKTIME ,
557
+ ..Default :: default ( )
558
+ } )
559
+ . collect ( ) ;
536
560
537
561
let prevouts = selected_txos
538
562
. iter ( )
@@ -593,7 +617,7 @@ where
593
617
}
594
618
}
595
619
596
- let change_info = if selection_meta . drain_value . is_some ( ) {
620
+ let change_info = if drain . is_some ( ) {
597
621
Some ( ( additions, ( internal_keychain, change_index) ) )
598
622
} else {
599
623
None
0 commit comments