diff --git a/itest/assets_test.go b/itest/assets_test.go index dd66c747f..b46c5f367 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -864,8 +864,8 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, require.NoError(t, err) result, err := getPaymentResult(stream) - if cfg.expectTimeout { - require.ErrorContains(t, err, "context deadline exceeded") + if cfg.errSubStr != "" { + require.ErrorContains(t, err, cfg.errSubStr) } else { require.NoError(t, err) require.Equal(t, cfg.payStatus, result.Status) @@ -912,7 +912,9 @@ func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode, type payConfig struct { smallShards bool - expectTimeout bool + errSubStr string + allowOverpay bool + feeLimit lnwire.MilliSatoshi payStatus lnrpc.Payment_PaymentStatus failureReason lnrpc.PaymentFailureReason rfq fn.Option[rfqmsg.ID] @@ -921,7 +923,8 @@ type payConfig struct { func defaultPayConfig() *payConfig { return &payConfig{ smallShards: false, - expectTimeout: false, + errSubStr: "", + feeLimit: 1_000_000, payStatus: lnrpc.Payment_SUCCEEDED, failureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, } @@ -935,9 +938,9 @@ func withSmallShards() payOpt { } } -func withExpectTimeout() payOpt { +func withPayErrSubStr(errSubStr string) payOpt { return func(c *payConfig) { - c.expectTimeout = true + c.errSubStr = errSubStr } } @@ -956,6 +959,18 @@ func withRFQ(rfqID rfqmsg.ID) payOpt { } } +func withFeeLimit(limit lnwire.MilliSatoshi) payOpt { + return func(c *payConfig) { + c.feeLimit = limit + } +} + +func withAllowOverpay() payOpt { + return func(c *payConfig) { + c.allowOverpay = true + } +} + func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, payReq string, assetID []byte, opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) { @@ -979,7 +994,7 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, sendReq := &routerrpc.SendPaymentRequest{ PaymentRequest: payReq, TimeoutSeconds: int32(PaymentTimeout.Seconds()), - FeeLimitMsat: 1_000_000, + FeeLimitMsat: int64(cfg.feeLimit), } if cfg.smallShards { @@ -997,9 +1012,20 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, PeerPubkey: rfqPeer.PubKey[:], PaymentRequest: sendReq, RfqId: rfqBytes, + AllowOverpay: cfg.allowOverpay, }) require.NoError(t, err) + // If an error is returned by the RPC method (meaning the stream itself + // was established, no network or auth error), we expect the error to be + // returned on the first read on the stream. + if cfg.errSubStr != "" { + _, err := stream.Recv() + require.ErrorContains(t, err, cfg.errSubStr) + + return 0, rfqmath.BigIntFixedPoint{} + } + var ( numUnits uint64 rateVal rfqmath.FixedPoint[rfqmath.BigInt] @@ -1043,8 +1069,32 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, return numUnits, rateVal } +type invoiceConfig struct { + errSubStr string +} + +func defaultInvoiceConfig() *invoiceConfig { + return &invoiceConfig{ + errSubStr: "", + } +} + +type invoiceOpt func(*invoiceConfig) + +func withInvoiceErrSubStr(errSubStr string) invoiceOpt { + return func(c *invoiceConfig) { + c.errSubStr = errSubStr + } +} + func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, - assetAmount uint64, assetID []byte) *lnrpc.AddInvoiceResponse { + assetAmount uint64, assetID []byte, + opts ...invoiceOpt) *lnrpc.AddInvoiceResponse { + + cfg := defaultInvoiceConfig() + for _, opt := range opts { + opt(cfg) + } ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -1068,7 +1118,13 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, Expiry: timeoutSeconds, }, }) - require.NoError(t, err) + if cfg.errSubStr != "" { + require.ErrorContains(t, err, cfg.errSubStr) + + return nil + } else { + require.NoError(t, err) + } decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{ PayReq: resp.InvoiceResult.PaymentRequest, diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index bfb810017..398f66811 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -17,7 +17,6 @@ import ( "github.com/lightninglabs/taproot-assets/proof" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" - "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" @@ -1983,20 +1982,61 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context, // Yara with satoshi. This is a multi-hop payment going over 2 asset // channels, where the total asset value is less than the default anchor // amount of 354 sats. - invoiceResp = createAssetInvoice(t.t, dave, charlie, 1, assetID) - payInvoiceWithSatoshi(t.t, yara, invoiceResp, withFailure( - lnrpc.Payment_FAILED, failureNoRoute, + createAssetInvoice(t.t, dave, charlie, 1, assetID, withInvoiceErrSubStr( + "cannot create invoice over 1 asset units, as the minimal "+ + "transportable amount", )) logBalance(t.t, nodes, assetID, "after small payment (asset "+ "invoice, <354sats)") + // Edge case: We now create a small BTC invoice on Erin and ask Charlie + // to pay it with assets. We should get a payment failure as the amount + // is too small to be paid with assets economically. But a payment is + // still possible, since the amount is large enough to represent a + // single unit (17.1 sat per unit). + btcInvoiceResp, err := erin.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "small BTC invoice", + ValueMsat: 18_000, + }) + require.NoError(t.t, err) + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(2_000), withPayErrSubStr( + "rejecting payment of 20000 mSAT", + ), + ) + + // When we override the uneconomical payment, it should succeed. + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(2_000), withAllowOverpay(), + ) + logBalance( + t.t, nodes, assetID, "after small payment (BTC invoice 1 sat)", + ) + + // When we try to pay an invoice amount that's smaller than the + // corresponding value of a single asset unit, the payment will always + // be rejected, even if we set the allow_uneconomical flag. + btcInvoiceResp, err = erin.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "very small BTC invoice", + ValueMsat: 1_000, + }) + require.NoError(t.t, err) + payInvoiceWithAssets( + t.t, charlie, dave, btcInvoiceResp.PaymentRequest, assetID, + withFeeLimit(1_000), withAllowOverpay(), withPayErrSubStr( + "rejecting payment of 2000 mSAT", + ), + ) + // Edge case: Now Dave creates an asset invoice to be paid for by // Yara with satoshi. For the last hop we try to settle the invoice in // satoshi, where we will check whether Dave's strict forwarding works // as expected. Charlie is only used as a dummy RFQ peer in this case, // Yara totally ignored the RFQ hint and pays agnostically with sats. - invoiceResp = createAssetInvoice(t.t, charlie, dave, 1, assetID) + invoiceResp = createAssetInvoice(t.t, charlie, dave, 22, assetID) stream, err := dave.InvoicesClient.SubscribeSingleInvoice( ctxb, &invoicesrpc.SubscribeSingleInvoiceRequest{ @@ -2149,7 +2189,7 @@ func testCustomChannelsLiquidityEdgeCases(ctxb 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, withExpectTimeout(), + t.t, erin, iResp, withPayErrSubStr("context deadline exceeded"), withFailure(lnrpc.Payment_FAILED, failureNone), ) @@ -2702,7 +2742,7 @@ func testCustomChannelsOraclePricing(_ context.Context, commitFeeP2WSH int64 = 2810 anchorAmount int64 = 330 assetHtlcCarryAmount = int64( - tapchannel.DefaultOnChainHtlcAmount, + rfqmath.DefaultOnChainHtlcSat, ) unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR - anchorAmount