diff --git a/go.mod b/go.mod index 8a3e8a227..13546e949 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df github.com/lightninglabs/pool/auctioneerrpc v1.1.2 github.com/lightninglabs/pool/poolrpc v1.0.0 - github.com/lightninglabs/taproot-assets v0.5.0-rc1 + github.com/lightninglabs/taproot-assets v0.5.0-rc1.0.20241206085244-8113fa8bb314 github.com/lightningnetwork/lnd v0.18.4-beta.rc1 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/fn v1.2.3 @@ -46,7 +46,7 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect @@ -89,7 +89,7 @@ require ( github.com/fortytw2/leaktest v1.3.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang-migrate/migrate/v4 v4.17.0 // indirect @@ -191,12 +191,12 @@ require ( go.etcd.io/etcd/raft/v3 v3.5.12 // indirect go.etcd.io/etcd/server/v3 v3.5.12 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/mock v0.4.0 // indirect diff --git a/go.sum b/go.sum index 42d66da84..4b5685183 100644 --- a/go.sum +++ b/go.sum @@ -596,8 +596,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= @@ -855,8 +855,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -1177,8 +1177,8 @@ github.com/lightninglabs/pool/poolrpc v1.0.0 h1:vvosrgNx9WXF4mcHGqLjZOW8wNM0q+BL github.com/lightninglabs/pool/poolrpc v1.0.0/go.mod h1:ZqpEpBFRMMBAerMmilEjh27tqauSXDwLaLR0O3jvmMA= 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.0-rc1 h1:UuDuvOJErqvapdF5gA8gfTy7X8Dfi96fCoNhrRuB9ZA= -github.com/lightninglabs/taproot-assets v0.5.0-rc1/go.mod h1:3MxoqsBdZGju3ExSIA9kFle5nqjWeb4alK4aVlfT0tA= +github.com/lightninglabs/taproot-assets v0.5.0-rc1.0.20241206085244-8113fa8bb314 h1:ykWP63wGxW0OKW4lITz8/lWv6CLMswRRX4DCr37wAkU= +github.com/lightninglabs/taproot-assets v0.5.0-rc1.0.20241206085244-8113fa8bb314/go.mod h1:d9GdY5DVoSh6+dEQRS4UUcjpVvZlAHgP8U2DAp4YedA= 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.18.4-beta.rc1 h1:z6hFKvtbfo8udPrIb81GbSoKlUWd06d4LRxTkD19IMQ= @@ -1438,20 +1438,20 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= diff --git a/itest/assets_test.go b/itest/assets_test.go index 97b399fe0..090d7c582 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -56,6 +56,10 @@ const ( DefaultPushSat int64 = 1062 ) +var ( + NoRfqIDOpt = fn.None[rfqmsg.ID]() +) + // 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 @@ -714,6 +718,9 @@ func sendAssetKeySendPayment(t *testing.T, src, dst *HarnessNode, amt uint64, result, err := getAssetPaymentResult(stream, false) require.NoError(t, err) + if result.Status == lnrpc.Payment_FAILED { + t.Logf("Failure reason: %v", result.FailureReason) + } require.Equal(t, expectedStatus, result.Status) expectedReason := failReason.UnwrapOr( @@ -772,7 +779,9 @@ func createAndPayNormalInvoiceWithBtc(t *testing.T, src, dst *HarnessNode, }) require.NoError(t, err) - payInvoiceWithSatoshi(t, src, invoiceResp, lnrpc.Payment_SUCCEEDED) + payInvoiceWithSatoshi( + t, src, invoiceResp, lnrpc.Payment_SUCCEEDED, false, + ) } func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode, @@ -792,7 +801,7 @@ func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode, numUnits, _ := payInvoiceWithAssets( t, src, rfqPeer, invoiceResp.PaymentRequest, assetID, smallShards, - fn.None[lnrpc.Payment_PaymentStatus](), + fn.None[lnrpc.Payment_PaymentStatus](), NoRfqIDOpt, ) return numUnits @@ -800,7 +809,7 @@ func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode, func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, invoice *lnrpc.AddInvoiceResponse, - expectedStatus lnrpc.Payment_PaymentStatus) { + expectedStatus lnrpc.Payment_PaymentStatus, expectTimeout bool) { ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -816,8 +825,12 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, require.NoError(t, err) result, err := getPaymentResult(stream) - require.NoError(t, err) - require.Equal(t, expectedStatus, result.Status) + if expectTimeout { + require.ErrorContains(t, err, "context deadline exceeded") + } else { + require.NoError(t, err) + require.Equal(t, expectedStatus, result.Status) + } } func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode, @@ -858,7 +871,8 @@ func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode, func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, payReq string, assetID []byte, smallShards bool, - expectedPayStatus fn.Option[lnrpc.Payment_PaymentStatus]) (uint64, + expectedPayStatus fn.Option[lnrpc.Payment_PaymentStatus], + manualRfq fn.Option[rfqmsg.ID]) (uint64, rfqmath.BigIntFixedPoint) { ctxb := context.Background() @@ -882,44 +896,62 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, sendReq.MaxShardSizeMsat = 80_000_000 } + var rfqBytes []byte + manualRfq.WhenSome(func(i rfqmsg.ID) { + rfqBytes = make([]byte, len(i[:])) + copy(rfqBytes, i[:]) + }) + stream, err := payerTapd.SendPayment(ctxt, &tchrpc.SendPaymentRequest{ AssetId: assetID, PeerPubkey: rfqPeer.PubKey[:], PaymentRequest: sendReq, + RfqId: rfqBytes, }) require.NoError(t, err) - // We want to receive the accepted quote message first, so we know how - // many assets we're going to pay. - quoteMsg, err := stream.Recv() - require.NoError(t, err) - acceptedQuote := quoteMsg.GetAcceptedSellOrder() - require.NotNil(t, acceptedQuote) + var ( + numUnits uint64 + rateVal rfqmath.FixedPoint[rfqmath.BigInt] + ) - peerPubKey := acceptedQuote.Peer - require.Equal(t, peerPubKey, rfqPeer.PubKeyStr) + if manualRfq.IsNone() { + // We want to receive the accepted quote message first, so we know how + // many assets we're going to pay. + quoteMsg, err := stream.Recv() + require.NoError(t, err) + acceptedQuote := quoteMsg.GetAcceptedSellOrder() + require.NotNil(t, acceptedQuote) - rpcRate := acceptedQuote.BidAssetRate - rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) - require.NoError(t, err) + peerPubKey := acceptedQuote.Peer + require.Equal(t, peerPubKey, rfqPeer.PubKeyStr) - t.Logf("Got quote for %v asset units per BTC", rate) + rpcRate := acceptedQuote.BidAssetRate + rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) + require.NoError(t, err) + + rateVal = *rate - amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat) - milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate) - numUnits := milliSatsFP.ScaleTo(0).ToUint64() - msatPerUnit := float64(decodedInvoice.NumMsat) / float64(numUnits) - t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+ - "with SCID %d", numUnits, msatPerUnit, peerPubKey, - acceptedQuote.Scid) + t.Logf("Got quote for %v asset units per BTC", rate) + + amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat) + milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate) + numUnits = milliSatsFP.ScaleTo(0).ToUint64() + msatPerUnit := float64(decodedInvoice.NumMsat) / float64(numUnits) + t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+ + "with SCID %d", numUnits, msatPerUnit, peerPubKey, + acceptedQuote.Scid) + } expectedStatus := expectedPayStatus.UnwrapOr(lnrpc.Payment_SUCCEEDED) - result, err := getAssetPaymentResult(stream, expectedPayStatus.IsSome()) + result, err := getAssetPaymentResult( + stream, expectedStatus == lnrpc.Payment_IN_FLIGHT, + ) require.NoError(t, err) require.Equal(t, expectedStatus, result.Status) - return numUnits, *rate + return numUnits, rateVal } func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index bc51a9b70..f4dd2e9d9 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -451,6 +451,13 @@ func getAssetPaymentResult( return nil, err } + // Ignore RFQ quote acceptance messages read from the send + // payment stream, as they are not relevant. + quote := msg.GetAcceptedSellOrder() + if quote != nil { + continue + } + payment := msg.GetPaymentResult() if payment == nil { return nil, fmt.Errorf("unexpected message: %v", msg) diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 0990d051f..ed6b60fe1 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -1,6 +1,7 @@ package itest import ( + "bytes" "context" "fmt" "math" @@ -15,9 +16,11 @@ import ( "github.com/lightninglabs/taproot-assets/itest" "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" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightninglabs/taproot-assets/tapscript" @@ -47,7 +50,7 @@ var ( shortTimeout = time.Second * 5 - defaultPaymentStatus = fn.None[lnrpc.Payment_PaymentStatus]() + defaultPaymentStatusOpt = fn.None[lnrpc.Payment_PaymentStatus]() ) var ( @@ -227,7 +230,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -242,7 +245,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, payInvoiceWithAssets( t.t, fabia, erin, invoiceResp2.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice 2") @@ -253,7 +256,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp3.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice 3") @@ -445,7 +448,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, dave, charlie, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice back") @@ -510,7 +513,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -527,7 +530,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, t.t, charlie, dave, daveInvoiceAssetAmount, assetID, ) payInvoiceWithSatoshi( - t.t, charlie, invoiceResp, lnrpc.Payment_FAILED, + t.t, charlie, invoiceResp, lnrpc.Payment_FAILED, false, ) logBalance(t.t, nodes, assetID, "after asset invoice paid with sats") @@ -555,7 +558,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -574,7 +577,9 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, invoiceResp = createAssetInvoice( t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, ) - payInvoiceWithSatoshi(t.t, dave, invoiceResp, lnrpc.Payment_SUCCEEDED) + payInvoiceWithSatoshi( + t.t, dave, invoiceResp, lnrpc.Payment_SUCCEEDED, false, + ) logBalance(t.t, nodes, assetID, "after invoice") erinAssetBalance -= fabiaInvoiceAssetAmount2 @@ -592,7 +597,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -613,7 +618,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after asset-to-asset") @@ -949,7 +954,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -987,7 +992,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -1006,7 +1011,9 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, invoiceResp = createAssetInvoice( t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, ) - payInvoiceWithSatoshi(t.t, dave, invoiceResp, lnrpc.Payment_SUCCEEDED) + payInvoiceWithSatoshi( + t.t, dave, invoiceResp, lnrpc.Payment_SUCCEEDED, false, + ) logBalance(t.t, nodes, assetID, "after invoice") erinAssetBalance -= fabiaInvoiceAssetAmount2 @@ -1024,7 +1031,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -1045,7 +1052,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, ) payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after asset-to-asset") @@ -1805,7 +1812,7 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, SatPerVByte: 5, }, ) - defer closeChannelAndAssert(t, net, dave, channelOp, false) + defer closeChannelAndAssert(t, net, dave, channelOp, true) // This is the only public channel, we need everyone to be aware of it. assertChannelKnown(t.t, charlie, channelOp) @@ -1958,7 +1965,7 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ @@ -2004,7 +2011,7 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, payInvoiceWithAssets( t.t, yara, dave, invoiceResp.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ @@ -2015,15 +2022,18 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, // 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, lnrpc.Payment_FAILED) + payInvoiceWithSatoshi( + t.t, yara, invoiceResp, lnrpc.Payment_FAILED, false, + ) logBalance(t.t, nodes, assetID, "after small payment (asset "+ "invoice, <354sats)") - // Edge case: Now Charlie creates an asset invoice to be paid for by + // 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 Charlie's strict forwarding - // works as expected. + // 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, ) @@ -2046,6 +2056,144 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, logBalance(t.t, nodes, assetID, "after failed payment (asset "+ "invoice, strict forwarding)") + + // Edge case: Check if the RFQ HTLC tracking accounts for cancelled + // htlcs. We achieve this by manually creating & using an RFQ quote with + // a set max amount. We first pay to a hodl invoice that we eventually + // cancel, then pay to a normal invoice which should succeed. + + // We start by sloshing some funds in the Erin<->Fabia + sendAssetKeySendPayment( + t.t, erin, fabia, 100_000, assetID, + fn.Some[int64](20_000), lnrpc.Payment_SUCCEEDED, + fn.None[lnrpc.PaymentFailureReason](), + ) + + logBalance(t.t, nodes, assetID, "balance after 1st slosh") + + // We create the RFQ order. We set the max amt to ~180k sats which is + // going to evaluate to about 10k assets. + resQ, err := charlieTap.RfqClient.AddAssetSellOrder( + ctxb, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + PaymentMaxAmt: 180_000_000, + Expiry: uint64(time.Now().Add(time.Hour).Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 100, + }, + ) + require.NoError(t.t, err) + + // We now create a hodl invoice on Fabia, for 10k assets. + hodlInv := createAssetHodlInvoice(t.t, erin, fabia, 10_000, assetID) + + // Charlie tries to pay via Dave, by providing the rfq quote ID that was + // manually created above. + var quoteID rfqmsg.ID + copy(quoteID[:], resQ.GetAcceptedQuote().Id) + payInvoiceWithAssets( + t.t, charlie, dave, hodlInv.payReq, assetID, true, + fn.Some(lnrpc.Payment_IN_FLIGHT), fn.Some(quoteID), + ) + + // We now assert that the expected numbers of HTLCs are present on each + // node. + // Reminder, topology looks like this: + // + // Charlie <-> Dave <-> Erin <-> Fabia + // + // Therefore the routing nodes should have double the number of HTLCs + // required for the payment present. + assertNumHtlcs(t.t, charlie, 3) + assertNumHtlcs(t.t, dave, 6) + assertNumHtlcs(t.t, erin, 6) + assertNumHtlcs(t.t, fabia, 3) + + // Now let's cancel the invoice on Fabia. + payHash := hodlInv.preimage.Hash() + _, err = fabia.InvoicesClient.CancelInvoice( + ctxb, &invoicesrpc.CancelInvoiceMsg{ + PaymentHash: payHash[:], + }, + ) + require.NoError(t.t, err) + + // There should be no HTLCs present on any channel. + assertNumHtlcs(t.t, charlie, 0) + assertNumHtlcs(t.t, dave, 0) + assertNumHtlcs(t.t, erin, 0) + assertNumHtlcs(t.t, fabia, 0) + + // Now Fabia creates the normal invoice. + invoiceResp = createAssetInvoice( + t.t, erin, fabia, 10_000, assetID, + ) + + // Now Charlie pays the invoice, again by using the manually specified + // rfq quote ID. This payment should succeed. + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, true, + defaultPaymentStatusOpt, fn.Some(quoteID), + ) + + logBalance(t.t, nodes, assetID, "after manual rfq hodl") + + // Edge case: Charlie negotiates a quote with Dave which has a low max + // amount (~170k sats). Then Charlie creates an invoice with a total + // amount slightly larger than the max allowed in the quote (200k sats). + // Erin will try to pay that invoice with sats, in shards of max size + // 80k sats. Dave will eventually stop forwarding HTLCs as the RFQ HTLC + // tracking mechanism should stop them from being forwarded, as they + // violate the maximum allowed amount of the quote. + + // Charlie starts by negotiating the quote. + res, err := charlieTap.RfqClient.AddAssetBuyOrder( + ctxb, &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + AssetMaxAmt: 10_000, + Expiry: uint64(time.Now().Add(time.Hour).Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 10, + }, + ) + require.NoError(t.t, err) + + quote, ok := res.Response.(*rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote) + require.True(t.t, ok) + + // We now manually add the invoice in order to inject the above, + // manually generated, quote. + iResp, err := charlie.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "", + Value: 200_000, + RPreimage: bytes.Repeat([]byte{11}, 32), + CltvExpiry: 60, + RouteHints: []*lnrpc.RouteHint{ + { + HopHints: []*lnrpc.HopHint{ + { + NodeId: dave.PubKeyStr, + ChanId: quote.AcceptedQuote.Scid, + }, + }, + }, + }, + }) + require.NoError(t.t, err) + + // 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, lnrpc.Payment_FAILED, true) + + logBalance(t.t, nodes, assetID, "after small manual rfq") } // testCustomChannelsBalanceConsistency is a test that test the balance of nodes @@ -2563,7 +2711,7 @@ func testCustomChannelsOraclePricing(_ context.Context, numUnits, rate := payInvoiceWithAssets( t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, false, - defaultPaymentStatus, + defaultPaymentStatusOpt, NoRfqIDOpt, ) logBalance(t.t, nodes, assetID, "after invoice") @@ -2911,13 +3059,13 @@ func runCustomChannelsHtlcForceClose(ctxb context.Context, t *harnessTest, for _, aliceInvoice := range aliceHodlInvoices { payInvoiceWithAssets( t.t, bob, alice, aliceInvoice.payReq, assetID, mpp, - fn.Some(lnrpc.Payment_IN_FLIGHT), + fn.Some(lnrpc.Payment_IN_FLIGHT), NoRfqIDOpt, ) } for _, bobInvoice := range bobHodlInvoices { payInvoiceWithAssets( t.t, alice, bob, bobInvoice.payReq, assetID, false, - fn.Some(lnrpc.Payment_IN_FLIGHT), + fn.Some(lnrpc.Payment_IN_FLIGHT), NoRfqIDOpt, ) }