@@ -206,10 +206,12 @@ impl PriorContribution {
206206/// For a fresh splice (no pending splice to replace), build a new contribution using one of
207207/// the splice methods:
208208/// - [`FundingTemplate::splice_in_sync`] to add funds to the channel
209- /// - [`FundingTemplate::splice_out_sync `] to remove funds from the channel
209+ /// - [`FundingTemplate::splice_out `] to remove funds from the channel
210210/// - [`FundingTemplate::splice_in_and_out_sync`] to do both
211211///
212- /// These perform coin selection and require `min_feerate` and `max_feerate` parameters.
212+ /// These require `min_feerate` and `max_feerate` parameters. The splice-in variants perform
213+ /// coin selection when wallet inputs are needed, while splice-out spends only from the channel
214+ /// balance.
213215///
214216/// # Replace By Fee (RBF)
215217///
@@ -285,31 +287,13 @@ macro_rules! build_funding_contribution {
285287 let max_feerate: FeeRate = $max_feerate;
286288 let force_coin_selection: bool = $force_coin_selection;
287289
288- if feerate > max_feerate {
289- return Err ( FundingContributionError :: FeeRateExceedsMaximum { feerate, max_feerate } ) ;
290- }
291-
292- if let Some ( min_rbf_feerate) = min_rbf_feerate {
293- if feerate < min_rbf_feerate {
294- return Err ( FundingContributionError :: FeeRateBelowRbfMinimum { feerate, min_rbf_feerate } ) ;
295- }
296- }
297-
298- // Validate user-provided amounts are within MAX_MONEY before coin selection to
299- // ensure FundingContribution::net_value() arithmetic cannot overflow. With all
300- // amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value()
301- // computation is -2 * MAX_MONEY (~-4.2e15), well within i64::MIN (~-9.2e18).
302- if value_added > Amount :: MAX_MONEY {
303- return Err ( FundingContributionError :: InvalidSpliceValue ) ;
304- }
305-
306- let mut value_removed = Amount :: ZERO ;
307- for txout in outputs. iter( ) {
308- value_removed = match value_removed. checked_add( txout. value) {
309- Some ( sum) if sum <= Amount :: MAX_MONEY => sum,
310- _ => return Err ( FundingContributionError :: InvalidSpliceValue ) ,
311- } ;
312- }
290+ let value_removed = validate_funding_contribution_params(
291+ value_added,
292+ & outputs,
293+ min_rbf_feerate,
294+ feerate,
295+ max_feerate,
296+ ) ?;
313297
314298 let is_splice = shared_input. is_some( ) ;
315299
@@ -348,25 +332,52 @@ macro_rules! build_funding_contribution {
348332
349333 let CoinSelection { confirmed_utxos: inputs, change_output } = coin_selection;
350334
351- // The caller creating a FundingContribution is always the initiator for fee estimation
352- // purposes — this is conservative, overestimating rather than underestimating fees if
353- // the node ends up as the acceptor.
354- let estimated_fee = estimate_transaction_fee( & inputs, & outputs, change_output. as_ref( ) , true , is_splice, feerate) ;
355- debug_assert!( estimated_fee <= Amount :: MAX_MONEY ) ;
356-
357- let contribution = FundingContribution {
335+ Ok ( FundingContribution :: new(
358336 value_added,
359- estimated_fee,
360- inputs,
361337 outputs,
338+ inputs,
362339 change_output,
363340 feerate,
364341 max_feerate,
365342 is_splice,
343+ ) )
344+ } } ;
345+ }
346+
347+ fn validate_funding_contribution_params (
348+ value_added : Amount , outputs : & [ TxOut ] , min_rbf_feerate : Option < FeeRate > , feerate : FeeRate ,
349+ max_feerate : FeeRate ,
350+ ) -> Result < Amount , FundingContributionError > {
351+ if feerate > max_feerate {
352+ return Err ( FundingContributionError :: FeeRateExceedsMaximum { feerate, max_feerate } ) ;
353+ }
354+
355+ if let Some ( min_rbf_feerate) = min_rbf_feerate {
356+ if feerate < min_rbf_feerate {
357+ return Err ( FundingContributionError :: FeeRateBelowRbfMinimum {
358+ feerate,
359+ min_rbf_feerate,
360+ } ) ;
361+ }
362+ }
363+
364+ // Validate user-provided amounts are within MAX_MONEY before coin selection to
365+ // ensure FundingContribution::net_value() arithmetic cannot overflow. With all
366+ // amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value()
367+ // computation is -2 * MAX_MONEY (~-4.2e15), well within i64::MIN (~-9.2e18).
368+ if value_added > Amount :: MAX_MONEY {
369+ return Err ( FundingContributionError :: InvalidSpliceValue ) ;
370+ }
371+
372+ let mut value_removed = Amount :: ZERO ;
373+ for txout in outputs. iter ( ) {
374+ value_removed = match value_removed. checked_add ( txout. value ) {
375+ Some ( sum) if sum <= Amount :: MAX_MONEY => sum,
376+ _ => return Err ( FundingContributionError :: InvalidSpliceValue ) ,
366377 } ;
378+ }
367379
368- Ok ( contribution)
369- } } ;
380+ Ok ( value_removed)
370381}
371382
372383impl FundingTemplate {
@@ -410,44 +421,37 @@ impl FundingTemplate {
410421 )
411422 }
412423
413- /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
414- /// perform coin selection.
424+ /// Creates a [`FundingContribution`] for removing funds from a channel.
425+ ///
426+ /// Fees are paid from the channel balance, so this does not perform coin selection or spend
427+ /// wallet inputs.
415428 ///
416429 /// `outputs` are the complete set of withdrawal outputs for this contribution. When
417430 /// replacing a prior contribution via RBF, use [`FundingTemplate::prior_contribution`] to
418431 /// inspect the prior parameters. To keep existing withdrawals and add new ones, include the
419432 /// prior's outputs: combine [`FundingContribution::outputs`] with the new outputs.
420- pub async fn splice_out < W : CoinSelectionSource + MaybeSend > (
421- self , outputs : Vec < TxOut > , min_feerate : FeeRate , max_feerate : FeeRate , wallet : W ,
422- ) -> Result < FundingContribution , FundingContributionError > {
423- if outputs. is_empty ( ) {
424- return Err ( FundingContributionError :: InvalidSpliceValue ) ;
425- }
426- let FundingTemplate { shared_input, min_rbf_feerate, .. } = self ;
427- build_funding_contribution ! ( Amount :: ZERO , outputs, shared_input, min_rbf_feerate, min_feerate, max_feerate, false , wallet, await )
428- }
429-
430- /// Creates a [`FundingContribution`] for removing funds from a channel using `wallet` to
431- /// perform coin selection.
432- ///
433- /// See [`FundingTemplate::splice_out`] for details.
434- pub fn splice_out_sync < W : CoinSelectionSourceSync > (
435- self , outputs : Vec < TxOut > , min_feerate : FeeRate , max_feerate : FeeRate , wallet : W ,
433+ pub fn splice_out (
434+ self , outputs : Vec < TxOut > , min_feerate : FeeRate , max_feerate : FeeRate ,
436435 ) -> Result < FundingContribution , FundingContributionError > {
437436 if outputs. is_empty ( ) {
438437 return Err ( FundingContributionError :: InvalidSpliceValue ) ;
439438 }
440- let FundingTemplate { shared_input, min_rbf_feerate, .. } = self ;
441- build_funding_contribution ! (
439+ validate_funding_contribution_params (
440+ Amount :: ZERO ,
441+ & outputs,
442+ self . min_rbf_feerate ,
443+ min_feerate,
444+ max_feerate,
445+ ) ?;
446+ Ok ( FundingContribution :: new (
442447 Amount :: ZERO ,
443448 outputs,
444- shared_input ,
445- min_rbf_feerate ,
449+ vec ! [ ] ,
450+ None ,
446451 min_feerate,
447452 max_feerate,
448- false ,
449- wallet,
450- )
453+ self . shared_input . is_some ( ) ,
454+ ) )
451455 }
452456
453457 /// Creates a [`FundingContribution`] for both adding and removing funds from a channel using
@@ -708,6 +712,35 @@ impl_writeable_tlv_based!(FundingContribution, {
708712} ) ;
709713
710714impl FundingContribution {
715+ fn new (
716+ value_added : Amount , outputs : Vec < TxOut > , inputs : Vec < FundingTxInput > ,
717+ change_output : Option < TxOut > , feerate : FeeRate , max_feerate : FeeRate , is_splice : bool ,
718+ ) -> Self {
719+ // The caller creating a FundingContribution is always the initiator for fee estimation
720+ // purposes — this is conservative, overestimating rather than underestimating fees if the
721+ // node ends up as the acceptor.
722+ let estimated_fee = estimate_transaction_fee (
723+ & inputs,
724+ & outputs,
725+ change_output. as_ref ( ) ,
726+ true ,
727+ is_splice,
728+ feerate,
729+ ) ;
730+ debug_assert ! ( estimated_fee <= Amount :: MAX_MONEY ) ;
731+
732+ Self {
733+ value_added,
734+ estimated_fee,
735+ inputs,
736+ outputs,
737+ change_output,
738+ feerate,
739+ max_feerate,
740+ is_splice,
741+ }
742+ }
743+
711744 pub ( super ) fn feerate ( & self ) -> FeeRate {
712745 self . feerate
713746 }
@@ -1440,17 +1473,17 @@ mod tests {
14401473 ) ) ;
14411474 }
14421475
1443- // splice_out_sync with single output value > MAX_MONEY
1476+ // splice_out with single output value > MAX_MONEY
14441477 {
14451478 let template = FundingTemplate :: new ( None , None , None ) ;
14461479 let outputs = vec ! [ funding_output_sats( over_max. to_sat( ) ) ] ;
14471480 assert ! ( matches!(
1448- template. splice_out_sync ( outputs, feerate, feerate, UnreachableWallet ) ,
1481+ template. splice_out ( outputs, feerate, feerate) ,
14491482 Err ( FundingContributionError :: InvalidSpliceValue ) ,
14501483 ) ) ;
14511484 }
14521485
1453- // splice_out_sync with multiple outputs summing > MAX_MONEY
1486+ // splice_out with multiple outputs summing > MAX_MONEY
14541487 {
14551488 let template = FundingTemplate :: new ( None , None , None ) ;
14561489 let half_over = Amount :: MAX_MONEY / 2 + Amount :: from_sat ( 1 ) ;
@@ -1459,7 +1492,7 @@ mod tests {
14591492 funding_output_sats( half_over. to_sat( ) ) ,
14601493 ] ;
14611494 assert ! ( matches!(
1462- template. splice_out_sync ( outputs, feerate, feerate, UnreachableWallet ) ,
1495+ template. splice_out ( outputs, feerate, feerate) ,
14631496 Err ( FundingContributionError :: InvalidSpliceValue ) ,
14641497 ) ) ;
14651498 }
@@ -2454,23 +2487,22 @@ mod tests {
24542487 }
24552488
24562489 #[ test]
2457- fn test_splice_out_sync_skips_coin_selection_during_rbf ( ) {
2458- // When splice_out_sync is called on a template with min_rbf_feerate set (user
2459- // choosing a fresh splice-out instead of rbf_sync), coin selection should NOT run.
2460- // Fees come from the channel balance .
2490+ fn test_splice_out_skips_coin_selection_during_rbf ( ) {
2491+ // When splice_out is called on a template with min_rbf_feerate set (user
2492+ // choosing a fresh splice-out instead of rbf_sync), fees still come from the
2493+ // channel balance rather than wallet inputs .
24612494 let min_rbf_feerate = FeeRate :: from_sat_per_kwu ( 5000 ) ;
24622495 let feerate = FeeRate :: from_sat_per_kwu ( 5000 ) ;
24632496 let withdrawal = funding_output_sats ( 20_000 ) ;
24642497
24652498 let template =
24662499 FundingTemplate :: new ( Some ( shared_input ( 100_000 ) ) , Some ( min_rbf_feerate) , None ) ;
24672500
2468- // UnreachableWallet panics if coin selection runs — verifying it is skipped.
2469- let contribution = template
2470- . splice_out_sync ( vec ! [ withdrawal. clone( ) ] , feerate, FeeRate :: MAX , UnreachableWallet )
2471- . unwrap ( ) ;
2501+ let contribution =
2502+ template. splice_out ( vec ! [ withdrawal. clone( ) ] , feerate, FeeRate :: MAX ) . unwrap ( ) ;
24722503 assert_eq ! ( contribution. value_added, Amount :: ZERO ) ;
24732504 assert ! ( contribution. inputs. is_empty( ) ) ;
2505+ assert ! ( contribution. change_output. is_none( ) ) ;
24742506 assert_eq ! ( contribution. outputs, vec![ withdrawal] ) ;
24752507 }
24762508}
0 commit comments