@@ -211,11 +211,11 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
211211 }
212212 }
213213
214- private def createFixtureParams (fundingAmountA : Satoshi , fundingAmountB : Satoshi , targetFeerate : FeeratePerKw , dustLimit : Satoshi , lockTime : Long , requireConfirmedInputs : RequireConfirmedInputs = RequireConfirmedInputs (forLocal = false , forRemote = false )): FixtureParams = {
214+ private def createFixtureParams (fundingAmountA : Satoshi , fundingAmountB : Satoshi , targetFeerate : FeeratePerKw , dustLimit : Satoshi , lockTime : Long , requireConfirmedInputs : RequireConfirmedInputs = RequireConfirmedInputs (forLocal = false , forRemote = false ), nonInitiatorPaysCommitTxFees : Boolean = false ): FixtureParams = {
215215 val channelFeatures = ChannelFeatures (ChannelTypes .AnchorOutputsZeroFeeHtlcTx (), Features [InitFeature ](Features .DualFunding -> FeatureSupport .Optional ), Features [InitFeature ](Features .DualFunding -> FeatureSupport .Optional ), announceChannel = true )
216216 val Seq (nodeParamsA, nodeParamsB) = Seq (TestConstants .Alice .nodeParams, TestConstants .Bob .nodeParams).map(_.copy(features = Features (channelFeatures.features.map(f => f -> FeatureSupport .Optional ).toMap[Feature , FeatureSupport ])))
217- val localParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None , None , isChannelOpener = true , dualFunded = true , fundingAmountA, unlimitedMaxHtlcValueInFlight = false )
218- val localParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None , None , isChannelOpener = false , dualFunded = true , fundingAmountB, unlimitedMaxHtlcValueInFlight = false )
217+ val localParamsA = makeChannelParams(nodeParamsA, nodeParamsA.features.initFeatures(), None , None , isChannelOpener = true , dualFunded = true , fundingAmountA, unlimitedMaxHtlcValueInFlight = false ).copy(paysCommitTxFees = ! nonInitiatorPaysCommitTxFees)
218+ val localParamsB = makeChannelParams(nodeParamsB, nodeParamsB.features.initFeatures(), None , None , isChannelOpener = false , dualFunded = true , fundingAmountB, unlimitedMaxHtlcValueInFlight = false ).copy(paysCommitTxFees = nonInitiatorPaysCommitTxFees)
219219
220220 val Seq (remoteParamsA, remoteParamsB) = Seq ((nodeParamsA, localParamsA), (nodeParamsB, localParamsB)).map {
221221 case (nodeParams, localParams) =>
@@ -287,7 +287,7 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
287287 utxosB.foreach(amount => addUtxo(walletB, amount, probe))
288288 generateBlocks(1 )
289289
290- val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs)
290+ val fixtureParams = createFixtureParams(fundingAmountA, fundingAmountB, targetFeerate, dustLimit, lockTime, requireConfirmedInputs, nonInitiatorPaysCommitTxFees = liquidityPurchase_opt.nonEmpty )
291291 val alice = fixtureParams.spawnTxBuilderAlice(walletA, liquidityPurchase_opt = liquidityPurchase_opt)
292292 val bob = fixtureParams.spawnTxBuilderBob(walletB, liquidityPurchase_opt = liquidityPurchase_opt)
293293 testFun(Fixture (alice, bob, fixtureParams, walletA, rpcClientA, walletB, rpcClientB, TestProbe (), TestProbe ()))
@@ -564,6 +564,59 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
564564 }
565565 }
566566
567+ test(" initiator does not contribute -- on-the-fly funding" ) {
568+ val targetFeerate = FeeratePerKw (5000 sat)
569+ val fundingB = 150_000 .sat
570+ val utxosB = Seq (200_000 sat)
571+ // When on-the-fly funding is used, the initiator may not contribute to the funding transaction.
572+ // It will receive HTLCs later that use the purchased inbound liquidity, and liquidity fees will be deduced from those HTLCs.
573+ val purchase = LiquidityAds .Purchase .Standard (fundingB, LiquidityAds .Fees (2500 sat, 7500 sat), LiquidityAds .PaymentDetails .FromFutureHtlc (Nil ))
574+ withFixture(0 sat, Nil , fundingB, utxosB, targetFeerate, 330 sat, 0 , RequireConfirmedInputs (forLocal = false , forRemote = false ), Some (purchase)) { f =>
575+ import f ._
576+
577+ alice ! Start (alice2bob.ref)
578+ bob ! Start (bob2alice.ref)
579+
580+ // Alice --- tx_add_output --> Bob
581+ fwd.forwardAlice2Bob[TxAddOutput ]
582+ // Alice <-- tx_add_input --- Bob
583+ fwd.forwardBob2Alice[TxAddInput ]
584+ // Alice --- tx_complete --> Bob
585+ fwd.forwardAlice2Bob[TxComplete ]
586+ // Alice <-- tx_add_output --- Bob
587+ fwd.forwardBob2Alice[TxAddOutput ]
588+ // Alice --- tx_complete --> Bob
589+ fwd.forwardAlice2Bob[TxComplete ]
590+ // Alice <-- tx_complete --- Bob
591+ fwd.forwardBob2Alice[TxComplete ]
592+
593+ // Alice is responsible for adding the shared output, but Bob is paying for everything.
594+ assert(aliceParams.fundingAmount == fundingB)
595+
596+ // Alice sends signatures first as she did not contribute at all.
597+ val successA = alice2bob.expectMsgType[Succeeded ]
598+ val successB = bob2alice.expectMsgType[Succeeded ]
599+ val (txA, _, txB, commitmentB) = fixtureParams.exchangeSigsAliceFirst(aliceParams, successA, successB)
600+ // Alice doesn't pay any fees to Bob during the interactive-tx, fees will be paid from future HTLCs.
601+ assert(commitmentB.localCommit.spec.toLocal == fundingB.toMilliSatoshi)
602+
603+ // The resulting transaction is valid but has a lower feerate than expected.
604+ assert(txA.txId == txB.txId)
605+ assert(txA.tx.localAmountIn == 0 .msat)
606+ assert(txA.tx.localFees == 0 .msat)
607+ assert(txB.tx.remoteAmountIn == 0 .msat)
608+ assert(txB.tx.remoteFees == 0 .msat)
609+ assert(txB.tx.localFees > 0 .msat)
610+ val probe = TestProbe ()
611+ walletA.publishTransaction(txA.signedTx).pipeTo(probe.ref)
612+ probe.expectMsg(txA.txId)
613+ walletA.getMempoolTx(txA.txId).pipeTo(probe.ref)
614+ val mempoolTx = probe.expectMsgType[MempoolTx ]
615+ assert(mempoolTx.fees == txA.tx.fees)
616+ assert(targetFeerate * 0.5 <= txA.feerate && txA.feerate < targetFeerate, s " unexpected feerate (target= $targetFeerate actual= ${txA.feerate}) " )
617+ }
618+ }
619+
567620 test(" initiator and non-initiator splice-in" ) {
568621 val targetFeerate = FeeratePerKw (1000 sat)
569622 // We chose those amounts to ensure that Bob always signs first:
@@ -2193,6 +2246,10 @@ class InteractiveTxBuilderSpec extends TestKitBaseClass with AnyFunSuiteLike wit
21932246 val bobSplice = params.spawnTxBuilderSpliceBob(spliceParams, previousCommitment, wallet, Some (purchase))
21942247 bobSplice ! Start (probe.ref)
21952248 assert(probe.expectMsgType[LocalFailure ].cause == InvalidFundingBalances (params.channelId, 620_000 sat, 625_000_000 msat, - 5_000_000 msat))
2249+ // If we use a payment type where fees are paid outside of the interactive-tx session, the funding attempt is valid.
2250+ val bobFutureHtlc = params.spawnTxBuilderBob(wallet, params.fundingParamsB, Some (purchase.copy(paymentDetails = LiquidityAds .PaymentDetails .FromFutureHtlc (Nil ))))
2251+ bobFutureHtlc ! Start (probe.ref)
2252+ probe.expectNoMessage(100 millis)
21962253 }
21972254
21982255 test(" invalid input" ) {
0 commit comments