diff --git a/go.mod b/go.mod index 19987e8a7..16c7c59d2 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/auctioneerrpc v1.1.3-0.20250305125211-4e860ec4e77f github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f - github.com/lightninglabs/taproot-assets v0.5.2-0.20250424162728-b6000498210d + github.com/lightninglabs/taproot-assets v0.5.2-0.20250425170006-2a288fb33aa5 github.com/lightningnetwork/lnd v0.19.0-beta.rc3 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 diff --git a/go.sum b/go.sum index 3443d3720..2c1ea1f5f 100644 --- a/go.sum +++ b/go.sum @@ -1171,8 +1171,10 @@ github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f h1:5p github.com/lightninglabs/pool/poolrpc v1.0.1-0.20250305125211-4e860ec4e77f/go.mod h1:lGs2hSVZ+GFpdv3btaIl9icG5/gz7BBRfvmD2iqqNl0= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.5.2-0.20250424162728-b6000498210d h1:8WcOFToO9iY62c6ghbAaJfVP8qudzdrK89OXiOri9K4= -github.com/lightninglabs/taproot-assets v0.5.2-0.20250424162728-b6000498210d/go.mod h1:6kQm7VC4yWAwczJaxfOWlCOQ4TuzfCLUkGKBwVONN7k= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250425153005-204964fc27bf h1:YzLfqaqcfkgIfp7spgVvCyHG312EWKqQBI2lqcPGr/w= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250425153005-204964fc27bf/go.mod h1:6kQm7VC4yWAwczJaxfOWlCOQ4TuzfCLUkGKBwVONN7k= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250425170006-2a288fb33aa5 h1:RuO65IW6CwjBsuDy/+Vo4viDqUoSFzZrZUiurjZIl9M= +github.com/lightninglabs/taproot-assets v0.5.2-0.20250425170006-2a288fb33aa5/go.mod h1:6kQm7VC4yWAwczJaxfOWlCOQ4TuzfCLUkGKBwVONN7k= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= github.com/lightningnetwork/lnd v0.19.0-beta.rc3 h1:XWMlyZvBrp69AnQqrshLOOPZjOl4hFWR1RD4ceCXt9k= diff --git a/itest/assets_test.go b/itest/assets_test.go index 8ab67e598..299fcfc8d 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -67,6 +67,196 @@ var ( failureNone = lnrpc.PaymentFailureReason_FAILURE_REASON_NONE ) +func createTestMultiRFQAssetNetwork(t *harnessTest, net *NetworkHarness, + charlieTap, daveTap, erinTap, fabiaTap, yaraTap, universeTap *tapClient, + mintedAsset *taprpc.Asset, assetSendAmount, fundingAmount uint64, + pushSat int64) (*lnrpc.ChannelPoint, *lnrpc.ChannelPoint, + *lnrpc.ChannelPoint) { + + ctxb := context.Background() + assetID := mintedAsset.AssetGenesis.AssetId + var groupKey []byte + if mintedAsset.AssetGroup != nil { + groupKey = mintedAsset.AssetGroup.TweakedGroupKey + } + + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + // We need to send some assets to Dave, so he can fund an asset channel + // with Fabia. + daveAddr, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Dave...", assetSendAmount) + + // Send the assets to Dave. + itest.AssertAddrCreated(t.t, daveTap, mintedAsset, daveAddr) + sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{mintedAsset.Amount - assetSendAmount, assetSendAmount}, + 0, 1, + ) + itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 1) + + // We need to send some assets to Erin, so he can fund an asset channel + // with Fabia. + erinAddr, err := erinTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Erin...", assetSendAmount) + + // Send the assets to Erin. + itest.AssertAddrCreated(t.t, erinTap, mintedAsset, erinAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{erinAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + mintedAsset.Amount - 2*assetSendAmount, assetSendAmount, + }, 1, 2, + ) + itest.AssertNonInteractiveRecvComplete(t.t, erinTap, 1) + + // We need to send some assets to Yara, so he can fund an asset channel + // with Fabia. + yaraAddr, err := yaraTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Yara...", assetSendAmount) + + // Send the assets to Yara. + itest.AssertAddrCreated(t.t, yaraTap, mintedAsset, yaraAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{yaraAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + mintedAsset.Amount - 3*assetSendAmount, assetSendAmount, + }, 2, 3, + ) + itest.AssertNonInteractiveRecvComplete(t.t, yaraTap, 1) + + // We fund the Dave->Fabia channel. + fundRespDF, err := daveTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Dave and Fabia: %v", fundRespDF) + + // We fund the Erin->Fabia channel. + fundRespEF, err := erinTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF) + + // We fund the Yara->Fabia channel. + fundRespYF, err := yaraTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Yara and Fabia: %v", fundRespYF) + + // Make sure the pending channel shows up in the list and has the + // custom records set as JSON. + assertPendingChannels( + t.t, daveTap.node, mintedAsset, 1, fundingAmount, 0, + ) + assertPendingChannels( + t.t, erinTap.node, mintedAsset, 1, fundingAmount, 0, + ) + assertPendingChannels( + t.t, yaraTap.node, mintedAsset, 1, fundingAmount, 0, + ) + + // Now that we've looked at the pending channels, let's actually confirm + // all three of them. + mineBlocks(t, net, 6, 3) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := mintedAsset.Amount - 3*assetSendAmount + daveAssetBalance := assetSendAmount - fundingAmount + erinAssetBalance := assetSendAmount - fundingAmount + yaraAssetBalance := assetSendAmount - fundingAmount + + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, yaraTap, assetID, yaraAssetBalance) + + assertNumAssetOutputs(t.t, charlieTap, assetID, 1) + assertNumAssetOutputs(t.t, daveTap, assetID, 1) + assertNumAssetOutputs(t.t, erinTap, assetID, 1) + assertNumAssetOutputs(t.t, yaraTap, assetID, 1) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespDF.Txid, fundRespDF.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespYF.Txid, fundRespYF.OutputIndex), + ) + + return nil, nil, nil +} + // createTestAssetNetwork sends asset funds from Charlie to Dave and Erin, so // they can fund asset channels with Yara and Fabia, respectively. So the asset // channels created are Charlie->Dave, Dave->Yara, Erin->Fabia. The channels @@ -1119,7 +1309,7 @@ func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, stream, err := src.RouterClient.SendPaymentV2(ctxt, req) require.NoError(t, err) - result, err := getPaymentResult(stream) + result, err := getPaymentResult(stream, false) require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) } @@ -1170,19 +1360,27 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, } ctxb := context.Background() - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout*5) defer cancel() + shardSize := uint64(0) + + if cfg.smallShards { + shardSize = 80_000_000 + } + sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: invoice.PaymentRequest, TimeoutSeconds: int32(PaymentTimeout.Seconds()), - MaxShardSizeMsat: 80_000_000, - FeeLimitMsat: 1_000_000, + MaxShardSizeMsat: shardSize, + FeeLimitMsat: 10_000_000, } stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) require.NoError(t, err) - result, err := getPaymentResult(stream) + result, err := getPaymentResult( + stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT, + ) if cfg.errSubStr != "" { require.ErrorContains(t, err, cfg.errSubStr) } else { @@ -1462,16 +1660,21 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) - t.Logf("Asking peer %x for quote to buy assets to receive for "+ - "invoice over %d units; waiting up to %ds", - dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + var peerPubKey []byte + if dstRfqPeer != nil { + peerPubKey = dstRfqPeer.PubKey[:] + + t.Logf("Asking peer %x for quote to buy assets to receive for "+ + "invoice over %d units; waiting up to %ds", + dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + } dstTapd := newTapClient(t, dst) request := &tchrpc.AddInvoiceRequest{ GroupKey: cfg.groupKey, AssetAmount: assetAmount, - PeerPubkey: dstRfqPeer.PubKey[:], + PeerPubkey: peerPubKey, InvoiceRequest: &lnrpc.Invoice{ Memo: fmt.Sprintf("this is an asset invoice for "+ "%d units", assetAmount), @@ -1515,7 +1718,7 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+ "SCID %d", decodedInvoice.NumMsat, mSatPerUnit, - dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid) + resp.AcceptedBuyQuote.Peer, resp.AcceptedBuyQuote.Scid) return resp.InvoiceResult } @@ -1649,9 +1852,15 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + var rfqPeer []byte + + if dstRfqPeer != nil { + rfqPeer = dstRfqPeer.PubKey[:] + } + t.Logf("Asking peer %x for quote to buy assets to receive for "+ "invoice for %d units; waiting up to %ds", - dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + rfqPeer, assetAmount, timeoutSeconds) dstTapd := newTapClient(t, dst) @@ -1664,7 +1873,7 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, payHash := preimage.Hash() request := &tchrpc.AddInvoiceRequest{ AssetAmount: assetAmount, - PeerPubkey: dstRfqPeer.PubKey[:], + PeerPubkey: rfqPeer, InvoiceRequest: &lnrpc.Invoice{ Memo: fmt.Sprintf("this is an asset invoice for "+ "%d units", assetAmount), @@ -1702,7 +1911,7 @@ func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, require.EqualValues(t, uint64(numMSats), uint64(decodedInvoice.NumMsat)) t.Logf("Got quote for %d msat at %v msat/unit from peer %x with SCID "+ - "%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:], + "%d", decodedInvoice.NumMsat, mSatPerUnit, rfqPeer, resp.AcceptedBuyQuote.Scid) return assetHodlInvoice{ diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index 891f4fb3a..918f2e9a1 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -416,13 +416,13 @@ func payNode(invoiceCtx, paymentCtx context.Context, t *harnessTest, stream, err := from.SendPaymentV2(paymentCtx, sendReq) require.NoError(t.t, err) - result, err := getPaymentResult(stream) + result, err := getPaymentResult(stream, false) require.NoError(t.t, err) require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED) } -func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( - *lnrpc.Payment, error) { +func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client, + isHodl bool) (*lnrpc.Payment, error) { for { payment, err := stream.Recv() @@ -430,7 +430,14 @@ func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( return nil, err } - if payment.Status != lnrpc.Payment_IN_FLIGHT { + // If this is a hodl payment, then we'll return the first + // expected response. Otherwise, we'll wait until the in flight + // clears to we can observe the other payment states. + switch { + case isHodl: + return payment, nil + + case payment.Status != lnrpc.Payment_IN_FLIGHT: return payment, nil } } diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index c50d91467..8434551a3 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -2516,7 +2516,7 @@ func testCustomChannelsLiquidtyEdgeCasesCore(ctx context.Context, // amount of 354 sats. createAssetInvoice( t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( - "1 asset units, as the minimal transportable amount", + "could not create any quotes for the invoice", ), withInvGroupKey(groupID), ) @@ -2686,7 +2686,7 @@ func testCustomChannelsLiquidtyEdgeCasesCore(ctx context.Context, res, err := charlieTap.RfqClient.AddAssetBuyOrder( ctx, &rfqrpc.AddAssetBuyOrderRequest{ AssetSpecifier: &assetSpecifier, - AssetMaxAmt: 10_000, + AssetMaxAmt: 1_000, Expiry: uint64(inOneHour.Unix()), PeerPubKey: dave.PubKey[:], TimeoutSeconds: 10, @@ -2702,7 +2702,7 @@ func testCustomChannelsLiquidtyEdgeCasesCore(ctx context.Context, // manually generated, quote. iResp, err := charlie.AddInvoice(ctx, &lnrpc.Invoice{ Memo: "", - Value: 200_000, + Value: 20_000, RPreimage: bytes.Repeat([]byte{11}, 32), CltvExpiry: 60, RouteHints: []*lnrpc.RouteHint{{ @@ -2717,8 +2717,8 @@ func testCustomChannelsLiquidtyEdgeCasesCore(ctx context.Context, // Now Erin tries to pay the invoice. Since rfq quote cannot satisfy the // total amount of the invoice this payment will fail. payInvoiceWithSatoshi( - t.t, erin, iResp, withPayErrSubStr("context deadline exceeded"), - withFailure(lnrpc.Payment_FAILED, failureNone), + t.t, erin, iResp, + withFailure(lnrpc.Payment_FAILED, failureNoRoute), withGroupKey(groupID), ) @@ -2745,6 +2745,160 @@ func testCustomChannelsLiquidityEdgeCasesGroup(ctx context.Context, testCustomChannelsLiquidtyEdgeCasesCore(ctx, net, t, true) } +// testCustomChannelsMultiRFQReceive tests that a node creating an invoice with +// multiple RFQ quotes can actually guide the payer into using multiple private +// taproot asset channels to pay the invoice. +func testCustomChannelsMultiRFQReceive(ctx context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // The topology we are going for looks like the following: + // + // /---[sats]--> Erin --[assets]--\ + // / \ + // / \ + // Charlie -----[sats]--> Dave --[assets]---->Fabia + // \ / + // \ / + // \---[sats]--> Yara --[assets]--/ + // + + // Let's open the normal sats channels between Charlie and the routing + // peers. + _ = openChannelAndAssert( + t, net, charlie, erin, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + _ = openChannelAndAssert( + t, net, charlie, dave, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + _ = openChannelAndAssert( + t, net, charlie, yara, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + + // Let's create the tap clients. + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + assetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ + Asset: itestAsset, + }) + + assetReq.Asset.NewGroupedAsset = true + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{assetReq}, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + groupID := cents.GetAssetGroup().GetTweakedGroupKey() + + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + + createTestMultiRFQAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, 10_000, 10_000, 10_000, + ) + + logBalance(t.t, nodes, assetID, "before multi-rfq receive") + + hodlInv := createAssetHodlInvoice(t.t, nil, fabia, 20_000, assetID) + + payInvoiceWithSatoshi( + t.t, charlie, &lnrpc.AddInvoiceResponse{ + PaymentRequest: hodlInv.payReq, + }, + withGroupKey(groupID), + withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), + ) + + logBalance(t.t, nodes, assetID, "after inflight multi-rfq") + + // TODO: assert minNumHtlcs after rebase + + // Now let's cancel the invoice and assert that all inbound channels + // have cleared their HTLCs. + payHash := hodlInv.preimage.Hash() + _, err = fabia.InvoicesClient.CancelInvoice( + ctx, &invoicesrpc.CancelInvoiceMsg{ + PaymentHash: payHash[:], + }, + ) + require.NoError(t.t, err) + + assertNumHtlcs(t.t, dave, 0) + assertNumHtlcs(t.t, erin, 0) + assertNumHtlcs(t.t, yara, 0) + + logBalance(t.t, nodes, assetID, "after cancelled hodl") + + // Now let's create a normal invoice that will be settled once all the + // HTLCs have been received. This is only possible because the payer + // uses multiple bolt11 hop hints to reach the destination. + invoiceResp := createAssetInvoice(t.t, nil, fabia, 15_000, assetID) + + payInvoiceWithSatoshi( + t.t, charlie, invoiceResp, withGroupKey(groupID), + ) + + logBalance(t.t, nodes, assetID, "after multi-rfq receive") +} + // testCustomChannelsStrictForwarding is a test that tests the strict forwarding // behavior of a node when it comes to paying asset invoices with assets and // BTC invoices with satoshis. diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 41ccc8937..aa01c0951 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -114,4 +114,9 @@ var allTestCases = []*testCase{ test: testCustomChannelsDecodeAssetInvoice, noAliceBob: true, }, + { + name: "test custom channels multi rfq", + test: testCustomChannelsMultiRFQReceive, + noAliceBob: true, + }, }