@@ -619,9 +619,27 @@ where
619
619
_ => 1 ,
620
620
} ;
621
621
622
+ // We use a match here instead of a map_or_else as it's way more readable :)
623
+ let current_height = match params. current_height {
624
+ // If they didn't tell us the current height, we assume it's the latest sync height.
625
+ None => self
626
+ . database ( )
627
+ . get_sync_time ( ) ?
628
+ . map ( |sync_time| sync_time. block_time . height ) ,
629
+ h => h,
630
+ } ;
631
+
622
632
let lock_time = match params. locktime {
623
- // No nLockTime, default to 0
624
- None => requirements. timelock . unwrap_or ( 0 ) ,
633
+ // When no nLockTime is specified, we try to prevent fee sniping, if possible
634
+ None => {
635
+ // Fee sniping can be partially prevented by setting the timelock
636
+ // to current_height. If we don't know the current_height,
637
+ // we default to 0.
638
+ let fee_sniping_height = current_height. unwrap_or ( 0 ) ;
639
+ // We choose the biggest between the required nlocktime and the fee sniping
640
+ // height
641
+ std:: cmp:: max ( requirements. timelock . unwrap_or ( 0 ) , fee_sniping_height)
642
+ }
625
643
// Specific nLockTime required and we have no constraints, so just set to that value
626
644
Some ( x) if requirements. timelock . is_none ( ) => x,
627
645
// Specific nLockTime required and it's compatible with the constraints
@@ -791,7 +809,14 @@ where
791
809
let drain_val = ( coin_selection. selected_amount ( ) - outgoing) . saturating_sub ( fee_amount) ;
792
810
793
811
if tx. output . is_empty ( ) {
794
- if params. drain_to . is_some ( ) {
812
+ // Uh oh, our transaction has no outputs.
813
+ // We allow this when:
814
+ // - We have a drain_to address and the utxos we must spend (this happens,
815
+ // for example, when we RBF)
816
+ // - We have a drain_to address and drain_wallet set
817
+ // Otherwise, we don't know who we should send the funds to, and how much
818
+ // we should send!
819
+ if params. drain_to . is_some ( ) && ( params. drain_wallet || !params. utxos . is_empty ( ) ) {
795
820
if drain_val. is_dust ( & drain_output. script_pubkey ) {
796
821
return Err ( Error :: InsufficientFunds {
797
822
needed : drain_output. script_pubkey . dust_value ( ) . as_sat ( ) ,
@@ -1980,9 +2005,59 @@ pub(crate) mod test {
1980
2005
builder. add_recipient ( addr. script_pubkey ( ) , 25_000 ) ;
1981
2006
let ( psbt, _) = builder. finish ( ) . unwrap ( ) ;
1982
2007
2008
+ // Since we never synced the wallet we don't have a last_sync_height
2009
+ // we could use to try to prevent fee sniping. We default to 0.
1983
2010
assert_eq ! ( psbt. unsigned_tx. lock_time, 0 ) ;
1984
2011
}
1985
2012
2013
+ #[ test]
2014
+ fn test_create_tx_fee_sniping_locktime_provided_height ( ) {
2015
+ let ( wallet, _, _) = get_funded_wallet ( get_test_wpkh ( ) ) ;
2016
+ let addr = wallet. get_address ( New ) . unwrap ( ) ;
2017
+ let mut builder = wallet. build_tx ( ) ;
2018
+ builder. add_recipient ( addr. script_pubkey ( ) , 25_000 ) ;
2019
+ let sync_time = SyncTime {
2020
+ block_time : BlockTime {
2021
+ height : 24 ,
2022
+ timestamp : 0 ,
2023
+ } ,
2024
+ } ;
2025
+ wallet
2026
+ . database
2027
+ . borrow_mut ( )
2028
+ . set_sync_time ( sync_time)
2029
+ . unwrap ( ) ;
2030
+ let current_height = 25 ;
2031
+ builder. set_current_height ( current_height) ;
2032
+ let ( psbt, _) = builder. finish ( ) . unwrap ( ) ;
2033
+
2034
+ // current_height will override the last sync height
2035
+ assert_eq ! ( psbt. unsigned_tx. lock_time, current_height) ;
2036
+ }
2037
+
2038
+ #[ test]
2039
+ fn test_create_tx_fee_sniping_locktime_last_sync ( ) {
2040
+ let ( wallet, _, _) = get_funded_wallet ( get_test_wpkh ( ) ) ;
2041
+ let addr = wallet. get_address ( New ) . unwrap ( ) ;
2042
+ let mut builder = wallet. build_tx ( ) ;
2043
+ builder. add_recipient ( addr. script_pubkey ( ) , 25_000 ) ;
2044
+ let sync_time = SyncTime {
2045
+ block_time : BlockTime {
2046
+ height : 25 ,
2047
+ timestamp : 0 ,
2048
+ } ,
2049
+ } ;
2050
+ wallet
2051
+ . database
2052
+ . borrow_mut ( )
2053
+ . set_sync_time ( sync_time. clone ( ) )
2054
+ . unwrap ( ) ;
2055
+ let ( psbt, _) = builder. finish ( ) . unwrap ( ) ;
2056
+
2057
+ // If there's no current_height we're left with using the last sync height
2058
+ assert_eq ! ( psbt. unsigned_tx. lock_time, sync_time. block_time. height) ;
2059
+ }
2060
+
1986
2061
#[ test]
1987
2062
fn test_create_tx_default_locktime_cltv ( ) {
1988
2063
let ( wallet, _, _) = get_funded_wallet ( get_test_single_sig_cltv ( ) ) ;
@@ -2001,9 +2076,13 @@ pub(crate) mod test {
2001
2076
let mut builder = wallet. build_tx ( ) ;
2002
2077
builder
2003
2078
. add_recipient ( addr. script_pubkey ( ) , 25_000 )
2079
+ . set_current_height ( 630_001 )
2004
2080
. nlocktime ( 630_000 ) ;
2005
2081
let ( psbt, _) = builder. finish ( ) . unwrap ( ) ;
2006
2082
2083
+ // When we explicitly specify a nlocktime
2084
+ // we don't try any fee sniping prevention trick
2085
+ // (we ignore the current_height)
2007
2086
assert_eq ! ( psbt. unsigned_tx. lock_time, 630_000 ) ;
2008
2087
}
2009
2088
@@ -2176,6 +2255,40 @@ pub(crate) mod test {
2176
2255
assert_eq ! ( drain_output. value, 30_000 - details. fee. unwrap_or( 0 ) ) ;
2177
2256
}
2178
2257
2258
+ #[ test]
2259
+ fn test_create_tx_drain_to_and_utxos ( ) {
2260
+ let ( wallet, _, _) = get_funded_wallet ( get_test_wpkh ( ) ) ;
2261
+ let addr = wallet. get_address ( New ) . unwrap ( ) ;
2262
+ let utxos: Vec < _ > = wallet
2263
+ . get_available_utxos ( )
2264
+ . unwrap ( )
2265
+ . into_iter ( )
2266
+ . map ( |( u, _) | u. outpoint )
2267
+ . collect ( ) ;
2268
+ let mut builder = wallet. build_tx ( ) ;
2269
+ builder
2270
+ . drain_to ( addr. script_pubkey ( ) )
2271
+ . add_utxos ( & utxos)
2272
+ . unwrap ( ) ;
2273
+ let ( psbt, details) = builder. finish ( ) . unwrap ( ) ;
2274
+
2275
+ assert_eq ! ( psbt. unsigned_tx. output. len( ) , 1 ) ;
2276
+ assert_eq ! (
2277
+ psbt. unsigned_tx. output[ 0 ] . value,
2278
+ 50_000 - details. fee. unwrap_or( 0 )
2279
+ ) ;
2280
+ }
2281
+
2282
+ #[ test]
2283
+ #[ should_panic( expected = "NoRecipients" ) ]
2284
+ fn test_create_tx_drain_to_no_drain_wallet_no_utxos ( ) {
2285
+ let ( wallet, _, _) = get_funded_wallet ( get_test_wpkh ( ) ) ;
2286
+ let drain_addr = wallet. get_address ( New ) . unwrap ( ) ;
2287
+ let mut builder = wallet. build_tx ( ) ;
2288
+ builder. drain_to ( drain_addr. script_pubkey ( ) ) ;
2289
+ builder. finish ( ) . unwrap ( ) ;
2290
+ }
2291
+
2179
2292
#[ test]
2180
2293
fn test_create_tx_default_fee_rate ( ) {
2181
2294
let ( wallet, _, _) = get_funded_wallet ( get_test_wpkh ( ) ) ;
0 commit comments