diff --git a/cmd/commands/rfq.go b/cmd/commands/rfq.go index ed3ba67218..d4a13f25a8 100644 --- a/cmd/commands/rfq.go +++ b/cmd/commands/rfq.go @@ -1,6 +1,7 @@ package commands import ( + "encoding/hex" "fmt" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" @@ -15,6 +16,7 @@ var rfqCommands = []cli.Command{ Category: "Channels", Subcommands: []cli.Command{ acceptedQuotesCommand, + forwardingHistoryCommand, }, }, } @@ -45,3 +47,123 @@ func acceptedQuotes(ctx *cli.Context) error { return nil } + +const ( + minTimestampName = "min_timestamp" + maxTimestampName = "max_timestamp" + peerName = "peer" +) + +var forwardingHistoryCommand = cli.Command{ + Name: "forwardinghistory", + ShortName: "f", + Usage: "query historical asset forwarding events", + Description: ` + Query historical records of asset forwarding events executed by the RFQ + system. This provides accounting and record-keeping for edge nodes that + perform asset swaps. Events are recorded when opened and updated when + they settle or fail. +`, + Flags: []cli.Flag{ + cli.Uint64Flag{ + Name: minTimestampName, + Usage: "minimum Unix timestamp in seconds; only " + + "events opened at or after this time are " + + "returned", + }, + cli.Uint64Flag{ + Name: maxTimestampName, + Usage: "maximum Unix timestamp in seconds; only " + + "events opened at or before this time are " + + "returned", + }, + cli.StringFlag{ + Name: peerName, + Usage: "filter by peer public key", + }, + cli.StringFlag{ + Name: assetIDName, + Usage: "filter by asset ID", + }, + cli.StringFlag{ + Name: groupKeyName, + Usage: "filter by asset group key", + }, + cli.IntFlag{ + Name: limitName, + Usage: "maximum number of records to return", + Value: 100, + }, + cli.IntFlag{ + Name: offsetName, + Usage: "number of records to skip", + Value: 0, + }, + }, + Action: queryForwardingHistory, +} + +func queryForwardingHistory(ctx *cli.Context) error { + ctxc := getContext() + client, cleanUp := getRfqClient(ctx) + defer cleanUp() + + req := &rfqrpc.ForwardingHistoryRequest{ + MinTimestamp: ctx.Uint64(minTimestampName), + MaxTimestamp: ctx.Uint64(maxTimestampName), + Limit: int32(ctx.Int(limitName)), + Offset: int32(ctx.Int(offsetName)), + } + + // Parse peer if provided. + if peerStr := ctx.String(peerName); peerStr != "" { + peerBytes, err := hex.DecodeString(peerStr) + if err != nil { + return fmt.Errorf("invalid peer hex: %w", err) + } + req.Peer = peerBytes + } + + // Parse asset specifier if provided. + assetIDStr := ctx.String(assetIDName) + groupKeyStr := ctx.String(groupKeyName) + + // Check for mutual exclusivity. + if assetIDStr != "" && groupKeyStr != "" { + return fmt.Errorf("cannot specify both --%s and --%s", + assetIDName, groupKeyName) + } + + if assetIDStr != "" || groupKeyStr != "" { + req.AssetSpecifier = &rfqrpc.AssetSpecifier{} + + if assetIDStr != "" { + assetID, err := hex.DecodeString(assetIDStr) + if err != nil { + return fmt.Errorf("invalid asset ID hex: %w", + err) + } + req.AssetSpecifier.Id = &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + } + } else if groupKeyStr != "" { + groupKey, err := hex.DecodeString(groupKeyStr) + if err != nil { + return fmt.Errorf("invalid group key hex: %w", + err) + } + req.AssetSpecifier.Id = &rfqrpc.AssetSpecifier_GroupKey{ + GroupKey: groupKey, + } + } + } + + resp, err := client.ForwardingHistory(ctxc, req) + if err != nil { + return fmt.Errorf("unable to query forwarding history: %w", err) + } + + printRespJSON(resp) + + return nil +} diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md index 226a26a404..0ca09e417f 100644 --- a/docs/release-notes/release-notes-0.8.0.md +++ b/docs/release-notes/release-notes-0.8.0.md @@ -56,10 +56,27 @@ ## Functional Enhancements +- [Forwarding History Tracking](https://github.com/lightninglabs/taproot-assets/pull/1921): + Routing nodes can now track and query historical asset forwarding events. + When a node successfully routes an asset payment, the forward event is logged. + This provides edge nodes with an audit trail of their swap activity. + ## RPC Additions +- [ForwardingHistory RPC](https://github.com/lightninglabs/taproot-assets/pull/1921): + New RPC endpoint `rfqrpc.ForwardingHistory` allows querying historical + forwarding events with filtering and pagination support. Filters include: + timestamp range (min/max), peer public key, asset ID, and asset group key. + ## tapcli Additions +- [tapcli rfq forwardinghistory](https://github.com/lightninglabs/taproot-assets/pull/1921): + New CLI command `tapcli rfq forwardinghistory` (alias: `f`) to query forwarding event + history. Supports flags for filtering by timestamp (`--min-timestamp`, + `--max-timestamp`), peer (`--peer`), asset ID (`--asset-id`), and asset + group key (`--group-key`). Includes pagination support via `--limit` and + `--offset` flags. + # Improvements ## Functional Updates @@ -156,8 +173,15 @@ creation hits an unreachable mailbox courier with the upfront connection check skipped, ensuring mailbox subscription failures do not crash tapd. +- [Forwarding History Integration Test](https://github.com/lightninglabs/taproot-assets/pull/1921): + New integration test `testForwardingEventHistory` verifies that forwarding events are + properly logged when routing asset payments. + ## Database +- [forwards table](https://github.com/lightninglabs/taproot-assets/pull/1921): + New database table `forwards` stores historical forwarding events. + ## Code Health ## Tooling and Documentation diff --git a/itest/rfq_forwards_test.go b/itest/rfq_forwards_test.go new file mode 100644 index 0000000000..8334f26a55 --- /dev/null +++ b/itest/rfq_forwards_test.go @@ -0,0 +1,351 @@ +package itest + +import ( + "bytes" + "context" + "fmt" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest/port" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// testForwardingEventHistory tests that forwarding events are properly logged +// and can be +// queried via the ForwardingHistory RPC endpoint. +// +// The procedure is as follows: +// 1. Alice sends an asset sell order to Bob. +// 2. Bob accepts the quote, creating a purchase policy. +// 3. Alice sends a payment with asset custom records to Carol via Bob. +// 4. Bob intercepts the HTLC and validates it against the accepted quote. +// 5. The payment settles successfully. +// 6. We query Bob's forwarding history and verify the event was recorded. +// 7. We test query filters (timestamp, peer, asset_id) and pagination. +func testForwardingEventHistory(t *harnessTest) { + oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort()) + oracle := newOracleHarness(oracleAddr) + oracle.start(t.t) + t.t.Cleanup(oracle.stop) + + oracleURL := fmt.Sprintf("rfqrpc://%s", oracleAddr) + ts := newRfqTestScenario(t, WithRfqOracleServer(oracleURL)) + + // Mint an asset with Alice's tapd node. + rpcAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner().Client, ts.AliceTapd, + []*mintrpc.MintAssetRequest{issuableAssets[0]}, + ) + mintedAssetIdBytes := rpcAssets[0].AssetGenesis.AssetId + + var mintedAssetId asset.ID + copy(mintedAssetId[:], mintedAssetIdBytes[:]) + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) + defer cancel() + + // Add an asset buy offer to Bob's tapd node. + _, err := ts.BobTapd.AddAssetBuyOffer( + ctxt, &rfqrpc.AddAssetBuyOfferRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetIdBytes, + }, + }, + MaxUnits: 1000, + }, + ) + require.NoError(t.t, err) + + aliceEventNtfns, err := ts.AliceTapd.SubscribeRfqEventNtfns( + ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + // Alice sends a sell order to Bob. + askAmt := uint64(46258) + sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix()) + + sellReq := &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetIdBytes, + }, + }, + PaymentMaxAmt: askAmt, + Expiry: sellOrderExpiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + PriceOracleMetadata: "forward-history-test", + } + + // Set up the expected oracle calls. + buySpecifier := &oraclerpc.AssetSpecifier{ + Id: &oraclerpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetIdBytes, + }, + } + btcSpecifier := &oraclerpc.AssetSpecifier{ + Id: &oraclerpc.AssetSpecifier_AssetId{ + AssetId: bytes.Repeat([]byte{0}, 32), + }, + } + + expiryTimestamp := uint64(time.Now().Add(time.Minute).Unix()) + mockResult := &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Ok{ + Ok: &oraclerpc.QueryAssetRatesOkResponse{ + AssetRates: &oraclerpc.AssetRates{ + SubjectAssetRate: &oraclerpc.FixedPoint{ + Coefficient: "1101000", + Scale: 3, + }, + ExpiryTimestamp: expiryTimestamp, + }, + }, + }, + } + + oracle.On( + "QueryAssetRates", oraclerpc.TransactionType_SALE, + buySpecifier, mock.Anything, btcSpecifier, + askAmt, mock.Anything, + oraclerpc.Intent_INTENT_PAY_INVOICE_HINT, + mock.Anything, "forward-history-test", + ).Return(mockResult, nil).Once() + + oracle.On( + "QueryAssetRates", oraclerpc.TransactionType_PURCHASE, + buySpecifier, mock.Anything, btcSpecifier, + askAmt, mock.Anything, + oraclerpc.Intent_INTENT_PAY_INVOICE, + mock.Anything, "forward-history-test", + ).Return(mockResult, nil).Once() + + oracle.On( + "QueryAssetRates", oraclerpc.TransactionType_SALE, + buySpecifier, mock.Anything, btcSpecifier, + askAmt, mock.Anything, + oraclerpc.Intent_INTENT_PAY_INVOICE_QUALIFY, + mock.Anything, "forward-history-test", + ).Return(mockResult, nil).Once() + + defer oracle.AssertExpectations(t.t) + + _, err = ts.AliceTapd.AddAssetSellOrder(ctxt, sellReq) + require.NoError(t.t, err) + + // Wait for Alice to receive the quote accept from Bob. + BeforeTimeout(t.t, func() { + event, err := aliceEventNtfns.Recv() + require.NoError(t.t, err) + _, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote) + require.True(t.t, ok) + }, rfqTimeout) + + acceptedQuotes, err := ts.AliceTapd.QueryPeerAcceptedQuotes( + ctxt, &rfqrpc.QueryPeerAcceptedQuotesRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, acceptedQuotes.SellQuotes, 1) + + acceptedQuote := acceptedQuotes.SellQuotes[0] + var acceptedQuoteId rfqmsg.ID + copy(acceptedQuoteId[:], acceptedQuote.Id[:]) + + // Record timestamp before payment for filtering tests. + timestampBeforePayment := time.Now().Unix() + + bobEventNtfns, err := ts.BobTapd.SubscribeRfqEventNtfns( + ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + // Carol generates an invoice for Alice to settle via Bob. + addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: int64(askAmt), + }) + invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash) + payReq := ts.CarolLnd.RPC.DecodePayReq(invoice.PaymentRequest) + + // Construct route: Alice -> Bob -> Carol. + routeBuildResp := ts.AliceLnd.RPC.BuildRoute( + &routerrpc.BuildRouteRequest{ + AmtMsat: int64(askAmt), + HopPubkeys: [][]byte{ + ts.BobLnd.PubKey[:], + ts.CarolLnd.PubKey[:], + }, + PaymentAddr: payReq.PaymentAddr, + }, + ) + + // Construct first hop custom records. + assetAmounts := []*rfqmsg.AssetBalance{ + rfqmsg.NewAssetBalance(mintedAssetId, 42), + } + htlcCustomRecords := rfqmsg.NewHtlc( + assetAmounts, fn.Some(acceptedQuoteId), fn.None[[]rfqmsg.ID](), + ) + firstHopCustomRecords, err := tlv.RecordsToMap( + htlcCustomRecords.Records(), + ) + require.NoError(t.t, err) + + // Send the payment. + sendAttempt := ts.AliceLnd.RPC.SendToRouteV2( + &routerrpc.SendToRouteRequest{ + PaymentHash: invoice.RHash, + Route: routeBuildResp.Route, + FirstHopCustomRecords: firstHopCustomRecords, + }, + ) + require.Equal(t.t, lnrpc.HTLCAttempt_SUCCEEDED, sendAttempt.Status) + + // Wait for Bob to accept the HTLC. + BeforeTimeout(t.t, func() { + event, err := bobEventNtfns.Recv() + require.NoError(t.t, err) + _, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc) + require.True(t.t, ok) + }, rfqTimeout) + + // Confirm Carol received the payment. + invoice = ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash) + require.Equal(t.t, lnrpc.Invoice_SETTLED, invoice.State) + + timestampAfterPayment := time.Now().Unix() + + // Wait for the forward to be logged. + var forwardsResp *rfqrpc.ForwardingHistoryResponse + BeforeTimeout(t.t, func() { + var err error + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 1) + }, rfqTimeout) + + fwd := forwardsResp.Forwards[0] + + // Verify forwarding event fields. + require.Equal(t.t, acceptedQuote.Id, fwd.RfqId) + require.Equal(t.t, rfqrpc.RfqPolicyType_RFQ_POLICY_TYPE_PURCHASE, + fwd.PolicyType) + require.Equal(t.t, ts.AliceLnd.PubKeyStr, fwd.Peer) + require.NotNil(t.t, fwd.AssetSpec) + require.Equal(t.t, mintedAssetIdBytes, fwd.AssetSpec.Id) + require.Equal(t.t, uint64(42), fwd.AssetAmt) + + // Verify opened_at is within the expected range. + require.GreaterOrEqual(t.t, fwd.OpenedAt, + uint64(timestampBeforePayment), + ) + require.LessOrEqual(t.t, fwd.OpenedAt, uint64(timestampAfterPayment+10)) + + // Verify settled_at is within the expected range and after opened_at. + require.GreaterOrEqual(t.t, fwd.SettledAt, fwd.OpenedAt) + require.LessOrEqual(t.t, fwd.SettledAt, + uint64(timestampAfterPayment+10), + ) + + // Verify failed_at is 0 for a successful forward. + require.Zero(t.t, fwd.FailedAt) + + // Verify amount fields are set. + require.Equal(t.t, fwd.AmtInMsat, askAmt+1000) + require.Equal(t.t, fwd.AmtOutMsat, askAmt) + require.Equal(t.t, fwd.Rate.Coefficient, "1101000") + require.Equal(t.t, int64(1), forwardsResp.TotalCount) + + // Test timestamp filters. + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + MinTimestamp: uint64(timestampBeforePayment), + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 1) + + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + MaxTimestamp: uint64(timestampBeforePayment - 10), + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 0) + + // Test peer filter. + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + Peer: ts.AliceLnd.PubKey[:], + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 1) + + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + Peer: ts.CarolLnd.PubKey[:], + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 0) + + // Test asset filter. + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetIdBytes, + }, + }, + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 1) + + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: bytes.Repeat([]byte{0xab}, 32), + }, + }, + }, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 0) + + // Test pagination. + forwardsResp, err = ts.BobTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{Limit: 100, Offset: 10}, + ) + require.NoError(t.t, err) + require.Len(t.t, forwardsResp.Forwards, 0) + require.Equal(t.t, int64(1), forwardsResp.TotalCount) + + // Alice should have no forwards (she's not an edge node). + aliceForwardsResp, err := ts.AliceTapd.ForwardingHistory( + ctxt, &rfqrpc.ForwardingHistoryRequest{}, + ) + require.NoError(t.t, err) + require.Len(t.t, aliceForwardsResp.Forwards, 0) + + // Cleanup. + require.NoError(t.t, aliceEventNtfns.CloseSend()) + require.NoError(t.t, bobEventNtfns.CloseSend()) +} diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 1898c4c11e..b5fdca2e05 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -351,6 +351,10 @@ var allTestCases = []*testCase{ name: "rfq negotiation group key", test: testRfqNegotiationGroupKey, }, + { + name: "rfq forwarding history", + test: testForwardingEventHistory, + }, { name: "multi signature on all levels", test: testMultiSignature, diff --git a/lndservices/router_client.go b/lndservices/router_client.go index 0969ca1887..2af3879218 100644 --- a/lndservices/router_client.go +++ b/lndservices/router_client.go @@ -5,6 +5,7 @@ import ( "github.com/lightninglabs/lndclient" "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnwire" ) @@ -61,6 +62,23 @@ func (l *LndRouterClient) SubscribeHtlcEvents( return l.lnd.Router.SubscribeHtlcEvents(ctx) } +// LookupHtlcResolution retrieves the final resolution for an HTLC. +func (l *LndRouterClient) LookupHtlcResolution(ctx context.Context, + chanID uint64, htlcID uint64) (*lnrpc.LookupHtlcResolutionResponse, + error) { + + rpcCtx, timeout, client := l.lnd.Client.RawClientWithMacAuth(ctx) + rpcCtx, cancel := context.WithTimeout(rpcCtx, timeout) + defer cancel() + + return client.LookupHtlcResolution(rpcCtx, + &lnrpc.LookupHtlcResolutionRequest{ + ChanId: chanID, + HtlcIndex: htlcID, + }, + ) +} + // Ensure LndRouterClient implements the rfq.HtlcInterceptor interface. var _ rfq.HtlcInterceptor = (*LndRouterClient)(nil) var _ rfq.ScidAliasManager = (*LndRouterClient)(nil) diff --git a/rfq/interface.go b/rfq/interface.go index 3ab85d9a49..91d31f9492 100644 --- a/rfq/interface.go +++ b/rfq/interface.go @@ -2,10 +2,31 @@ package rfq import ( "context" + "time" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/routing/route" ) +// RfqPolicyType denotes the type of a persisted RFQ policy. +type RfqPolicyType string + +const ( + // RfqPolicyTypeAssetSale identifies an asset sale policy. + RfqPolicyTypeAssetSale RfqPolicyType = "RFQ_POLICY_TYPE_SALE" + + // RfqPolicyTypeAssetPurchase identifies an asset purchase policy. + RfqPolicyTypeAssetPurchase RfqPolicyType = "RFQ_POLICY_TYPE_PURCHASE" +) + +// String converts the policy type to its string representation. +func (t RfqPolicyType) String() string { + return string(t) +} + // PolicyStore abstracts persistence of RFQ policies. type PolicyStore interface { // StoreSalePolicy stores an asset sale policy. @@ -18,3 +39,130 @@ type PolicyStore interface { FetchAcceptedQuotes(ctx context.Context) ([]rfqmsg.BuyAccept, []rfqmsg.SellAccept, error) } + +// ForwardInput contains the data needed to upsert a forward event. +type ForwardInput struct { + // OpenedAt is the time when the forward was initiated. + OpenedAt time.Time + + // SettledAt is the time when the forward settled (if any). + SettledAt fn.Option[time.Time] + + // FailedAt is the time when the forward failed (if any). + FailedAt fn.Option[time.Time] + + // RfqID is the RFQ session identifier for this forward. + RfqID rfqmsg.ID + + // ChanIDIn is the short channel ID of the incoming channel. + ChanIDIn uint64 + + // ChanIDOut is the short channel ID of the outgoing channel. + ChanIDOut uint64 + + // HtlcID is the HTLC ID on the incoming channel. + HtlcID uint64 + + // AssetAmt is the asset amount involved in this swap. + AssetAmt uint64 + + // AmtInMsat is the actual amount received on the incoming channel in + // millisatoshis. + AmtInMsat uint64 + + // AmtOutMsat is the actual amount sent on the outgoing channel in + // millisatoshis. + AmtOutMsat uint64 +} + +// QueryForwardsParams contains the parameters for querying forwarding event +// records. +type QueryForwardsParams struct { + // MinTimestamp filters forwarding events to those settled at or after + // this time. + // None means no lower bound. + MinTimestamp fn.Option[time.Time] + + // MaxTimestamp filters forwarding events to those settled at or before + // this + // time. None means no upper bound. + MaxTimestamp fn.Option[time.Time] + + // Peer filters forwarding events to those with this counterparty. + // Nil means no filter. + Peer *route.Vertex + + // AssetSpecifier filters forwarding events to those involving this + // asset or + // asset group. Nil means no filter. + AssetSpecifier *asset.Specifier + + // Limit is the maximum number of records to return. + Limit int32 + + // Offset is the number of records to skip (for pagination). + Offset int32 +} + +// ForwardingEvent is a complete forwarding event record including policy data. +type ForwardingEvent struct { + // OpenedAt is the time when the forward was initiated. + OpenedAt time.Time + + // SettledAt is the time when the forward settled (nil if not settled). + SettledAt *time.Time + + // FailedAt is the time when the forward failed (nil if not failed). + FailedAt *time.Time + + // RfqID is the RFQ session identifier. + RfqID rfqmsg.ID + + // ChanIDIn is the short channel ID of the incoming channel. + ChanIDIn uint64 + + // ChanIDOut is the short channel ID of the outgoing channel. + ChanIDOut uint64 + + // HtlcID is the HTLC ID on the incoming channel. + HtlcID uint64 + + // AssetAmt is the asset amount involved in this swap. + AssetAmt uint64 + + // AmtInMsat is the actual amount received on the incoming channel in + // millisatoshis. + AmtInMsat uint64 + + // AmtOutMsat is the actual amount sent on the outgoing channel in + // millisatoshis. + AmtOutMsat uint64 + + // PolicyType indicates whether this was a sale or purchase from the + // edge node's perspective. + PolicyType RfqPolicyType + + // Peer is the counterparty peer's public key. + Peer route.Vertex + + // AssetSpecifier identifies the specific asset or asset group (if set). + AssetSpecifier asset.Specifier + + // Rate is the exchange rate used for this forward. + Rate rfqmath.BigIntFixedPoint +} + +// ForwardStore abstracts persistence of forwarding events. +type ForwardStore interface { + // UpsertForward inserts or updates a forwarding event record in the + // database. + UpsertForward(ctx context.Context, input ForwardInput) error + + // PendingForwards retrieves forwards that haven't settled or failed. + PendingForwards(ctx context.Context) ([]ForwardInput, error) + + // QueryForwardsWithCount retrieves forwarding event records matching + // the given filters along with the total count. + QueryForwardsWithCount(ctx context.Context, + params QueryForwardsParams) ([]ForwardingEvent, int64, error) +} diff --git a/rfq/manager.go b/rfq/manager.go index d7003f8cfc..801118fd42 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -131,6 +131,11 @@ type ManagerCfg struct { // PolicyStore provides persistence for agreed RFQ policies. PolicyStore PolicyStore + // ForwardStore provides persistence for forwarding events. This is + // used for edge node accounting. If nil, forwarding event logging is + // disabled. + ForwardStore ForwardStore + // AcceptPriceDeviationPpm is the price deviation in // parts per million that is accepted by the RFQ negotiator. // @@ -248,6 +253,7 @@ func (m *Manager) startSubsystems(ctx context.Context) error { AuxChanNegotiator: m.cfg.AuxChanNegotiator, ErrChan: m.subsystemErrChan, PolicyStore: m.cfg.PolicyStore, + ForwardStore: m.cfg.ForwardStore, }) if err != nil { return fmt.Errorf("error initializing RFQ order handler: %w", @@ -1304,6 +1310,18 @@ func (m *Manager) publishSubscriberEvent(event fn.Event) { ) } +// QueryForwardsWithCount retrieves historical forwarding event records +// matching the given filters, along with the total count. +func (m *Manager) QueryForwardsWithCount(ctx context.Context, + params QueryForwardsParams) ([]ForwardingEvent, int64, error) { + + if m.cfg.ForwardStore == nil { + return nil, 0, fmt.Errorf("forward store not configured") + } + + return m.cfg.ForwardStore.QueryForwardsWithCount(ctx, params) +} + // EstimateAssetUnits is a helper function that queries our price oracle to find // out how many units of an asset are needed to evaluate to the provided amount // in milli satoshi. diff --git a/rfq/order.go b/rfq/order.go index 21417dfc53..326d20dfa7 100644 --- a/rfq/order.go +++ b/rfq/order.go @@ -3,6 +3,7 @@ package rfq import ( "bytes" "context" + "encoding/hex" "fmt" "sync" "time" @@ -16,12 +17,15 @@ import ( "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/tapfeatures" "github.com/lightningnetwork/lnd/graph/db/models" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // parseHtlcCustomRecords parses a HTLC custom record to extract any data which @@ -57,6 +61,56 @@ func parseHtlcCustomRecords(customRecords map[uint64][]byte) (*rfqmsg.Htlc, // SerialisedScid is a serialised short channel id (SCID). type SerialisedScid = rfqmsg.SerialisedScid +// computeHtlcAssetAmount derives the asset amount carried by the HTLC from the +// custom records or computes it from the msat amount and rate. +func computeHtlcAssetAmount(ctx context.Context, policy Policy, + htlc lndclient.InterceptedHtlc, + specifierChecker rfqmsg.SpecifierChecker) (uint64, error) { + + switch p := policy.(type) { + case *AssetSalePolicy: + // For asset sales (BTC in, assets out), compute the asset + // amount from the outgoing msat amount using the agreed rate. + assetAmt := rfqmath.MilliSatoshiToUnits( + htlc.AmountOutMsat, p.AskAssetRate, + ) + + return assetAmt.ToUint64(), nil + + case *AssetPurchasePolicy: + // For asset purchases (assets in, BTC out), extract the asset + // amount from the incoming HTLC's custom records. + htlcRecord, err := parseHtlcCustomRecords( + htlc.InWireCustomRecords, + ) + if err != nil { + return 0, fmt.Errorf("parsing HTLC custom records "+ + "failed: %w", err) + } + + assetAmt, err := htlcRecord.SumAssetBalance( + ctx, p.AssetSpecifier, specifierChecker, + ) + if err != nil { + return 0, fmt.Errorf("summing asset balance failed: "+ + "%w", err) + } + + return assetAmt.ToUint64(), nil + + case *AssetForwardPolicy: + // Asset-to-asset forwards use the incoming purchase policy to + // determine the asset amount. + return computeHtlcAssetAmount( + ctx, p.incomingPolicy, htlc, specifierChecker, + ) + + default: + return 0, fmt.Errorf("unsupported policy type %T for HTLC "+ + "asset extraction", policy) + } +} + // Policy is an interface that abstracts the terms which determine whether an // asset sale/purchase channel HTLC is accepted or rejected. type Policy interface { @@ -75,6 +129,9 @@ type Policy interface { // which the policy applies. Scid() uint64 + // RfqID returns the RFQ session identifier for this policy. + RfqID() rfqmsg.ID + // TrackAcceptedHtlc makes the policy aware of this new accepted HTLC. // This is important in cases where the set of existing HTLCs may affect // whether the next compliance check passes. @@ -262,6 +319,11 @@ func (c *AssetSalePolicy) Scid() uint64 { return uint64(c.AcceptedQuoteId.Scid()) } +// RfqID returns the RFQ session identifier for this policy. +func (c *AssetSalePolicy) RfqID() rfqmsg.ID { + return c.AcceptedQuoteId +} + // GenerateInterceptorResponse generates an interceptor response for the policy. func (c *AssetSalePolicy) GenerateInterceptorResponse( htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse, @@ -507,6 +569,11 @@ func (c *AssetPurchasePolicy) Scid() uint64 { return uint64(c.scid) } +// RfqID returns the RFQ session identifier for this policy. +func (c *AssetPurchasePolicy) RfqID() rfqmsg.ID { + return c.AcceptedQuoteId +} + // GenerateInterceptorResponse generates an interceptor response for the policy. func (c *AssetPurchasePolicy) GenerateInterceptorResponse( htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse, @@ -636,6 +703,12 @@ func (a *AssetForwardPolicy) Scid() uint64 { return a.incomingPolicy.Scid() } +// RfqID returns the RFQ session identifier for this policy. For forward +// policies, we use the incoming policy's RFQ ID. +func (a *AssetForwardPolicy) RfqID() rfqmsg.ID { + return a.incomingPolicy.RfqID() +} + // GenerateInterceptorResponse generates an interceptor response for the policy. func (a *AssetForwardPolicy) GenerateInterceptorResponse( htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse, @@ -718,6 +791,10 @@ type OrderHandlerCfg struct { // PolicyStore persists agreed RFQ policies. PolicyStore PolicyStore + + // ForwardStore persists forwarding events for accounting. + // If nil, forwarding event logging is disabled. + ForwardStore ForwardStore } // OrderHandler orchestrates management of accepted quote bundles. It monitors @@ -770,6 +847,13 @@ type OrderHandler struct { // data available, so we need to cache this info. htlcToPolicy lnutils.SyncMap[models.CircuitKey, Policy] + // htlcToForward maps an HTLC circuit key to the forward input data + // needed for logging. This is populated when an HTLC is accepted. + htlcToForward lnutils.SyncMap[models.CircuitKey, *ForwardInput] + + // forwardStore persists forwarding events for accounting. + forwardStore ForwardStore + // ContextGuard provides a wait group and main quit channel that can be // used to create guarded contexts. *fn.ContextGuard @@ -778,9 +862,10 @@ type OrderHandler struct { // NewOrderHandler creates a new struct instance. func NewOrderHandler(cfg OrderHandlerCfg) (*OrderHandler, error) { return &OrderHandler{ - cfg: cfg, - policyStore: cfg.PolicyStore, - policies: lnutils.SyncMap[SerialisedScid, Policy]{}, + cfg: cfg, + policyStore: cfg.PolicyStore, + forwardStore: cfg.ForwardStore, + policies: lnutils.SyncMap[SerialisedScid, Policy]{}, ContextGuard: &fn.ContextGuard{ DefaultTimeout: DefaultTimeout, Quit: make(chan struct{}), @@ -854,6 +939,52 @@ func (h *OrderHandler) handleIncomingHtlc(ctx context.Context, // accepted HTLC. policy.TrackAcceptedHtlc(htlc.IncomingCircuitKey, htlc.AmountOutMsat) + // Log the forwarding event when the HTLC is accepted. We store the + // circuit key + // so we can later update the record when the HTLC settles or fails. + if h.forwardStore != nil { + assetAmt, err := computeHtlcAssetAmount( + ctx, policy, htlc, h.cfg.SpecifierChecker, + ) + if err != nil { + log.Warnf("Skipping forwarding event logging, "+ + "failed to extract asset amount from HTLC: %v", + err) + } else { + chanIdIn := htlc.IncomingCircuitKey.ChanID.ToUint64() + fwdInput := ForwardInput{ + OpenedAt: time.Now().UTC(), + RfqID: policy.RfqID(), + ChanIDIn: chanIdIn, + ChanIDOut: htlc.OutgoingChannelID.ToUint64(), + HtlcID: htlc.IncomingCircuitKey.HtlcID, + AssetAmt: assetAmt, + AmtInMsat: uint64(htlc.AmountInMsat), + AmtOutMsat: uint64(htlc.AmountOutMsat), + } + + // Store the circuit key so we can update the record + // later. + h.htlcToForward.Store(htlc.IncomingCircuitKey, + &fwdInput) + + // Log the forwarding event immediately when it opens. + err := h.forwardStore.UpsertForward(ctx, fwdInput) + rfqIDHex := hex.EncodeToString(fwdInput.RfqID[:]) + if err != nil { + log.Errorf("Failed to log forward for "+ + "RFQ %x: %v", rfqIDHex, err) + } else { + log.DebugS(ctx, "Logged forwarding event", + "rfq_id", rfqIDHex, + "asset_amt", fwdInput.AssetAmt, + "amt_in_msat", fwdInput.AmtInMsat, + "amt_out_msat", fwdInput.AmtOutMsat, + ) + } + } + } + log.Debug("HTLC complies with policy. Broadcasting accept event.") h.cfg.AcceptHtlcEvents <- NewAcceptHtlcEvent(htlc, policy) @@ -901,8 +1032,9 @@ func (h *OrderHandler) mainEventLoop() { } // subscribeHtlcs subscribes the OrderHandler to HTLC events provided by the lnd -// RPC interface. We use this subscription to track HTLC forwarding failures, -// which we use to perform a live update of our policies. +// RPC interface. We use this subscription to track HTLC forwarding failures +// and settlements, which we use to perform live updates of our policies and +// to log forward events for accounting. func (h *OrderHandler) subscribeHtlcs(ctx context.Context) error { events, chErr, err := h.cfg.HtlcSubscriber.SubscribeHtlcEvents(ctx) if err != nil { @@ -917,9 +1049,10 @@ func (h *OrderHandler) subscribeHtlcs(ctx context.Context) error { continue } - // Retrieve the two instances that may be relevant. + // Retrieve the event instances that may be relevant. failEvent := event.GetForwardFailEvent() linkFail := event.GetLinkFailEvent() + settleEvent := event.GetSettleEvent() // Craft the circuit key that identifies this HTLC. circuitKey := models.CircuitKey{ @@ -930,21 +1063,18 @@ func (h *OrderHandler) subscribeHtlcs(ctx context.Context) error { } switch { + case settleEvent != nil: + // HTLC settled successfully - update the + // forwarding event record. + h.handleHtlcSettle(ctx, circuitKey) + case failEvent != nil: fallthrough case linkFail != nil: - // Fetch the policy that is related to this - // HTLC. - policy, found := h.htlcToPolicy.LoadAndDelete( - circuitKey, - ) - - if !found { - continue - } - - // Stop tracking this HTLC as it failed. - policy.UntrackHtlc(circuitKey) + // HTLC failed - update the forwarding event + // record + // with the failure timestamp. + h.handleHtlcFail(ctx, circuitKey) } case err := <-chErr: @@ -956,6 +1086,220 @@ func (h *OrderHandler) subscribeHtlcs(ctx context.Context) error { } } +// handleHtlcSettle handles an HTLC settle event by updating the forwarding +// event record +// in the database with the settlement timestamp. +func (h *OrderHandler) handleHtlcSettle(ctx context.Context, + circuitKey models.CircuitKey) { + + // Clean up the policy tracking first. + policy, found := h.htlcToPolicy.LoadAndDelete(circuitKey) + if found { + policy.UntrackHtlc(circuitKey) + } + + // If forwarding event logging is disabled, nothing more to do. + if h.forwardStore == nil { + return + } + + // Get the forward input we stored when we accepted the HTLC. + fwdInput, found := h.htlcToForward.LoadAndDelete(circuitKey) + if !found { + // This HTLC wasn't one we tracked (not an RFQ HTLC). + return + } + + // Update the forwarding event record with the settlement timestamp. + settledAt := time.Now().UTC() + updatedInput := *fwdInput + updatedInput.SettledAt = fn.Some(settledAt) + + err := h.forwardStore.UpsertForward(ctx, updatedInput) + rfqIDHex := hex.EncodeToString(fwdInput.RfqID[:]) + if err != nil { + log.Errorf("Failed to settle forwarding event (rfq_id=%x): %v", + rfqIDHex, err) + return + } + + log.DebugS(ctx, "Settled forwarding event", + "rfq_id", rfqIDHex, + "asset_amt", fwdInput.AssetAmt, + ) +} + +// handleHtlcFail handles an HTLC failure event by updating the forwarding +// event record +// in the database with the failure timestamp. +func (h *OrderHandler) handleHtlcFail(ctx context.Context, + circuitKey models.CircuitKey) { + + // Clean up the policy tracking first. + policy, found := h.htlcToPolicy.LoadAndDelete(circuitKey) + if found { + policy.UntrackHtlc(circuitKey) + } + + // If forwarding event logging is disabled, nothing more to do. + if h.forwardStore == nil { + return + } + + // Get the forward input we stored when we accepted the HTLC. + fwdInput, found := h.htlcToForward.LoadAndDelete(circuitKey) + if !found { + // This HTLC wasn't one we tracked (not an RFQ HTLC). + return + } + + // Update the forwarding event record with the failure timestamp. + failedAt := time.Now().UTC() + updatedInput := *fwdInput + updatedInput.FailedAt = fn.Some(failedAt) + + err := h.forwardStore.UpsertForward(ctx, updatedInput) + rfqIDHex := hex.EncodeToString(fwdInput.RfqID[:]) + if err != nil { + log.Errorf("Failed to mark forwarding event as failed "+ + "(rfq_id=%x): %v", rfqIDHex, err) + return + } + + log.DebugS(ctx, "Failed forwarding event", "rfq_id", rfqIDHex, + "asset_amt", fwdInput.AssetAmt, + ) +} + +// restorePendingForwards reconciles pending forwarding events and repopulates +// the +// in-memory forward caches. +func (h *OrderHandler) restorePendingForwards(ctx context.Context) error { + pendingForwards, err := h.forwardStore.PendingForwards(ctx) + if err != nil { + return fmt.Errorf("error fetching pending forwarding events: "+ + "%w", err) + } + + if len(pendingForwards) == 0 { + return nil + } + + log.Debugf("Reconciling %d pending forwarding events", + len(pendingForwards)) + + for _, forward := range pendingForwards { + circuitKey := models.CircuitKey{ + ChanID: lnwire.NewShortChanIDFromInt( + forward.ChanIDIn, + ), + HtlcID: forward.HtlcID, + } + + res, err := h.cfg.HtlcSubscriber.LookupHtlcResolution( + ctx, forward.ChanIDIn, forward.HtlcID, + ) + if err != nil { + switch status.Code(err) { + case codes.NotFound: + h.restorePendingForward(circuitKey, forward) + default: + log.Warnf("Unable to lookup HTLC resolution "+ + "chan_id=%d htlc_id=%d: %v", + forward.ChanIDIn, forward.HtlcID, err) + h.restorePendingForward(circuitKey, forward) + } + + continue + } + + updated := forward + resolvedAt := time.Now().UTC() + if res.Settled { + updated.SettledAt = fn.Some(resolvedAt) + } else { + updated.FailedAt = fn.Some(resolvedAt) + } + + err = h.forwardStore.UpsertForward(ctx, updated) + rfqIDHex := hex.EncodeToString(forward.RfqID[:]) + if err != nil { + log.Errorf("Failed to reconcile forwarding event "+ + "(rfq_id=%x): %v", rfqIDHex, err) + continue + } + + log.DebugS(ctx, "Reconciled forwarding event", + "rfq_id", rfqIDHex, + "settled", res.Settled, + ) + } + + return nil +} + +// restorePendingForward repopulates caches for a pending forward. +func (h *OrderHandler) restorePendingForward( + circuitKey models.CircuitKey, forward ForwardInput) { + + forwardCopy := forward + h.htlcToForward.Store(circuitKey, &forwardCopy) + + policy, ok := h.policyForForward(forward) + if !ok { + return + } + + policy.TrackAcceptedHtlc( + circuitKey, lnwire.MilliSatoshi(forward.AmtOutMsat), + ) + h.htlcToPolicy.Store(circuitKey, policy) +} + +// policyForForward attempts to reconstruct the policy for a pending forward. +func (h *OrderHandler) policyForForward( + forward ForwardInput) (Policy, bool) { + + inScid := SerialisedScid(forward.ChanIDIn) + outScid := SerialisedScid(forward.ChanIDOut) + + inPolicy, haveIn := h.policies.Load(inScid) + outPolicy, haveOut := h.policies.Load(outScid) + + if haveIn && haveOut { + forwardPolicy, err := NewAssetForwardPolicy( + inPolicy, outPolicy, + ) + if err == nil { + return forwardPolicy, true + } + + log.Warnf("Unable to restore forward policy "+ + "(scid_in=%d, scid_out=%d): %v", + forward.ChanIDIn, forward.ChanIDOut, err) + } + + if haveIn { + return inPolicy, true + } + + if haveOut { + return outPolicy, true + } + + var matched Policy + h.policies.Range(func(_ SerialisedScid, policy Policy) bool { + if policy.RfqID() == forward.RfqID { + matched = policy + return false + } + + return true + }) + + return matched, matched != nil +} + // Start starts the service. func (h *OrderHandler) Start(ctx context.Context) error { var startErr error @@ -968,6 +1312,11 @@ func (h *OrderHandler) Start(ctx context.Context) error { return } + if err := h.restorePendingForwards(ctx); err != nil { + log.Errorf("restoring pending forwarding events: "+ + "%v", err) + } + // Start the HTLC interceptor in a separate go routine. h.Wg.Add(1) go func() { @@ -1328,4 +1677,8 @@ type HtlcSubscriber interface { // HTLC updates. SubscribeHtlcEvents(ctx context.Context) (<-chan *routerrpc.HtlcEvent, <-chan error, error) + + // LookupHtlcResolution retrieves the final resolution for an HTLC. + LookupHtlcResolution(ctx context.Context, chanID uint64, + htlcID uint64) (*lnrpc.LookupHtlcResolutionResponse, error) } diff --git a/rpcserver.go b/rpcserver.go index eaf4799c4a..eaf9a9734a 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -8707,6 +8707,136 @@ func (r *rpcServer) SubscribeRfqEventNtfns( ) } +// ForwardingHistory queries the historical records of asset forwarding events. +func (r *rpcServer) ForwardingHistory(ctx context.Context, + req *rfqrpc.ForwardingHistoryRequest) ( + *rfqrpc.ForwardingHistoryResponse, error) { + + // Build the query parameters. + params := rfq.QueryForwardsParams{ + Limit: req.Limit, + Offset: req.Offset, + } + + // Parse timestamp filters if provided. + if req.MinTimestamp > 0 { + params.MinTimestamp = fn.Some(time.Unix( + int64(req.MinTimestamp), 0)) + } + if req.MaxTimestamp > 0 { + params.MaxTimestamp = fn.Some(time.Unix( + int64(req.MaxTimestamp), 0)) + } + + // Parse peer filter if provided. + if len(req.Peer) > 0 { + peer, err := route.NewVertexFromBytes(req.Peer) + if err != nil { + return nil, fmt.Errorf("invalid peer public key: %w", + err) + } + params.Peer = &peer + } + + // Parse asset specifier if provided. + if req.AssetSpecifier != nil { + assetID, groupKey, err := unmarshalAssetSpecifier( + req.AssetSpecifier, + ) + if err != nil { + return nil, fmt.Errorf("error parsing asset "+ + "specifier: %w", err) + } + + specifier, err := asset.NewSpecifier( + assetID, groupKey, nil, true, + ) + if err != nil { + return nil, fmt.Errorf("error building asset "+ + "specifier: %w", err) + } + params.AssetSpecifier = &specifier + } + + // Set default limit if not provided. + if params.Limit == 0 { + params.Limit = 100 + } + + // Query the forwarding events and total count. + forwards, totalCount, err := r.cfg.RfqManager.QueryForwardsWithCount( + ctx, params, + ) + if err != nil { + return nil, fmt.Errorf("error querying forwarding events: "+ + "%w", err) + } + + // Convert the forwarding events to RPC format. + rpcForwards := make([]*rfqrpc.ForwardingEvent, len(forwards)) + for i, fwd := range forwards { + rpcForwards[i] = marshalForwardingEvent(fwd) + } + + return &rfqrpc.ForwardingHistoryResponse{ + Forwards: rpcForwards, + TotalCount: totalCount, + }, nil +} + +// marshalForwardingEvent converts a database forwarding event record to its RPC +// representation. +func marshalForwardingEvent(fwd rfq.ForwardingEvent) *rfqrpc.ForwardingEvent { + rpcForward := &rfqrpc.ForwardingEvent{ + OpenedAt: uint64(fwd.OpenedAt.Unix()), + RfqId: fwd.RfqID[:], + ChanIdIn: fwd.ChanIDIn, + ChanIdOut: fwd.ChanIDOut, + HtlcId: fwd.HtlcID, + AssetAmt: fwd.AssetAmt, + AmtInMsat: fwd.AmtInMsat, + AmtOutMsat: fwd.AmtOutMsat, + Peer: fwd.Peer.String(), + Rate: &rfqrpc.FixedPoint{ + Coefficient: fwd.Rate.Coefficient.String(), + Scale: uint32(fwd.Rate.Scale), + }, + } + + // Set timestamps if available. + if fwd.SettledAt != nil { + rpcForward.SettledAt = uint64(fwd.SettledAt.Unix()) + } + if fwd.FailedAt != nil { + rpcForward.FailedAt = uint64(fwd.FailedAt.Unix()) + } + + // Convert policy type. + if fwd.PolicyType == rfq.RfqPolicyTypeAssetSale { + rpcForward.PolicyType = + rfqrpc.RfqPolicyType_RFQ_POLICY_TYPE_SALE + } else { + rpcForward.PolicyType = + rfqrpc.RfqPolicyType_RFQ_POLICY_TYPE_PURCHASE + } + + // Add asset specifier if available. + if fwd.AssetSpecifier.IsSome() { + rpcForward.AssetSpec = &rfqrpc.AssetSpec{} + assetID := fwd.AssetSpecifier.UnwrapIdToPtr() + if assetID != nil { + rpcForward.AssetSpec.Id = assetID[:] + } + groupKey := fwd.AssetSpecifier.UnwrapGroupKeyToPtr() + if groupKey != nil { + rpcForward.AssetSpec.GroupPubKey = + schnorr.SerializePubKey(groupKey) + } + } + + return rpcForward +} + // FundChannel initiates the channel funding negotiation with a peer for the // creation of a channel that contains a specified amount of a given asset. func (r *rpcServer) FundChannel(ctx context.Context, diff --git a/tapcfg/server.go b/tapcfg/server.go index c50766f341..e9c3883aaf 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -134,6 +134,13 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, ) policyStore := tapdb.NewPersistedPolicyStore(rfqPolicyDB) + rfqForwardDB := tapdb.NewTransactionExecutor( + db, func(tx *sql.Tx) tapdb.ForwardStore { + return db.WithTx(tx) + }, + ) + forwardStore := tapdb.NewPersistedForwardStore(rfqForwardDB) + // Create a block header cache with default configuration. headerCache, err := lndservices.NewBlockHeaderCache( lndservices.DefaultBlockHeaderCacheConfig(), @@ -534,6 +541,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, SendPeerId: rfqCfg.PriceOracleSendPeerId, NoOpHTLCs: cfg.Channel.NoopHTLCs, PolicyStore: policyStore, + ForwardStore: forwardStore, ErrChan: mainErrChan, }) if err != nil { diff --git a/tapdb/forwards.go b/tapdb/forwards.go new file mode 100644 index 0000000000..843fb0ea17 --- /dev/null +++ b/tapdb/forwards.go @@ -0,0 +1,341 @@ +package tapdb + +import ( + "context" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/tapdb/sqlc" + "github.com/lightningnetwork/lnd/routing/route" +) + +// ForwardStore is the database interface for forwarding event records. +type ForwardStore interface { + // UpsertForward inserts or updates a forwarding event record. + UpsertForward(ctx context.Context, + arg sqlc.UpsertForwardParams) (int64, error) + + // QueryPendingForwards fetches events without a terminal state. + QueryPendingForwards(ctx context.Context) ( + []sqlc.QueryPendingForwardsRow, error) + + // QueryForwards queries forwarding event records with optional filters. + QueryForwards(ctx context.Context, + arg sqlc.QueryForwardsParams) ([]sqlc.QueryForwardsRow, error) + + // CountForwards counts forwarding event records matching the filters. + CountForwards(ctx context.Context, + arg sqlc.CountForwardsParams) (int64, error) +} + +// BatchedForwardingEventStore supports batched database operations. +type BatchedForwardingEventStore interface { + ForwardStore + BatchedTx[ForwardStore] +} + +// PersistedForwardStore provides methods to persist and query forwarding +// events. +type PersistedForwardStore struct { + db BatchedForwardingEventStore +} + +// NewPersistedForwardStore creates a new forward persistence helper. +func NewPersistedForwardStore( + db BatchedForwardingEventStore) *PersistedForwardStore { + + return &PersistedForwardStore{ + db: db, + } +} + +// UpsertForward inserts or updates a forwarding event record. +func (s *PersistedForwardStore) UpsertForward(ctx context.Context, + input rfq.ForwardInput) error { + + writeOpts := WriteTxOption() + + return s.db.ExecTx(ctx, writeOpts, func(q ForwardStore) error { + _, err := q.UpsertForward(ctx, sqlc.UpsertForwardParams{ + OpenedAt: input.OpenedAt.UTC(), + SettledAt: sqlOptTime(input.SettledAt), + FailedAt: sqlOptTime(input.FailedAt), + RfqID: input.RfqID[:], + ChanIDIn: int64(input.ChanIDIn), + ChanIDOut: int64(input.ChanIDOut), + HtlcID: int64(input.HtlcID), + AssetAmt: int64(input.AssetAmt), + AmtInMsat: int64(input.AmtInMsat), + AmtOutMsat: int64(input.AmtOutMsat), + }) + if err != nil { + return fmt.Errorf("error upserting forwarding event: "+ + "%w", err) + } + + return nil + }) +} + +// PendingForwards retrieves events without a settled or failed timestamp. +func (s *PersistedForwardStore) PendingForwards( + ctx context.Context, +) ([]rfq.ForwardInput, error) { + + readOpts := ReadTxOption() + var forwards []rfq.ForwardInput + + err := s.db.ExecTx(ctx, readOpts, func(q ForwardStore) error { + rows, err := q.QueryPendingForwards(ctx) + if err != nil { + return fmt.Errorf("querying pending forwarding "+ + "events: %w", err) + } + + forwards = make([]rfq.ForwardInput, 0, len(rows)) + for _, row := range rows { + input, err := forwardInputFromPendingRow(row) + if err != nil { + return fmt.Errorf( + "converting pending forward: %w", err) + } + + forwards = append(forwards, input) + } + + return nil + }) + if err != nil { + return nil, err + } + + return forwards, nil +} + +type forwardQueryFilters struct { + openedAfter time.Time + openedBefore time.Time + peer []byte + assetID []byte + assetGroupKey []byte +} + +func buildForwardQueryFilters( + params rfq.QueryForwardsParams) forwardQueryFilters { + + // Set default time bounds if not specified. + openedAfter := params.MinTimestamp.UnwrapOr(time.Unix(0, 0).UTC()) + openedBefore := params.MaxTimestamp.UnwrapOr(MaxValidSQLTime) + + filters := forwardQueryFilters{ + openedAfter: openedAfter.UTC(), + openedBefore: openedBefore.UTC(), + } + + if params.Peer != nil { + filters.peer = params.Peer[:] + } + + if params.AssetSpecifier != nil { + params.AssetSpecifier.WhenId(func(id asset.ID) { + filters.assetID = id[:] + }) + + params.AssetSpecifier.WhenGroupPubKey( + func(key btcec.PublicKey) { + filters.assetGroupKey = + key.SerializeCompressed() + }, + ) + } + + return filters +} + +func queryForwardRecords(ctx context.Context, q ForwardStore, + queryParams sqlc.QueryForwardsParams) ([]rfq.ForwardingEvent, error) { + + rows, err := q.QueryForwards(ctx, queryParams) + if err != nil { + return nil, fmt.Errorf("error querying forwarding events: %w", + err) + } + + records := make([]rfq.ForwardingEvent, 0, len(rows)) + for _, row := range rows { + record, err := forwardRecordFromRow(row) + if err != nil { + return nil, fmt.Errorf("error converting row: %w", + err) + } + + records = append(records, record) + } + + return records, nil +} + +func countForwardRecords(ctx context.Context, q ForwardStore, + countParams sqlc.CountForwardsParams) (int64, error) { + + count, err := q.CountForwards(ctx, countParams) + if err != nil { + return 0, fmt.Errorf("error counting forwarding events: %w", + err) + } + + return count, nil +} + +// QueryForwardsWithCount retrieves forwarding event records matching the given +// filters along with the total count in a single transaction. +func (s *PersistedForwardStore) QueryForwardsWithCount(ctx context.Context, + params rfq.QueryForwardsParams) ([]rfq.ForwardingEvent, int64, error) { + + filters := buildForwardQueryFilters(params) + queryParams := sqlc.QueryForwardsParams{ + OpenedAfter: filters.openedAfter, + OpenedBefore: filters.openedBefore, + NumLimit: params.Limit, + NumOffset: params.Offset, + Peer: filters.peer, + AssetID: filters.assetID, + AssetGroupKey: filters.assetGroupKey, + } + countParams := sqlc.CountForwardsParams{ + OpenedAfter: filters.openedAfter, + OpenedBefore: filters.openedBefore, + Peer: filters.peer, + AssetID: filters.assetID, + AssetGroupKey: filters.assetGroupKey, + } + + readOpts := ReadTxOption() + var ( + records []rfq.ForwardingEvent + count int64 + ) + + err := s.db.ExecTx(ctx, readOpts, func(q ForwardStore) error { + var err error + records, err = queryForwardRecords(ctx, q, queryParams) + if err != nil { + return err + } + + count, err = countForwardRecords(ctx, q, countParams) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, 0, err + } + + return records, count, nil +} + +// forwardInputFromPendingRow converts a pending forward row to a forward input. +func forwardInputFromPendingRow( + row sqlc.QueryPendingForwardsRow, +) (rfq.ForwardInput, error) { + + var rfqID rfqmsg.ID + if len(row.RfqID) != len(rfqID) { + return rfq.ForwardInput{}, + fmt.Errorf("invalid RFQ ID length: %d", len(row.RfqID)) + } + copy(rfqID[:], row.RfqID) + + return rfq.ForwardInput{ + OpenedAt: row.OpenedAt.UTC(), + SettledAt: fn.None[time.Time](), + FailedAt: fn.None[time.Time](), + RfqID: rfqID, + ChanIDIn: uint64(row.ChanIDIn), + ChanIDOut: uint64(row.ChanIDOut), + HtlcID: uint64(row.HtlcID), + AssetAmt: uint64(row.AssetAmt), + AmtInMsat: uint64(row.AmtInMsat), + AmtOutMsat: uint64(row.AmtOutMsat), + }, nil +} + +// forwardRecordFromRow converts a database row to a ForwardingEvent. +func forwardRecordFromRow(row sqlc.QueryForwardsRow) (rfq.ForwardingEvent, + error) { + + var rfqID rfqmsg.ID + copy(rfqID[:], row.RfqID) + + var peer route.Vertex + copy(peer[:], row.Peer) + + var assetID *asset.ID + if len(row.AssetID) > 0 { + id := new(asset.ID) + copy(id[:], row.AssetID) + assetID = id + } + + var groupKey *btcec.PublicKey + if len(row.AssetGroupKey) > 0 { + var err error + groupKey, err = btcec.ParsePubKey(row.AssetGroupKey) + if err != nil { + return rfq.ForwardingEvent{}, fmt.Errorf("error "+ + "parsing group key: %w", err) + } + } + + assetSpecifier, err := asset.NewSpecifier( + assetID, groupKey, nil, false, + ) + if err != nil { + return rfq.ForwardingEvent{}, fmt.Errorf("error "+ + "building asset specifier: %w", err) + } + + rate := rfqmath.BigIntFixedPoint{ + Coefficient: rfqmath.BigInt{}.FromBytes(row.RateCoefficient), + Scale: uint8(row.RateScale), + } + + // Convert nullable timestamps to pointers. + var settledAt *time.Time + if row.SettledAt.Valid { + t := row.SettledAt.Time.UTC() + settledAt = &t + } + + var failedAt *time.Time + if row.FailedAt.Valid { + t := row.FailedAt.Time.UTC() + failedAt = &t + } + + return rfq.ForwardingEvent{ + OpenedAt: row.OpenedAt.UTC(), + SettledAt: settledAt, + FailedAt: failedAt, + RfqID: rfqID, + ChanIDIn: uint64(row.ChanIDIn), + ChanIDOut: uint64(row.ChanIDOut), + HtlcID: uint64(row.HtlcID), + AssetAmt: uint64(row.AssetAmt), + AmtInMsat: uint64(row.AmtInMsat), + AmtOutMsat: uint64(row.AmtOutMsat), + PolicyType: rfq.RfqPolicyType(row.PolicyType), + Peer: peer, + AssetSpecifier: assetSpecifier, + Rate: rate, + }, nil +} diff --git a/tapdb/forwards_test.go b/tapdb/forwards_test.go new file mode 100644 index 0000000000..ea813620c9 --- /dev/null +++ b/tapdb/forwards_test.go @@ -0,0 +1,719 @@ +package tapdb + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/tapdb/sqlc" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// newForwardStore creates a new PersistedForwardStore for testing along with +// the underlying database handles needed to set up test data. +func newForwardStore(t *testing.T) (*PersistedForwardStore, + *PersistedPolicyStore, sqlc.Querier) { + + db := NewTestDB(t) + + forwardTxCreator := func(tx *sql.Tx) ForwardStore { + return db.WithTx(tx) + } + policyTxCreator := func(tx *sql.Tx) RfqPolicyStore { + return db.WithTx(tx) + } + + forwardDB := NewTransactionExecutor(db, forwardTxCreator) + policyDB := NewTransactionExecutor(db, policyTxCreator) + + return NewPersistedForwardStore(forwardDB), + NewPersistedPolicyStore(policyDB), + db +} + +// randRfqID generates a random RFQ ID for testing. +func randRfqID(t *testing.T) rfqmsg.ID { + var id rfqmsg.ID + copy(id[:], test.RandBytes(32)) + return id +} + +// randPeer generates a random peer vertex for testing. +func randPeer(t *testing.T) route.Vertex { + var peer route.Vertex + copy(peer[:], test.RandBytes(33)) + return peer +} + +// insertTestPolicy inserts an RFQ policy into the database for testing. +// This is required because forwards has a foreign key to rfq_policies. +func insertTestPolicy(t *testing.T, ctx context.Context, db sqlc.Querier, + rfqID rfqmsg.ID, policyType rfq.RfqPolicyType, peer route.Vertex, + assetID *asset.ID, groupKey *btcec.PublicKey) { + + var assetIDBytes []byte + if assetID != nil { + assetIDBytes = assetID[:] + } + + var groupKeyBytes []byte + if groupKey != nil { + groupKeyBytes = groupKey.SerializeCompressed() + } + + // Create a simple rate coefficient (just bytes for testing). + rateCoeffBytes := []byte{0x01, 0x23, 0x45} + + _, err := db.InsertRfqPolicy(ctx, sqlc.InsertRfqPolicyParams{ + PolicyType: string(policyType), + Scid: 12345, + RfqID: rfqID[:], + Peer: peer[:], + AssetID: assetIDBytes, + AssetGroupKey: groupKeyBytes, + RateCoefficient: rateCoeffBytes, + RateScale: 6, + Expiry: time.Now().Add(time.Hour).Unix(), + AgreedAt: time.Now().Unix(), + }) + require.NoError(t, err) +} + +// TestUpsertForward tests the UpsertForward method with various scenarios. +func TestUpsertForward(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + + setupFn func(t *testing.T, ctx context.Context, + db sqlc.Querier) ([]rfq.ForwardInput, any) + + verifyFn func(t *testing.T, ctx context.Context, + store *PersistedForwardStore, inputs []rfq.ForwardInput, + setupData any) + }{{ + name: "basic insert and retrieve", + setupFn: func(t *testing.T, ctx context.Context, + db sqlc.Querier) ([]rfq.ForwardInput, any) { + + rfqID := randRfqID(t) + peer := randPeer(t) + assetID := asset.RandID(t) + + insertTestPolicy( + t, ctx, db, rfqID, rfq.RfqPolicyTypeAssetSale, + peer, &assetID, nil, + ) + + openedAt := time.Now().UTC().Truncate(time.Second) + input := rfq.ForwardInput{ + OpenedAt: openedAt, + RfqID: rfqID, + ChanIDIn: 100, + ChanIDOut: 200, + HtlcID: 1, + AssetAmt: 500, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + return []rfq.ForwardInput{input}, map[string]any{ + "rfqID": rfqID, + "peer": peer, + "assetID": assetID, + "openedAt": openedAt, + } + }, + verifyFn: func(t *testing.T, ctx context.Context, + store *PersistedForwardStore, inputs []rfq.ForwardInput, + setupData any) { + + data := setupData.(map[string]any) + rfqID := data["rfqID"].(rfqmsg.ID) + peer := data["peer"].(route.Vertex) + assetID := data["assetID"].(asset.ID) + openedAt := data["openedAt"].(time.Time) + + records, _, err := store.QueryForwardsWithCount( + ctx, rfq.QueryForwardsParams{Limit: 10}, + ) + require.NoError(t, err) + require.Len(t, records, 1) + + record := records[0] + require.Equal(t, openedAt.Truncate(time.Microsecond), + record.OpenedAt) + require.Nil(t, record.SettledAt) + require.Nil(t, record.FailedAt) + require.Equal(t, rfqID, record.RfqID) + require.Equal(t, uint64(100), record.ChanIDIn) + require.Equal(t, uint64(200), record.ChanIDOut) + require.Equal(t, uint64(1), record.HtlcID) + require.Equal(t, uint64(500), record.AssetAmt) + require.Equal(t, uint64(42000), record.AmtInMsat) + require.Equal(t, uint64(41000), record.AmtOutMsat) + + // Verify joined policy data. + require.Equal( + t, rfq.RfqPolicyTypeAssetSale, + record.PolicyType, + ) + require.Equal(t, peer, record.Peer) + assetIDPtr := record.AssetSpecifier.UnwrapIdToPtr() + require.NotNil(t, assetIDPtr) + require.Equal(t, assetID, *assetIDPtr) + require.False(t, record.AssetSpecifier.HasGroupPubKey()) + }, + }, { + name: "duplicate insert updates", + setupFn: func(t *testing.T, ctx context.Context, + db sqlc.Querier) ([]rfq.ForwardInput, any) { + + rfqID := randRfqID(t) + peer := randPeer(t) + + insertTestPolicy( + t, ctx, db, rfqID, rfq.RfqPolicyTypeAssetSale, + peer, nil, nil, + ) + + input1 := rfq.ForwardInput{ + OpenedAt: time.Now().UTC(), + RfqID: rfqID, + ChanIDIn: 100, + ChanIDOut: 200, + HtlcID: 1, + AssetAmt: 500, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + input2 := rfq.ForwardInput{ + OpenedAt: time.Now().UTC(), + RfqID: rfqID, + ChanIDIn: 100, + ChanIDOut: 300, + HtlcID: 1, + AssetAmt: 1000, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + return []rfq.ForwardInput{input1, input2}, nil + }, + verifyFn: func(t *testing.T, ctx context.Context, + store *PersistedForwardStore, inputs []rfq.ForwardInput, + setupData any) { + + records, _, err := store.QueryForwardsWithCount( + ctx, rfq.QueryForwardsParams{Limit: 10}, + ) + require.NoError(t, err) + require.Len(t, records, 1) + + record := records[0] + require.Equal(t, inputs[1].OpenedAt.Unix(), + record.OpenedAt.Unix(), + ) + require.Equal(t, inputs[1].ChanIDOut, record.ChanIDOut) + require.Equal(t, inputs[1].AssetAmt, record.AssetAmt) + require.Equal(t, inputs[1].AmtInMsat, record.AmtInMsat) + require.Equal(t, inputs[1].AmtOutMsat, + record.AmtOutMsat) + }, + }, { + name: "forward with group key", + setupFn: func(t *testing.T, ctx context.Context, + db sqlc.Querier) ([]rfq.ForwardInput, any) { + + rfqID := randRfqID(t) + peer := randPeer(t) + groupKey := test.RandPubKey(t) + + insertTestPolicy( + t, ctx, db, rfqID, + rfq.RfqPolicyTypeAssetPurchase, peer, nil, + groupKey, + ) + + input := rfq.ForwardInput{ + OpenedAt: time.Now(), + RfqID: rfqID, + ChanIDIn: 100, + ChanIDOut: 200, + HtlcID: 1, + AssetAmt: 500, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + return []rfq.ForwardInput{input}, groupKey + }, + verifyFn: func(t *testing.T, ctx context.Context, + store *PersistedForwardStore, inputs []rfq.ForwardInput, + setupData any) { + + groupKey := setupData.(*btcec.PublicKey) + + records, _, err := store.QueryForwardsWithCount( + ctx, rfq.QueryForwardsParams{Limit: 10}, + ) + require.NoError(t, err) + require.Len(t, records, 1) + + groupKeyPtr := records[0].AssetSpecifier. + UnwrapGroupKeyToPtr() + require.NotNil(t, groupKeyPtr) + require.True(t, groupKey.IsEqual(groupKeyPtr)) + require.False(t, records[0].AssetSpecifier.HasId()) + + // Query filtered by group key. + groupSpec := asset.NewSpecifierFromGroupKey(*groupKey) + records, _, err = store.QueryForwardsWithCount( + ctx, rfq.QueryForwardsParams{ + AssetSpecifier: &groupSpec, + Limit: 10, + }, + ) + require.NoError(t, err) + require.Len(t, records, 1) + }, + }, { + name: "multiple forwards same RFQ ID", + setupFn: func(t *testing.T, ctx context.Context, + db sqlc.Querier) ([]rfq.ForwardInput, any) { + + rfqID := randRfqID(t) + peer := randPeer(t) + + insertTestPolicy( + t, ctx, db, rfqID, rfq.RfqPolicyTypeAssetSale, + peer, nil, nil, + ) + + // Multiple forwards with same RFQ ID but different + // HTLCs. + var inputs []rfq.ForwardInput + for i := 0; i < 5; i++ { + inputs = append(inputs, rfq.ForwardInput{ + OpenedAt: time.Now(), + RfqID: rfqID, + ChanIDIn: uint64(100 + i), + ChanIDOut: 200, + HtlcID: uint64(i), + AssetAmt: 100, + AmtInMsat: 42000, + AmtOutMsat: 41000, + }) + } + + return inputs, rfqID + }, + verifyFn: func(t *testing.T, ctx context.Context, + store *PersistedForwardStore, inputs []rfq.ForwardInput, + setupData any) { + + rfqID := setupData.(rfqmsg.ID) + + records, _, err := store.QueryForwardsWithCount( + ctx, rfq.QueryForwardsParams{Limit: 100}, + ) + require.NoError(t, err) + require.Len(t, records, 5) + + for _, r := range records { + require.Equal(t, rfqID, r.RfqID) + } + }, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + forwardStore, _, db := newForwardStore(t) + + inputs, setupData := tc.setupFn(t, ctx, db) + + for _, input := range inputs { + err := forwardStore.UpsertForward(ctx, input) + require.NoError(t, err) + } + + if tc.verifyFn != nil { + tc.verifyFn(t, ctx, forwardStore, inputs, + setupData, + ) + } + }) + } +} + +// TestPendingForwards verifies pending forwards are returned. +func TestPendingForwards(t *testing.T) { + t.Parallel() + + ctx := context.Background() + forwardStore, _, db := newForwardStore(t) + + rfqID := randRfqID(t) + peer := randPeer(t) + insertTestPolicy( + t, ctx, db, rfqID, rfq.RfqPolicyTypeAssetSale, + peer, nil, nil, + ) + + openedAt := time.Now().UTC().Truncate(time.Second) + pending := rfq.ForwardInput{ + OpenedAt: openedAt, + RfqID: rfqID, + ChanIDIn: 100, + ChanIDOut: 200, + HtlcID: 1, + AssetAmt: 500, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + settledAt := openedAt.Add(10 * time.Second) + settled := rfq.ForwardInput{ + OpenedAt: openedAt.Add(1 * time.Second), + SettledAt: fn.Some(settledAt), + RfqID: rfqID, + ChanIDIn: 101, + ChanIDOut: 201, + HtlcID: 2, + AssetAmt: 600, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + failedAt := openedAt.Add(20 * time.Second) + failed := rfq.ForwardInput{ + OpenedAt: openedAt.Add(2 * time.Second), + FailedAt: fn.Some(failedAt), + RfqID: rfqID, + ChanIDIn: 102, + ChanIDOut: 202, + HtlcID: 3, + AssetAmt: 700, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + + for _, input := range []rfq.ForwardInput{pending, settled, failed} { + require.NoError(t, forwardStore.UpsertForward(ctx, input)) + } + + forwards, err := forwardStore.PendingForwards(ctx) + require.NoError(t, err) + require.Len(t, forwards, 1) + + forward := forwards[0] + require.Equal(t, pending.OpenedAt.Truncate(time.Microsecond), + forward.OpenedAt) + require.Equal(t, pending.RfqID, forward.RfqID) + require.Equal(t, pending.ChanIDIn, forward.ChanIDIn) + require.Equal(t, pending.ChanIDOut, forward.ChanIDOut) + require.Equal(t, pending.HtlcID, forward.HtlcID) + require.Equal(t, pending.AssetAmt, forward.AssetAmt) + require.Equal(t, pending.AmtInMsat, forward.AmtInMsat) + require.Equal(t, pending.AmtOutMsat, forward.AmtOutMsat) + require.True(t, forward.SettledAt.IsNone()) + require.True(t, forward.FailedAt.IsNone()) +} + +type forwardTestSetup struct { + rfqID1 rfqmsg.ID + rfqID2 rfqmsg.ID + peer1 route.Vertex + peer2 route.Vertex + assetID1 asset.ID + assetID2 asset.ID + groupKey *btcec.PublicKey + times []time.Time +} + +// setupQueryTestData sets up test data for query tests and returns the setup. +func setupQueryTestData(t *testing.T, ctx context.Context, + store *PersistedForwardStore, + db sqlc.Querier) *forwardTestSetup { + + setup := &forwardTestSetup{ + rfqID1: randRfqID(t), + rfqID2: randRfqID(t), + peer1: randPeer(t), + peer2: randPeer(t), + assetID1: asset.RandID(t), + assetID2: asset.RandID(t), + groupKey: test.RandPubKey(t), + } + + now := time.Now().UTC() + setup.times = []time.Time{ + now.Add(-3 * time.Hour), + now.Add(-2 * time.Hour), + now.Add(-1 * time.Hour), + now, + } + + // Insert policies. + insertTestPolicy( + t, ctx, db, setup.rfqID1, rfq.RfqPolicyTypeAssetSale, + setup.peer1, &setup.assetID1, nil, + ) + insertTestPolicy( + t, ctx, db, setup.rfqID2, rfq.RfqPolicyTypeAssetPurchase, + setup.peer2, &setup.assetID2, nil, + ) + + // Insert forwards at different times for rfqID1. + for i, openedAt := range setup.times { + input := rfq.ForwardInput{ + OpenedAt: openedAt, + RfqID: setup.rfqID1, + ChanIDIn: uint64(100 + i), + ChanIDOut: 200, + HtlcID: uint64(i), + AssetAmt: uint64(500 * (i + 1)), + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + require.NoError(t, store.UpsertForward(ctx, input)) + } + + // Insert forwards for rfqID2 (different peer and asset). + for i := 0; i < 2; i++ { + input := rfq.ForwardInput{ + OpenedAt: time.Now(), + RfqID: setup.rfqID2, + ChanIDIn: uint64(200 + i), + ChanIDOut: 300, + HtlcID: uint64(i), + AssetAmt: 1000, + AmtInMsat: 42000, + AmtOutMsat: 41000, + } + require.NoError(t, store.UpsertForward(ctx, input)) + } + + return setup +} + +// TestQueryForwardsWithCountFilters tests QueryForwardsWithCount filters. +func TestQueryForwardsWithCountFilters(t *testing.T) { + t.Parallel() + + ctx := context.Background() + forwardStore, _, db := newForwardStore(t) + setup := setupQueryTestData(t, ctx, forwardStore, db) + now := setup.times[3] + assetID1Spec := asset.NewSpecifierFromId(setup.assetID1) + assetID2Spec := asset.NewSpecifierFromId(setup.assetID2) + + testCases := []struct { + name string + params rfq.QueryForwardsParams + numResult int + }{{ + name: "no filters", + params: rfq.QueryForwardsParams{Limit: 100}, + numResult: 6, + }, { + name: "min timestamp filter", + params: rfq.QueryForwardsParams{ + MinTimestamp: fn.Some(now.Add(-2 * time.Hour)), + Limit: 100, + }, + // -2h, -1h, now from rfqID1, plus 2 from rfqID2 + numResult: 5, + }, { + name: "max timestamp filter", + params: rfq.QueryForwardsParams{ + MaxTimestamp: fn.Some(now.Add(-1 * time.Hour)), + Limit: 100, + }, + // -3h, -2h, -1h from rfqID1 + numResult: 3, + }, { + name: "min and max timestamp filter", + params: rfq.QueryForwardsParams{ + MinTimestamp: fn.Some(now.Add(-2 * time.Hour)), + MaxTimestamp: fn.Some(now.Add(-1 * time.Hour)), + Limit: 100, + }, + // -2h, -1h from rfqID1 + numResult: 2, + }, { + name: "peer filter - peer1", + params: rfq.QueryForwardsParams{ + Peer: &setup.peer1, + Limit: 100, + }, + numResult: 4, + }, { + name: "peer filter - peer2", + params: rfq.QueryForwardsParams{ + Peer: &setup.peer2, + Limit: 100, + }, + numResult: 2, + }, { + name: "asset filter - assetID1", + params: rfq.QueryForwardsParams{ + AssetSpecifier: &assetID1Spec, + Limit: 100, + }, + numResult: 4, + }, { + name: "asset filter - assetID2", + params: rfq.QueryForwardsParams{ + AssetSpecifier: &assetID2Spec, + Limit: 100, + }, + numResult: 2, + }, { + name: "pagination - first page", + params: rfq.QueryForwardsParams{ + Limit: 3, + Offset: 0, + }, + numResult: 3, + }, { + name: "pagination - second page", + params: rfq.QueryForwardsParams{ + Limit: 3, + Offset: 3, + }, + numResult: 3, + }, { + name: "pagination - third page (empty)", + params: rfq.QueryForwardsParams{ + Limit: 3, + Offset: 6, + }, + numResult: 0, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + records, _, err := forwardStore.QueryForwardsWithCount( + ctx, tc.params, + ) + require.NoError(t, err) + require.Len(t, records, tc.numResult) + + // Verify peer filter results have correct peer. + if tc.params.Peer != nil { + for _, r := range records { + require.Equal( + t, *tc.params.Peer, r.Peer, + ) + } + } + }) + } +} + +// TestQueryForwardsWithCountCounts tests QueryForwardsWithCount counts. +func TestQueryForwardsWithCountCounts(t *testing.T) { + t.Parallel() + + ctx := context.Background() + forwardStore, _, db := newForwardStore(t) + setup := setupQueryTestData(t, ctx, forwardStore, db) + assetID1Spec := asset.NewSpecifierFromId(setup.assetID1) + + testCases := []struct { + name string + params rfq.QueryForwardsParams + expCount int64 + }{{ + name: "count all", + params: rfq.QueryForwardsParams{}, + expCount: 6, + }, { + name: "count filtered by asset", + params: rfq.QueryForwardsParams{ + AssetSpecifier: &assetID1Spec, + }, + expCount: 4, + }, { + name: "count filtered by peer", + params: rfq.QueryForwardsParams{ + Peer: &setup.peer2, + }, + expCount: 2, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, count, err := forwardStore.QueryForwardsWithCount( + ctx, tc.params, + ) + require.NoError(t, err) + require.Equal(t, tc.expCount, count) + }) + } +} + +// TestQueryForwardsWithCount tests the QueryForwardsWithCount method. +func TestQueryForwardsWithCount(t *testing.T) { + t.Parallel() + + ctx := context.Background() + store, _, db := newForwardStore(t) + setup := setupQueryTestData(t, ctx, store, db) + assetID1Spec := asset.NewSpecifierFromId(setup.assetID1) + + testCases := []struct { + name string + params rfq.QueryForwardsParams + expCount int64 + expLen int + }{{ + name: "limit without filters", + params: rfq.QueryForwardsParams{ + Limit: 3, + }, + expCount: 6, + expLen: 3, + }, { + name: "offset without filters", + params: rfq.QueryForwardsParams{ + Limit: 3, + Offset: 3, + }, + expCount: 6, + expLen: 3, + }, { + name: "filtered by asset", + params: rfq.QueryForwardsParams{ + AssetSpecifier: &assetID1Spec, + Limit: 10, + }, + expCount: 4, + expLen: 4, + }} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + records, count, err := store.QueryForwardsWithCount( + ctx, + tc.params, + ) + require.NoError(t, err) + require.Len(t, records, tc.expLen) + require.Equal(t, tc.expCount, count) + }) + } +} diff --git a/tapdb/migrations.go b/tapdb/migrations.go index d3718e8ca2..ae958e3c41 100644 --- a/tapdb/migrations.go +++ b/tapdb/migrations.go @@ -24,7 +24,7 @@ const ( // daemon. // // NOTE: This MUST be updated when a new migration is added. - LatestMigrationVersion = 51 + LatestMigrationVersion = 52 ) // DatabaseBackend is an interface that contains all methods our different diff --git a/tapdb/rfq_policies.go b/tapdb/rfq_policies.go index ae5b8171b8..89ab271912 100644 --- a/tapdb/rfq_policies.go +++ b/tapdb/rfq_policies.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/tapdb/sqlc" @@ -15,27 +16,11 @@ import ( "github.com/lightningnetwork/lnd/routing/route" ) -// RfqPolicyType denotes the type of a persisted RFQ policy. -type RfqPolicyType string - -const ( - // RfqPolicyTypeAssetSale identifies an asset sale policy. - RfqPolicyTypeAssetSale RfqPolicyType = "RFQ_POLICY_TYPE_SALE" - - // RfqPolicyTypeAssetPurchase identifies an asset purchase policy. - RfqPolicyTypeAssetPurchase RfqPolicyType = "RFQ_POLICY_TYPE_PURCHASE" -) - -// String converts the policy type to its string representation. -func (t RfqPolicyType) String() string { - return string(t) -} - // rfqPolicy is the database model for an RFQ policy. It contains all the // necessary fields to reconstruct a BuyAccept or SellAccept message. type rfqPolicy struct { // PolicyType denotes the type of the policy (buy or sell). - PolicyType RfqPolicyType + PolicyType rfq.RfqPolicyType // Scid is the short channel ID associated with the policy. Scid uint64 @@ -121,7 +106,7 @@ func (s *PersistedPolicyStore) StoreSalePolicy(ctx context.Context, expiry := acpt.AssetRate.Expiry.UTC() record := rfqPolicy{ - PolicyType: RfqPolicyTypeAssetSale, + PolicyType: rfq.RfqPolicyTypeAssetSale, Scid: uint64(acpt.ShortChannelId()), RfqID: rfqIDArray(acpt.ID), Peer: serializePeer(acpt.Peer), @@ -150,7 +135,7 @@ func (s *PersistedPolicyStore) StorePurchasePolicy(ctx context.Context, paymentMax := int64(acpt.Request.PaymentMaxAmt) record := rfqPolicy{ - PolicyType: RfqPolicyTypeAssetPurchase, + PolicyType: rfq.RfqPolicyTypeAssetPurchase, Scid: uint64(acpt.ShortChannelId()), RfqID: rfqIDArray(acpt.ID), Peer: serializePeer(acpt.Peer), @@ -205,7 +190,7 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) ( policy := policyFromRow(row) switch policy.PolicyType { - case RfqPolicyTypeAssetSale: + case rfq.RfqPolicyTypeAssetSale: accept, err := buyAcceptFromStored(policy) if err != nil { return fmt.Errorf("error restoring "+ @@ -213,7 +198,7 @@ func (s *PersistedPolicyStore) FetchAcceptedQuotes(ctx context.Context) ( } buyAccepts = append(buyAccepts, accept) - case RfqPolicyTypeAssetPurchase: + case rfq.RfqPolicyTypeAssetPurchase: accept, err := sellAcceptFromStored(policy) if err != nil { return fmt.Errorf("error restoring "+ @@ -313,7 +298,7 @@ func policyFromRow(row sqlc.RfqPolicy) rfqPolicy { } policy := rfqPolicy{ - PolicyType: RfqPolicyType(row.PolicyType), + PolicyType: rfq.RfqPolicyType(row.PolicyType), Scid: uint64(row.Scid), RfqID: rfqID, Peer: peer, diff --git a/tapdb/sqlc/migrations/000052_forwards.down.sql b/tapdb/sqlc/migrations/000052_forwards.down.sql new file mode 100644 index 0000000000..e9cbee91eb --- /dev/null +++ b/tapdb/sqlc/migrations/000052_forwards.down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS forwards_rfq_id_idx; +DROP INDEX IF EXISTS forwards_opened_at_idx; +DROP INDEX IF EXISTS forwards_settled_at_idx; +DROP TABLE IF EXISTS forwards; diff --git a/tapdb/sqlc/migrations/000052_forwards.up.sql b/tapdb/sqlc/migrations/000052_forwards.up.sql new file mode 100644 index 0000000000..cc8d12ce76 --- /dev/null +++ b/tapdb/sqlc/migrations/000052_forwards.up.sql @@ -0,0 +1,42 @@ +CREATE TABLE IF NOT EXISTS forwards ( + id INTEGER PRIMARY KEY, + + -- opened_at is the timestamp when the forward was initiated. + opened_at TIMESTAMP NOT NULL, + + -- settled_at is the timestamp when the forward settled. + settled_at TIMESTAMP, + + -- failed_at is the timestamp when the forward failed. + failed_at TIMESTAMP, + + -- rfq_id is the foreign key to the RFQ policy. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32) + REFERENCES rfq_policies(rfq_id), + + -- chan_id_in is the short channel ID of the incoming channel. + chan_id_in BIGINT NOT NULL, + + -- chan_id_out is the short channel ID of the outgoing channel. + chan_id_out BIGINT NOT NULL, + + -- htlc_id is the HTLC ID on the incoming channel. + htlc_id BIGINT NOT NULL, + + -- asset_amt is the asset amount involved in this swap. + asset_amt BIGINT NOT NULL, + + -- amt_in_msat is the actual amount received on the incoming channel in + -- millisatoshis. + amt_in_msat BIGINT NOT NULL, + + -- amt_out_msat is the actual amount sent on the outgoing channel in + -- millisatoshis. + amt_out_msat BIGINT NOT NULL, + + UNIQUE(chan_id_in, htlc_id) +); + +CREATE INDEX IF NOT EXISTS forwards_opened_at_idx ON forwards(opened_at); +CREATE INDEX IF NOT EXISTS forwards_settled_at_idx ON forwards(settled_at); +CREATE INDEX IF NOT EXISTS forwards_rfq_id_idx ON forwards(rfq_id); diff --git a/tapdb/sqlc/models.go b/tapdb/sqlc/models.go index 6dd37ef809..edaff94d38 100644 --- a/tapdb/sqlc/models.go +++ b/tapdb/sqlc/models.go @@ -236,6 +236,20 @@ type FederationUniSyncConfig struct { ProofType sql.NullString } +type Forward struct { + ID int64 + OpenedAt time.Time + SettledAt sql.NullTime + FailedAt sql.NullTime + RfqID []byte + ChanIDIn int64 + ChanIDOut int64 + HtlcID int64 + AssetAmt int64 + AmtInMsat int64 + AmtOutMsat int64 +} + type GenesisAsset struct { GenAssetID int64 AssetID []byte diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go index 8fee84c9bb..ae68f532ff 100644 --- a/tapdb/sqlc/querier.go +++ b/tapdb/sqlc/querier.go @@ -26,6 +26,7 @@ type Querier interface { ConfirmChainAnchorTx(ctx context.Context, arg ConfirmChainAnchorTxParams) error ConfirmChainTx(ctx context.Context, arg ConfirmChainTxParams) error CountAuthMailboxMessages(ctx context.Context) (int64, error) + CountForwards(ctx context.Context, arg CountForwardsParams) (int64, error) DeleteAllNodes(ctx context.Context, namespace string) (int64, error) DeleteAssetWitnesses(ctx context.Context, assetID int64) error DeleteExpiredUTXOLeases(ctx context.Context, now sql.NullTime) error @@ -198,10 +199,12 @@ type Querier interface { // Join on genesis_info_view to get leaf related fields. QueryFederationProofSyncLog(ctx context.Context, arg QueryFederationProofSyncLogParams) ([]QueryFederationProofSyncLogRow, error) QueryFederationUniSyncConfigs(ctx context.Context) ([]QueryFederationUniSyncConfigsRow, error) + QueryForwards(ctx context.Context, arg QueryForwardsParams) ([]QueryForwardsRow, error) QueryLastEventHeight(ctx context.Context, version int16) (int64, error) QueryLatestSupplyCommitment(ctx context.Context, groupKey []byte) (QueryLatestSupplyCommitmentRow, error) QueryMultiverseLeaves(ctx context.Context, arg QueryMultiverseLeavesParams) ([]QueryMultiverseLeavesRow, error) QueryPassiveAssets(ctx context.Context, transferID int64) ([]QueryPassiveAssetsRow, error) + QueryPendingForwards(ctx context.Context) ([]QueryPendingForwardsRow, error) QueryPendingSupplyCommitTransition(ctx context.Context, groupKey []byte) (QueryPendingSupplyCommitTransitionRow, error) QueryProofTransferAttempts(ctx context.Context, arg QueryProofTransferAttemptsParams) ([]time.Time, error) QueryStartingSupplyCommitment(ctx context.Context, groupKey []byte) (QueryStartingSupplyCommitmentRow, error) @@ -245,6 +248,7 @@ type Querier interface { UpsertFederationGlobalSyncConfig(ctx context.Context, arg UpsertFederationGlobalSyncConfigParams) error UpsertFederationProofSyncLog(ctx context.Context, arg UpsertFederationProofSyncLogParams) (int64, error) UpsertFederationUniSyncConfig(ctx context.Context, arg UpsertFederationUniSyncConfigParams) error + UpsertForward(ctx context.Context, arg UpsertForwardParams) (int64, error) UpsertGenesisAsset(ctx context.Context, arg UpsertGenesisAssetParams) (int64, error) UpsertGenesisPoint(ctx context.Context, prevOut []byte) (int64, error) UpsertInternalKey(ctx context.Context, arg UpsertInternalKeyParams) (int64, error) diff --git a/tapdb/sqlc/queries/rfq.sql b/tapdb/sqlc/queries/rfq.sql index 39b5f9c9be..c941a1e146 100644 --- a/tapdb/sqlc/queries/rfq.sql +++ b/tapdb/sqlc/queries/rfq.sql @@ -1,21 +1,9 @@ -- name: InsertRfqPolicy :one INSERT INTO rfq_policies ( - policy_type, - scid, - rfq_id, - peer, - asset_id, - asset_group_key, - rate_coefficient, - rate_scale, - expiry, - max_out_asset_amt, - payment_max_msat, - request_asset_max_amt, - request_payment_max_msat, - price_oracle_metadata, - request_version, - agreed_at + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, payment_max_msat, + request_asset_max_amt, request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, @@ -25,22 +13,62 @@ RETURNING id; -- name: FetchActiveRfqPolicies :many SELECT - id, - policy_type, - scid, - rfq_id, - peer, - asset_id, - asset_group_key, - rate_coefficient, - rate_scale, - expiry, - max_out_asset_amt, - payment_max_msat, - request_asset_max_amt, - request_payment_max_msat, - price_oracle_metadata, - request_version, - agreed_at + id, policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, payment_max_msat, + request_asset_max_amt, request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at FROM rfq_policies WHERE expiry >= sqlc.arg('min_expiry'); + +-- name: UpsertForward :one +INSERT INTO forwards ( + opened_at, settled_at, failed_at, rfq_id, chan_id_in, chan_id_out, + htlc_id, asset_amt, amt_in_msat, amt_out_msat +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +ON CONFLICT(chan_id_in, htlc_id) DO UPDATE SET + opened_at = excluded.opened_at, + settled_at = COALESCE(excluded.settled_at, forwards.settled_at), + failed_at = COALESCE(excluded.failed_at, forwards.failed_at), + rfq_id = excluded.rfq_id, + chan_id_out = excluded.chan_id_out, + asset_amt = excluded.asset_amt, + amt_in_msat = excluded.amt_in_msat, + amt_out_msat = excluded.amt_out_msat +RETURNING id; + +-- name: QueryPendingForwards :many +SELECT + f.opened_at, f.rfq_id, f.chan_id_in, f.chan_id_out, f.htlc_id, + f.asset_amt, f.amt_in_msat, f.amt_out_msat +FROM forwards f +WHERE f.settled_at IS NULL AND f.failed_at IS NULL +ORDER BY f.opened_at DESC; + +-- name: QueryForwards :many +SELECT + f.id, f.opened_at, f.settled_at, f.failed_at, f.rfq_id, + f.chan_id_in, f.chan_id_out, f.htlc_id, f.asset_amt, f.amt_in_msat, + f.amt_out_msat, p.policy_type, p.peer, p.asset_id, p.asset_group_key, + p.rate_coefficient, p.rate_scale +FROM forwards f +JOIN rfq_policies p ON f.rfq_id = p.rfq_id +WHERE f.opened_at >= @opened_after AND f.opened_at <= @opened_before + AND (p.peer = sqlc.narg('peer') OR sqlc.narg('peer') IS NULL) + AND (p.asset_id = sqlc.narg('asset_id') OR + sqlc.narg('asset_id') IS NULL) + AND (p.asset_group_key = sqlc.narg('asset_group_key') OR + sqlc.narg('asset_group_key') IS NULL) +ORDER BY f.opened_at DESC +LIMIT @num_limit OFFSET @num_offset; + +-- name: CountForwards :one +SELECT COUNT(*) as total +FROM forwards f +JOIN rfq_policies p ON f.rfq_id = p.rfq_id +WHERE f.opened_at >= @opened_after AND f.opened_at <= @opened_before + AND (p.peer = sqlc.narg('peer') OR sqlc.narg('peer') IS NULL) + AND (p.asset_id = sqlc.narg('asset_id') OR + sqlc.narg('asset_id') IS NULL) + AND (p.asset_group_key = sqlc.narg('asset_group_key') OR + sqlc.narg('asset_group_key') IS NULL); diff --git a/tapdb/sqlc/rfq.sql.go b/tapdb/sqlc/rfq.sql.go index fabb099d0d..49232920fa 100644 --- a/tapdb/sqlc/rfq.sql.go +++ b/tapdb/sqlc/rfq.sql.go @@ -8,27 +8,48 @@ package sqlc import ( "context" "database/sql" + "time" ) +const CountForwards = `-- name: CountForwards :one +SELECT COUNT(*) as total +FROM forwards f +JOIN rfq_policies p ON f.rfq_id = p.rfq_id +WHERE f.opened_at >= $1 AND f.opened_at <= $2 + AND (p.peer = $3 OR $3 IS NULL) + AND (p.asset_id = $4 OR + $4 IS NULL) + AND (p.asset_group_key = $5 OR + $5 IS NULL) +` + +type CountForwardsParams struct { + OpenedAfter time.Time + OpenedBefore time.Time + Peer []byte + AssetID []byte + AssetGroupKey []byte +} + +func (q *Queries) CountForwards(ctx context.Context, arg CountForwardsParams) (int64, error) { + row := q.db.QueryRowContext(ctx, CountForwards, + arg.OpenedAfter, + arg.OpenedBefore, + arg.Peer, + arg.AssetID, + arg.AssetGroupKey, + ) + var total int64 + err := row.Scan(&total) + return total, err +} + const FetchActiveRfqPolicies = `-- name: FetchActiveRfqPolicies :many SELECT - id, - policy_type, - scid, - rfq_id, - peer, - asset_id, - asset_group_key, - rate_coefficient, - rate_scale, - expiry, - max_out_asset_amt, - payment_max_msat, - request_asset_max_amt, - request_payment_max_msat, - price_oracle_metadata, - request_version, - agreed_at + id, policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, payment_max_msat, + request_asset_max_amt, request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at FROM rfq_policies WHERE expiry >= $1 ` @@ -76,22 +97,10 @@ func (q *Queries) FetchActiveRfqPolicies(ctx context.Context, minExpiry int64) ( const InsertRfqPolicy = `-- name: InsertRfqPolicy :one INSERT INTO rfq_policies ( - policy_type, - scid, - rfq_id, - peer, - asset_id, - asset_group_key, - rate_coefficient, - rate_scale, - expiry, - max_out_asset_amt, - payment_max_msat, - request_asset_max_amt, - request_payment_max_msat, - price_oracle_metadata, - request_version, - agreed_at + policy_type, scid, rfq_id, peer, asset_id, asset_group_key, + rate_coefficient, rate_scale, expiry, max_out_asset_amt, payment_max_msat, + request_asset_max_amt, request_payment_max_msat, price_oracle_metadata, + request_version, agreed_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, @@ -142,3 +151,201 @@ func (q *Queries) InsertRfqPolicy(ctx context.Context, arg InsertRfqPolicyParams err := row.Scan(&id) return id, err } + +const QueryForwards = `-- name: QueryForwards :many +SELECT + f.id, f.opened_at, f.settled_at, f.failed_at, f.rfq_id, + f.chan_id_in, f.chan_id_out, f.htlc_id, f.asset_amt, f.amt_in_msat, + f.amt_out_msat, p.policy_type, p.peer, p.asset_id, p.asset_group_key, + p.rate_coefficient, p.rate_scale +FROM forwards f +JOIN rfq_policies p ON f.rfq_id = p.rfq_id +WHERE f.opened_at >= $1 AND f.opened_at <= $2 + AND (p.peer = $3 OR $3 IS NULL) + AND (p.asset_id = $4 OR + $4 IS NULL) + AND (p.asset_group_key = $5 OR + $5 IS NULL) +ORDER BY f.opened_at DESC +LIMIT $7 OFFSET $6 +` + +type QueryForwardsParams struct { + OpenedAfter time.Time + OpenedBefore time.Time + Peer []byte + AssetID []byte + AssetGroupKey []byte + NumOffset int32 + NumLimit int32 +} + +type QueryForwardsRow struct { + ID int64 + OpenedAt time.Time + SettledAt sql.NullTime + FailedAt sql.NullTime + RfqID []byte + ChanIDIn int64 + ChanIDOut int64 + HtlcID int64 + AssetAmt int64 + AmtInMsat int64 + AmtOutMsat int64 + PolicyType string + Peer []byte + AssetID []byte + AssetGroupKey []byte + RateCoefficient []byte + RateScale int32 +} + +func (q *Queries) QueryForwards(ctx context.Context, arg QueryForwardsParams) ([]QueryForwardsRow, error) { + rows, err := q.db.QueryContext(ctx, QueryForwards, + arg.OpenedAfter, + arg.OpenedBefore, + arg.Peer, + arg.AssetID, + arg.AssetGroupKey, + arg.NumOffset, + arg.NumLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []QueryForwardsRow + for rows.Next() { + var i QueryForwardsRow + if err := rows.Scan( + &i.ID, + &i.OpenedAt, + &i.SettledAt, + &i.FailedAt, + &i.RfqID, + &i.ChanIDIn, + &i.ChanIDOut, + &i.HtlcID, + &i.AssetAmt, + &i.AmtInMsat, + &i.AmtOutMsat, + &i.PolicyType, + &i.Peer, + &i.AssetID, + &i.AssetGroupKey, + &i.RateCoefficient, + &i.RateScale, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const QueryPendingForwards = `-- name: QueryPendingForwards :many +SELECT + f.opened_at, f.rfq_id, f.chan_id_in, f.chan_id_out, f.htlc_id, + f.asset_amt, f.amt_in_msat, f.amt_out_msat +FROM forwards f +WHERE f.settled_at IS NULL AND f.failed_at IS NULL +ORDER BY f.opened_at DESC +` + +type QueryPendingForwardsRow struct { + OpenedAt time.Time + RfqID []byte + ChanIDIn int64 + ChanIDOut int64 + HtlcID int64 + AssetAmt int64 + AmtInMsat int64 + AmtOutMsat int64 +} + +func (q *Queries) QueryPendingForwards(ctx context.Context) ([]QueryPendingForwardsRow, error) { + rows, err := q.db.QueryContext(ctx, QueryPendingForwards) + if err != nil { + return nil, err + } + defer rows.Close() + var items []QueryPendingForwardsRow + for rows.Next() { + var i QueryPendingForwardsRow + if err := rows.Scan( + &i.OpenedAt, + &i.RfqID, + &i.ChanIDIn, + &i.ChanIDOut, + &i.HtlcID, + &i.AssetAmt, + &i.AmtInMsat, + &i.AmtOutMsat, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpsertForward = `-- name: UpsertForward :one +INSERT INTO forwards ( + opened_at, settled_at, failed_at, rfq_id, chan_id_in, chan_id_out, + htlc_id, asset_amt, amt_in_msat, amt_out_msat +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +ON CONFLICT(chan_id_in, htlc_id) DO UPDATE SET + opened_at = excluded.opened_at, + settled_at = COALESCE(excluded.settled_at, forwards.settled_at), + failed_at = COALESCE(excluded.failed_at, forwards.failed_at), + rfq_id = excluded.rfq_id, + chan_id_out = excluded.chan_id_out, + asset_amt = excluded.asset_amt, + amt_in_msat = excluded.amt_in_msat, + amt_out_msat = excluded.amt_out_msat +RETURNING id +` + +type UpsertForwardParams struct { + OpenedAt time.Time + SettledAt sql.NullTime + FailedAt sql.NullTime + RfqID []byte + ChanIDIn int64 + ChanIDOut int64 + HtlcID int64 + AssetAmt int64 + AmtInMsat int64 + AmtOutMsat int64 +} + +func (q *Queries) UpsertForward(ctx context.Context, arg UpsertForwardParams) (int64, error) { + row := q.db.QueryRowContext(ctx, UpsertForward, + arg.OpenedAt, + arg.SettledAt, + arg.FailedAt, + arg.RfqID, + arg.ChanIDIn, + arg.ChanIDOut, + arg.HtlcID, + arg.AssetAmt, + arg.AmtInMsat, + arg.AmtOutMsat, + ) + var id int64 + err := row.Scan(&id) + return id, err +} diff --git a/tapdb/sqlc/schemas/generated_schema.sql b/tapdb/sqlc/schemas/generated_schema.sql index 6728b94af8..380ddbc74f 100644 --- a/tapdb/sqlc/schemas/generated_schema.sql +++ b/tapdb/sqlc/schemas/generated_schema.sql @@ -502,6 +502,51 @@ CREATE TABLE federation_uni_sync_config ( ) ); +CREATE TABLE forwards ( + id INTEGER PRIMARY KEY, + + -- opened_at is the timestamp when the forward was initiated. + opened_at TIMESTAMP NOT NULL, + + -- settled_at is the timestamp when the forward settled. + settled_at TIMESTAMP, + + -- failed_at is the timestamp when the forward failed. + failed_at TIMESTAMP, + + -- rfq_id is the foreign key to the RFQ policy. + rfq_id BLOB NOT NULL CHECK (length(rfq_id) = 32) + REFERENCES rfq_policies(rfq_id), + + -- chan_id_in is the short channel ID of the incoming channel. + chan_id_in BIGINT NOT NULL, + + -- chan_id_out is the short channel ID of the outgoing channel. + chan_id_out BIGINT NOT NULL, + + -- htlc_id is the HTLC ID on the incoming channel. + htlc_id BIGINT NOT NULL, + + -- asset_amt is the asset amount involved in this swap. + asset_amt BIGINT NOT NULL, + + -- amt_in_msat is the actual amount received on the incoming channel in + -- millisatoshis. + amt_in_msat BIGINT NOT NULL, + + -- amt_out_msat is the actual amount sent on the outgoing channel in + -- millisatoshis. + amt_out_msat BIGINT NOT NULL, + + UNIQUE(chan_id_in, htlc_id) +); + +CREATE INDEX forwards_opened_at_idx ON forwards(opened_at); + +CREATE INDEX forwards_rfq_id_idx ON forwards(rfq_id); + +CREATE INDEX forwards_settled_at_idx ON forwards(settled_at); + CREATE TABLE genesis_assets ( gen_asset_id INTEGER PRIMARY KEY, diff --git a/tapdb/sqlutils.go b/tapdb/sqlutils.go index 94872d703a..f89588e08d 100644 --- a/tapdb/sqlutils.go +++ b/tapdb/sqlutils.go @@ -87,6 +87,12 @@ func sqlOptInt32[T constraints.Integer](num fn.Option[T]) sql.NullInt32 { }) } +// sqlOptTime turns an option of a time.Time into the NullTime that sql/sqlc +// uses when a time field can be permitted to be NULL. +func sqlOptTime(num fn.Option[time.Time]) sql.NullTime { + return fn.MapOptionZ(num, sqlTime) +} + // sqlInt16 turns a numerical integer type into the NullInt16 that sql/sqlc // uses when an integer field can be permitted to be NULL. // diff --git a/taprpc/perms.go b/taprpc/perms.go index e4e891c23c..276a4fb921 100644 --- a/taprpc/perms.go +++ b/taprpc/perms.go @@ -303,6 +303,10 @@ var ( Entity: "rfq", Action: "write", }}, + "/rfqrpc.Rfq/ForwardingHistory": {{ + Entity: "rfq", + Action: "read", + }}, "/tapchannelrpc.TaprootAssetChannels/FundChannel": {{ Entity: "channels", Action: "write", diff --git a/taprpc/rfqrpc/rfq.pb.go b/taprpc/rfqrpc/rfq.pb.go index 74ba018c44..38421c675d 100644 --- a/taprpc/rfqrpc/rfq.pb.go +++ b/taprpc/rfqrpc/rfq.pb.go @@ -76,6 +76,55 @@ func (QuoteRespStatus) EnumDescriptor() ([]byte, []int) { return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{0} } +// RfqPolicyType indicates the type of policy of an RFQ session. +type RfqPolicyType int32 + +const ( + // RFQ_POLICY_TYPE_SALE indicates that the RFQ session was a sale. + RfqPolicyType_RFQ_POLICY_TYPE_SALE RfqPolicyType = 0 + // RFQ_POLICY_TYPE_PURCHASE indicates that the RFQ session was a purchase. + RfqPolicyType_RFQ_POLICY_TYPE_PURCHASE RfqPolicyType = 1 +) + +// Enum value maps for RfqPolicyType. +var ( + RfqPolicyType_name = map[int32]string{ + 0: "RFQ_POLICY_TYPE_SALE", + 1: "RFQ_POLICY_TYPE_PURCHASE", + } + RfqPolicyType_value = map[string]int32{ + "RFQ_POLICY_TYPE_SALE": 0, + "RFQ_POLICY_TYPE_PURCHASE": 1, + } +) + +func (x RfqPolicyType) Enum() *RfqPolicyType { + p := new(RfqPolicyType) + *p = x + return p +} + +func (x RfqPolicyType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RfqPolicyType) Descriptor() protoreflect.EnumDescriptor { + return file_rfqrpc_rfq_proto_enumTypes[1].Descriptor() +} + +func (RfqPolicyType) Type() protoreflect.EnumType { + return &file_rfqrpc_rfq_proto_enumTypes[1] +} + +func (x RfqPolicyType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RfqPolicyType.Descriptor instead. +func (RfqPolicyType) EnumDescriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{1} +} + type AssetSpecifier struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1766,6 +1815,336 @@ func (*RfqEvent_PeerAcceptedSellQuote) isRfqEvent_Event() {} func (*RfqEvent_AcceptHtlc) isRfqEvent_Event() {} +type ForwardingHistoryRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // min_timestamp is the minimum Unix timestamp in seconds. Only forwarding + // events that settled at or after this time are returned. If not set, there + // is no lower bound. + MinTimestamp uint64 `protobuf:"varint,1,opt,name=min_timestamp,json=minTimestamp,proto3" json:"min_timestamp,omitempty"` + // max_timestamp is the maximum Unix timestamp in seconds. Only forwarding + // events that settled at or before this time are returned. If not set, + // there is no upper bound. + MaxTimestamp uint64 `protobuf:"varint,2,opt,name=max_timestamp,json=maxTimestamp,proto3" json:"max_timestamp,omitempty"` + // peer is the optional counterparty peer's public key. If set, + // only forwarding events involving this peer are returned. + Peer []byte `protobuf:"bytes,3,opt,name=peer,proto3" json:"peer,omitempty"` + // asset_specifier optionally filters forwarding events by asset ID or group + // key. + AssetSpecifier *AssetSpecifier `protobuf:"bytes,4,opt,name=asset_specifier,json=assetSpecifier,proto3" json:"asset_specifier,omitempty"` + // limit is the maximum number of records to return. + Limit int32 `protobuf:"varint,5,opt,name=limit,proto3" json:"limit,omitempty"` + // offset is the number of records to skip for pagination. + Offset int32 `protobuf:"varint,6,opt,name=offset,proto3" json:"offset,omitempty"` +} + +func (x *ForwardingHistoryRequest) Reset() { + *x = ForwardingHistoryRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ForwardingHistoryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForwardingHistoryRequest) ProtoMessage() {} + +func (x *ForwardingHistoryRequest) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForwardingHistoryRequest.ProtoReflect.Descriptor instead. +func (*ForwardingHistoryRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{22} +} + +func (x *ForwardingHistoryRequest) GetMinTimestamp() uint64 { + if x != nil { + return x.MinTimestamp + } + return 0 +} + +func (x *ForwardingHistoryRequest) GetMaxTimestamp() uint64 { + if x != nil { + return x.MaxTimestamp + } + return 0 +} + +func (x *ForwardingHistoryRequest) GetPeer() []byte { + if x != nil { + return x.Peer + } + return nil +} + +func (x *ForwardingHistoryRequest) GetAssetSpecifier() *AssetSpecifier { + if x != nil { + return x.AssetSpecifier + } + return nil +} + +func (x *ForwardingHistoryRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *ForwardingHistoryRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type ForwardingHistoryResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // forwards is the list of completed asset forwarding events matching the + // query. + Forwards []*ForwardingEvent `protobuf:"bytes,1,rep,name=forwards,proto3" json:"forwards,omitempty"` + // total_count is the total number of forwards matching the filter (before + // pagination is applied). This can be used for pagination. + TotalCount int64 `protobuf:"varint,2,opt,name=total_count,json=totalCount,proto3" json:"total_count,omitempty"` +} + +func (x *ForwardingHistoryResponse) Reset() { + *x = ForwardingHistoryResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ForwardingHistoryResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForwardingHistoryResponse) ProtoMessage() {} + +func (x *ForwardingHistoryResponse) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForwardingHistoryResponse.ProtoReflect.Descriptor instead. +func (*ForwardingHistoryResponse) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{23} +} + +func (x *ForwardingHistoryResponse) GetForwards() []*ForwardingEvent { + if x != nil { + return x.Forwards + } + return nil +} + +func (x *ForwardingHistoryResponse) GetTotalCount() int64 { + if x != nil { + return x.TotalCount + } + return 0 +} + +type ForwardingEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // rfq_id is the unique identifier of the RFQ session that governed this + // forward. + RfqId []byte `protobuf:"bytes,1,opt,name=rfq_id,json=rfqId,proto3" json:"rfq_id,omitempty"` + // chan_id_in is the short channel ID of the incoming channel. + ChanIdIn uint64 `protobuf:"varint,2,opt,name=chan_id_in,json=chanIdIn,proto3" json:"chan_id_in,omitempty"` + // chan_id_out is the short channel ID of the outgoing channel. + ChanIdOut uint64 `protobuf:"varint,3,opt,name=chan_id_out,json=chanIdOut,proto3" json:"chan_id_out,omitempty"` + // htlc_id is the HTLC ID on the incoming channel. + HtlcId uint64 `protobuf:"varint,4,opt,name=htlc_id,json=htlcId,proto3" json:"htlc_id,omitempty"` + // opened_at is the Unix timestamp in seconds when the forward was + // initiated. + OpenedAt uint64 `protobuf:"varint,5,opt,name=opened_at,json=openedAt,proto3" json:"opened_at,omitempty"` + // settled_at is the Unix timestamp in seconds when the forward settled. + // A value of 0 means the forward has not settled yet. + SettledAt uint64 `protobuf:"varint,6,opt,name=settled_at,json=settledAt,proto3" json:"settled_at,omitempty"` + // failed_at is the Unix timestamp in seconds when the forward failed. + // A value of 0 means the forward has not failed. + FailedAt uint64 `protobuf:"varint,7,opt,name=failed_at,json=failedAt,proto3" json:"failed_at,omitempty"` + // asset_amt is the asset amount that was swapped. + AssetAmt uint64 `protobuf:"varint,8,opt,name=asset_amt,json=assetAmt,proto3" json:"asset_amt,omitempty"` + // amt_in_msat is the actual amount received on the incoming channel in + // millisatoshis. + AmtInMsat uint64 `protobuf:"varint,9,opt,name=amt_in_msat,json=amtInMsat,proto3" json:"amt_in_msat,omitempty"` + // amt_out_msat is the actual amount sent on the outgoing channel in + // millisatoshis. + AmtOutMsat uint64 `protobuf:"varint,10,opt,name=amt_out_msat,json=amtOutMsat,proto3" json:"amt_out_msat,omitempty"` + // policy_type indicates whether this was a sale or purchase of assets + // from the edge node's perspective. + PolicyType RfqPolicyType `protobuf:"varint,11,opt,name=policy_type,json=policyType,proto3,enum=rfqrpc.RfqPolicyType" json:"policy_type,omitempty"` + // peer is the counterparty peer's public key as a hex string. + Peer string `protobuf:"bytes,12,opt,name=peer,proto3" json:"peer,omitempty"` + // asset_specifier identifies the asset that was swapped. + AssetSpec *AssetSpec `protobuf:"bytes,13,opt,name=asset_spec,json=assetSpec,proto3" json:"asset_spec,omitempty"` + // rate is the exchange rate that was used for this swap. + Rate *FixedPoint `protobuf:"bytes,14,opt,name=rate,proto3" json:"rate,omitempty"` +} + +func (x *ForwardingEvent) Reset() { + *x = ForwardingEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ForwardingEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ForwardingEvent) ProtoMessage() {} + +func (x *ForwardingEvent) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ForwardingEvent.ProtoReflect.Descriptor instead. +func (*ForwardingEvent) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{24} +} + +func (x *ForwardingEvent) GetRfqId() []byte { + if x != nil { + return x.RfqId + } + return nil +} + +func (x *ForwardingEvent) GetChanIdIn() uint64 { + if x != nil { + return x.ChanIdIn + } + return 0 +} + +func (x *ForwardingEvent) GetChanIdOut() uint64 { + if x != nil { + return x.ChanIdOut + } + return 0 +} + +func (x *ForwardingEvent) GetHtlcId() uint64 { + if x != nil { + return x.HtlcId + } + return 0 +} + +func (x *ForwardingEvent) GetOpenedAt() uint64 { + if x != nil { + return x.OpenedAt + } + return 0 +} + +func (x *ForwardingEvent) GetSettledAt() uint64 { + if x != nil { + return x.SettledAt + } + return 0 +} + +func (x *ForwardingEvent) GetFailedAt() uint64 { + if x != nil { + return x.FailedAt + } + return 0 +} + +func (x *ForwardingEvent) GetAssetAmt() uint64 { + if x != nil { + return x.AssetAmt + } + return 0 +} + +func (x *ForwardingEvent) GetAmtInMsat() uint64 { + if x != nil { + return x.AmtInMsat + } + return 0 +} + +func (x *ForwardingEvent) GetAmtOutMsat() uint64 { + if x != nil { + return x.AmtOutMsat + } + return 0 +} + +func (x *ForwardingEvent) GetPolicyType() RfqPolicyType { + if x != nil { + return x.PolicyType + } + return RfqPolicyType_RFQ_POLICY_TYPE_SALE +} + +func (x *ForwardingEvent) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + +func (x *ForwardingEvent) GetAssetSpec() *AssetSpec { + if x != nil { + return x.AssetSpec + } + return nil +} + +func (x *ForwardingEvent) GetRate() *FixedPoint { + if x != nil { + return x.Rate + } + return nil +} + var File_rfqrpc_rfq_proto protoreflect.FileDescriptor var file_rfqrpc_rfq_proto_rawDesc = []byte{ @@ -1995,47 +2374,109 @@ var file_rfqrpc_rfq_proto_rawDesc = []byte{ 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x42, 0x07, - 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x2a, 0x5a, 0x0a, 0x0f, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, - 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x52, 0x41, 0x54, 0x45, - 0x53, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x45, - 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x49, 0x43, 0x45, - 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x59, 0x5f, 0x45, 0x52, - 0x52, 0x10, 0x02, 0x32, 0xa8, 0x04, 0x0a, 0x03, 0x52, 0x66, 0x71, 0x12, 0x55, 0x0a, 0x10, 0x41, - 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, - 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xe7, 0x01, 0x0a, 0x18, 0x46, 0x6f, 0x72, 0x77, + 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6d, 0x69, 0x6e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x61, 0x78, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0c, 0x6d, 0x61, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x65, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x70, 0x65, + 0x65, 0x72, 0x12, 0x3f, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x22, 0x71, 0x0a, 0x19, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, + 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, + 0x0a, 0x08, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, + 0x64, 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x08, 0x66, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xdd, 0x03, 0x0a, 0x0f, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, + 0x69, 0x6e, 0x67, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x72, 0x66, 0x71, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x72, 0x66, 0x71, 0x49, 0x64, 0x12, + 0x1c, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x69, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x49, 0x6e, 0x12, 0x1e, 0x0a, + 0x0b, 0x63, 0x68, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6e, 0x49, 0x64, 0x4f, 0x75, 0x74, 0x12, 0x17, 0x0a, + 0x07, 0x68, 0x74, 0x6c, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, + 0x68, 0x74, 0x6c, 0x63, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x6e, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x6e, 0x65, + 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x65, 0x74, 0x74, 0x6c, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x41, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x74, 0x12, 0x1e, 0x0a, 0x0b, + 0x61, 0x6d, 0x74, 0x5f, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x61, 0x6d, 0x74, 0x49, 0x6e, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, + 0x61, 0x6d, 0x74, 0x5f, 0x6f, 0x75, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0a, 0x61, 0x6d, 0x74, 0x4f, 0x75, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x36, + 0x0a, 0x0b, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x70, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x65, 0x65, 0x72, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x65, 0x65, 0x72, 0x12, 0x30, 0x0a, 0x0a, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, + 0x63, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, 0x0a, 0x04, + 0x72, 0x61, 0x74, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x66, 0x71, + 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x04, + 0x72, 0x61, 0x74, 0x65, 0x2a, 0x5a, 0x0a, 0x0f, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x56, 0x41, 0x4c, + 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x53, 0x10, 0x00, + 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x45, 0x58, 0x50, 0x49, + 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, + 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x02, + 0x2a, 0x47, 0x0a, 0x0d, 0x52, 0x66, 0x71, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x46, 0x51, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x41, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, + 0x46, 0x51, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, + 0x55, 0x52, 0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x01, 0x32, 0x82, 0x05, 0x0a, 0x03, 0x52, 0x66, + 0x71, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, + 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, - 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, + 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, - 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, - 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, - 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, - 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, - 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, - 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, - 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, - 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, - 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, - 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x27, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, - 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x16, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, - 0x66, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, - 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x72, 0x66, 0x71, - 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x42, 0x37, + 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x10, + 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, + 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x26, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, + 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x53, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x10, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x30, 0x01, 0x12, 0x58, 0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, + 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, + 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, @@ -2054,73 +2495,84 @@ func file_rfqrpc_rfq_proto_rawDescGZIP() []byte { return file_rfqrpc_rfq_proto_rawDescData } -var file_rfqrpc_rfq_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_rfqrpc_rfq_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_rfqrpc_rfq_proto_goTypes = []any{ (QuoteRespStatus)(0), // 0: rfqrpc.QuoteRespStatus - (*AssetSpecifier)(nil), // 1: rfqrpc.AssetSpecifier - (*FixedPoint)(nil), // 2: rfqrpc.FixedPoint - (*AddAssetBuyOrderRequest)(nil), // 3: rfqrpc.AddAssetBuyOrderRequest - (*AddAssetBuyOrderResponse)(nil), // 4: rfqrpc.AddAssetBuyOrderResponse - (*AddAssetSellOrderRequest)(nil), // 5: rfqrpc.AddAssetSellOrderRequest - (*AddAssetSellOrderResponse)(nil), // 6: rfqrpc.AddAssetSellOrderResponse - (*AddAssetSellOfferRequest)(nil), // 7: rfqrpc.AddAssetSellOfferRequest - (*AddAssetSellOfferResponse)(nil), // 8: rfqrpc.AddAssetSellOfferResponse - (*AddAssetBuyOfferRequest)(nil), // 9: rfqrpc.AddAssetBuyOfferRequest - (*AddAssetBuyOfferResponse)(nil), // 10: rfqrpc.AddAssetBuyOfferResponse - (*QueryPeerAcceptedQuotesRequest)(nil), // 11: rfqrpc.QueryPeerAcceptedQuotesRequest - (*AssetSpec)(nil), // 12: rfqrpc.AssetSpec - (*PeerAcceptedBuyQuote)(nil), // 13: rfqrpc.PeerAcceptedBuyQuote - (*PeerAcceptedSellQuote)(nil), // 14: rfqrpc.PeerAcceptedSellQuote - (*InvalidQuoteResponse)(nil), // 15: rfqrpc.InvalidQuoteResponse - (*RejectedQuoteResponse)(nil), // 16: rfqrpc.RejectedQuoteResponse - (*QueryPeerAcceptedQuotesResponse)(nil), // 17: rfqrpc.QueryPeerAcceptedQuotesResponse - (*SubscribeRfqEventNtfnsRequest)(nil), // 18: rfqrpc.SubscribeRfqEventNtfnsRequest - (*PeerAcceptedBuyQuoteEvent)(nil), // 19: rfqrpc.PeerAcceptedBuyQuoteEvent - (*PeerAcceptedSellQuoteEvent)(nil), // 20: rfqrpc.PeerAcceptedSellQuoteEvent - (*AcceptHtlcEvent)(nil), // 21: rfqrpc.AcceptHtlcEvent - (*RfqEvent)(nil), // 22: rfqrpc.RfqEvent + (RfqPolicyType)(0), // 1: rfqrpc.RfqPolicyType + (*AssetSpecifier)(nil), // 2: rfqrpc.AssetSpecifier + (*FixedPoint)(nil), // 3: rfqrpc.FixedPoint + (*AddAssetBuyOrderRequest)(nil), // 4: rfqrpc.AddAssetBuyOrderRequest + (*AddAssetBuyOrderResponse)(nil), // 5: rfqrpc.AddAssetBuyOrderResponse + (*AddAssetSellOrderRequest)(nil), // 6: rfqrpc.AddAssetSellOrderRequest + (*AddAssetSellOrderResponse)(nil), // 7: rfqrpc.AddAssetSellOrderResponse + (*AddAssetSellOfferRequest)(nil), // 8: rfqrpc.AddAssetSellOfferRequest + (*AddAssetSellOfferResponse)(nil), // 9: rfqrpc.AddAssetSellOfferResponse + (*AddAssetBuyOfferRequest)(nil), // 10: rfqrpc.AddAssetBuyOfferRequest + (*AddAssetBuyOfferResponse)(nil), // 11: rfqrpc.AddAssetBuyOfferResponse + (*QueryPeerAcceptedQuotesRequest)(nil), // 12: rfqrpc.QueryPeerAcceptedQuotesRequest + (*AssetSpec)(nil), // 13: rfqrpc.AssetSpec + (*PeerAcceptedBuyQuote)(nil), // 14: rfqrpc.PeerAcceptedBuyQuote + (*PeerAcceptedSellQuote)(nil), // 15: rfqrpc.PeerAcceptedSellQuote + (*InvalidQuoteResponse)(nil), // 16: rfqrpc.InvalidQuoteResponse + (*RejectedQuoteResponse)(nil), // 17: rfqrpc.RejectedQuoteResponse + (*QueryPeerAcceptedQuotesResponse)(nil), // 18: rfqrpc.QueryPeerAcceptedQuotesResponse + (*SubscribeRfqEventNtfnsRequest)(nil), // 19: rfqrpc.SubscribeRfqEventNtfnsRequest + (*PeerAcceptedBuyQuoteEvent)(nil), // 20: rfqrpc.PeerAcceptedBuyQuoteEvent + (*PeerAcceptedSellQuoteEvent)(nil), // 21: rfqrpc.PeerAcceptedSellQuoteEvent + (*AcceptHtlcEvent)(nil), // 22: rfqrpc.AcceptHtlcEvent + (*RfqEvent)(nil), // 23: rfqrpc.RfqEvent + (*ForwardingHistoryRequest)(nil), // 24: rfqrpc.ForwardingHistoryRequest + (*ForwardingHistoryResponse)(nil), // 25: rfqrpc.ForwardingHistoryResponse + (*ForwardingEvent)(nil), // 26: rfqrpc.ForwardingEvent } var file_rfqrpc_rfq_proto_depIdxs = []int32{ - 1, // 0: rfqrpc.AddAssetBuyOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 13, // 1: rfqrpc.AddAssetBuyOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote - 15, // 2: rfqrpc.AddAssetBuyOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse - 16, // 3: rfqrpc.AddAssetBuyOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse - 1, // 4: rfqrpc.AddAssetSellOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 14, // 5: rfqrpc.AddAssetSellOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedSellQuote - 15, // 6: rfqrpc.AddAssetSellOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse - 16, // 7: rfqrpc.AddAssetSellOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse - 1, // 8: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 1, // 9: rfqrpc.AddAssetBuyOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 2, // 10: rfqrpc.PeerAcceptedBuyQuote.ask_asset_rate:type_name -> rfqrpc.FixedPoint - 12, // 11: rfqrpc.PeerAcceptedBuyQuote.asset_spec:type_name -> rfqrpc.AssetSpec - 2, // 12: rfqrpc.PeerAcceptedSellQuote.bid_asset_rate:type_name -> rfqrpc.FixedPoint - 12, // 13: rfqrpc.PeerAcceptedSellQuote.asset_spec:type_name -> rfqrpc.AssetSpec + 2, // 0: rfqrpc.AddAssetBuyOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 14, // 1: rfqrpc.AddAssetBuyOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote + 16, // 2: rfqrpc.AddAssetBuyOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse + 17, // 3: rfqrpc.AddAssetBuyOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse + 2, // 4: rfqrpc.AddAssetSellOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 15, // 5: rfqrpc.AddAssetSellOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedSellQuote + 16, // 6: rfqrpc.AddAssetSellOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse + 17, // 7: rfqrpc.AddAssetSellOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse + 2, // 8: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 2, // 9: rfqrpc.AddAssetBuyOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 3, // 10: rfqrpc.PeerAcceptedBuyQuote.ask_asset_rate:type_name -> rfqrpc.FixedPoint + 13, // 11: rfqrpc.PeerAcceptedBuyQuote.asset_spec:type_name -> rfqrpc.AssetSpec + 3, // 12: rfqrpc.PeerAcceptedSellQuote.bid_asset_rate:type_name -> rfqrpc.FixedPoint + 13, // 13: rfqrpc.PeerAcceptedSellQuote.asset_spec:type_name -> rfqrpc.AssetSpec 0, // 14: rfqrpc.InvalidQuoteResponse.status:type_name -> rfqrpc.QuoteRespStatus - 13, // 15: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote - 14, // 16: rfqrpc.QueryPeerAcceptedQuotesResponse.sell_quotes:type_name -> rfqrpc.PeerAcceptedSellQuote - 13, // 17: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote - 14, // 18: rfqrpc.PeerAcceptedSellQuoteEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuote - 19, // 19: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent - 20, // 20: rfqrpc.RfqEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuoteEvent - 21, // 21: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent - 3, // 22: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest - 5, // 23: rfqrpc.Rfq.AddAssetSellOrder:input_type -> rfqrpc.AddAssetSellOrderRequest - 7, // 24: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest - 9, // 25: rfqrpc.Rfq.AddAssetBuyOffer:input_type -> rfqrpc.AddAssetBuyOfferRequest - 11, // 26: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest - 18, // 27: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest - 4, // 28: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse - 6, // 29: rfqrpc.Rfq.AddAssetSellOrder:output_type -> rfqrpc.AddAssetSellOrderResponse - 8, // 30: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse - 10, // 31: rfqrpc.Rfq.AddAssetBuyOffer:output_type -> rfqrpc.AddAssetBuyOfferResponse - 17, // 32: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse - 22, // 33: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent - 28, // [28:34] is the sub-list for method output_type - 22, // [22:28] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 14, // 15: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote + 15, // 16: rfqrpc.QueryPeerAcceptedQuotesResponse.sell_quotes:type_name -> rfqrpc.PeerAcceptedSellQuote + 14, // 17: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote + 15, // 18: rfqrpc.PeerAcceptedSellQuoteEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuote + 20, // 19: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent + 21, // 20: rfqrpc.RfqEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuoteEvent + 22, // 21: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent + 2, // 22: rfqrpc.ForwardingHistoryRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 26, // 23: rfqrpc.ForwardingHistoryResponse.forwards:type_name -> rfqrpc.ForwardingEvent + 1, // 24: rfqrpc.ForwardingEvent.policy_type:type_name -> rfqrpc.RfqPolicyType + 13, // 25: rfqrpc.ForwardingEvent.asset_spec:type_name -> rfqrpc.AssetSpec + 3, // 26: rfqrpc.ForwardingEvent.rate:type_name -> rfqrpc.FixedPoint + 4, // 27: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest + 6, // 28: rfqrpc.Rfq.AddAssetSellOrder:input_type -> rfqrpc.AddAssetSellOrderRequest + 8, // 29: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest + 10, // 30: rfqrpc.Rfq.AddAssetBuyOffer:input_type -> rfqrpc.AddAssetBuyOfferRequest + 12, // 31: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest + 19, // 32: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest + 24, // 33: rfqrpc.Rfq.ForwardingHistory:input_type -> rfqrpc.ForwardingHistoryRequest + 5, // 34: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse + 7, // 35: rfqrpc.Rfq.AddAssetSellOrder:output_type -> rfqrpc.AddAssetSellOrderResponse + 9, // 36: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse + 11, // 37: rfqrpc.Rfq.AddAssetBuyOffer:output_type -> rfqrpc.AddAssetBuyOfferResponse + 18, // 38: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse + 23, // 39: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent + 25, // 40: rfqrpc.Rfq.ForwardingHistory:output_type -> rfqrpc.ForwardingHistoryResponse + 34, // [34:41] is the sub-list for method output_type + 27, // [27:34] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_rfqrpc_rfq_proto_init() } @@ -2393,6 +2845,42 @@ func file_rfqrpc_rfq_proto_init() { return nil } } + file_rfqrpc_rfq_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*ForwardingHistoryRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[23].Exporter = func(v any, i int) any { + switch v := v.(*ForwardingHistoryResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[24].Exporter = func(v any, i int) any { + switch v := v.(*ForwardingEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_rfqrpc_rfq_proto_msgTypes[0].OneofWrappers = []any{ (*AssetSpecifier_AssetId)(nil), @@ -2420,8 +2908,8 @@ func file_rfqrpc_rfq_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_rfqrpc_rfq_proto_rawDesc, - NumEnums: 1, - NumMessages: 22, + NumEnums: 2, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, diff --git a/taprpc/rfqrpc/rfq.pb.gw.go b/taprpc/rfqrpc/rfq.pb.gw.go index 57abbcc247..4b5a1855f6 100644 --- a/taprpc/rfqrpc/rfq.pb.gw.go +++ b/taprpc/rfqrpc/rfq.pb.gw.go @@ -550,6 +550,42 @@ func request_Rfq_SubscribeRfqEventNtfns_0(ctx context.Context, marshaler runtime } +var ( + filter_Rfq_ForwardingHistory_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + +func request_Rfq_ForwardingHistory_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ForwardingHistoryRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Rfq_ForwardingHistory_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.ForwardingHistory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_ForwardingHistory_0(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ForwardingHistoryRequest + var metadata runtime.ServerMetadata + + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Rfq_ForwardingHistory_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.ForwardingHistory(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterRfqHandlerServer registers the http handlers for service Rfq to "mux". // UnaryRPC :call RfqServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -788,6 +824,31 @@ func RegisterRfqHandlerServer(ctx context.Context, mux *runtime.ServeMux, server return }) + mux.Handle("GET", pattern_Rfq_ForwardingHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/ForwardingHistory", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/forwardinghistory")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Rfq_ForwardingHistory_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_ForwardingHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1049,6 +1110,28 @@ func RegisterRfqHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("GET", pattern_Rfq_ForwardingHistory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/ForwardingHistory", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/forwardinghistory")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_ForwardingHistory_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_ForwardingHistory_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -1072,6 +1155,8 @@ var ( pattern_Rfq_QueryPeerAcceptedQuotes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"v1", "taproot-assets", "rfq", "quotes", "peeraccepted"}, "")) pattern_Rfq_SubscribeRfqEventNtfns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "rfq", "ntfs"}, "")) + + pattern_Rfq_ForwardingHistory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "rfq", "forwardinghistory"}, "")) ) var ( @@ -1094,4 +1179,6 @@ var ( forward_Rfq_QueryPeerAcceptedQuotes_0 = runtime.ForwardResponseMessage forward_Rfq_SubscribeRfqEventNtfns_0 = runtime.ForwardResponseStream + + forward_Rfq_ForwardingHistory_0 = runtime.ForwardResponseMessage ) diff --git a/taprpc/rfqrpc/rfq.pb.json.go b/taprpc/rfqrpc/rfq.pb.json.go index 914bf39bb3..0d8971490d 100644 --- a/taprpc/rfqrpc/rfq.pb.json.go +++ b/taprpc/rfqrpc/rfq.pb.json.go @@ -187,4 +187,29 @@ func RegisterRfqJSONCallbacks(registry map[string]func(ctx context.Context, } }() } + + registry["rfqrpc.Rfq.ForwardingHistory"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ForwardingHistoryRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + resp, err := client.ForwardingHistory(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } } diff --git a/taprpc/rfqrpc/rfq.proto b/taprpc/rfqrpc/rfq.proto index 0d472c268d..abd27946b8 100644 --- a/taprpc/rfqrpc/rfq.proto +++ b/taprpc/rfqrpc/rfq.proto @@ -68,6 +68,13 @@ service Rfq { */ rpc SubscribeRfqEventNtfns (SubscribeRfqEventNtfnsRequest) returns (stream RfqEvent); + + /* tapcli: `rfq forwardinghistory` + ForwardingHistory is used to query for completed asset forwarding events. + These are historical records of HTLCs that were settled successfully. + */ + rpc ForwardingHistory (ForwardingHistoryRequest) + returns (ForwardingHistoryResponse); } message AssetSpecifier { @@ -434,3 +441,99 @@ message RfqEvent { AcceptHtlcEvent accept_htlc = 3; } } + +// RfqPolicyType indicates the type of policy of an RFQ session. +enum RfqPolicyType { + // RFQ_POLICY_TYPE_SALE indicates that the RFQ session was a sale. + RFQ_POLICY_TYPE_SALE = 0; + + // RFQ_POLICY_TYPE_PURCHASE indicates that the RFQ session was a purchase. + RFQ_POLICY_TYPE_PURCHASE = 1; +} + +message ForwardingHistoryRequest { + // min_timestamp is the minimum Unix timestamp in seconds. Only forwarding + // events that settled at or after this time are returned. If not set, there + // is no lower bound. + uint64 min_timestamp = 1; + + // max_timestamp is the maximum Unix timestamp in seconds. Only forwarding + // events that settled at or before this time are returned. If not set, + // there is no upper bound. + uint64 max_timestamp = 2; + + // peer is the optional counterparty peer's public key. If set, + // only forwarding events involving this peer are returned. + bytes peer = 3; + + // asset_specifier optionally filters forwarding events by asset ID or group + // key. + AssetSpecifier asset_specifier = 4; + + // limit is the maximum number of records to return. + int32 limit = 5; + + // offset is the number of records to skip for pagination. + int32 offset = 6; +} + +message ForwardingHistoryResponse { + // forwards is the list of completed asset forwarding events matching the + // query. + repeated ForwardingEvent forwards = 1; + + // total_count is the total number of forwards matching the filter (before + // pagination is applied). This can be used for pagination. + int64 total_count = 2; +} + +message ForwardingEvent { + // rfq_id is the unique identifier of the RFQ session that governed this + // forward. + bytes rfq_id = 1; + + // chan_id_in is the short channel ID of the incoming channel. + uint64 chan_id_in = 2; + + // chan_id_out is the short channel ID of the outgoing channel. + uint64 chan_id_out = 3; + + // htlc_id is the HTLC ID on the incoming channel. + uint64 htlc_id = 4; + + // opened_at is the Unix timestamp in seconds when the forward was + // initiated. + uint64 opened_at = 5; + + // settled_at is the Unix timestamp in seconds when the forward settled. + // A value of 0 means the forward has not settled yet. + uint64 settled_at = 6; + + // failed_at is the Unix timestamp in seconds when the forward failed. + // A value of 0 means the forward has not failed. + uint64 failed_at = 7; + + // asset_amt is the asset amount that was swapped. + uint64 asset_amt = 8; + + // amt_in_msat is the actual amount received on the incoming channel in + // millisatoshis. + uint64 amt_in_msat = 9; + + // amt_out_msat is the actual amount sent on the outgoing channel in + // millisatoshis. + uint64 amt_out_msat = 10; + + // policy_type indicates whether this was a sale or purchase of assets + // from the edge node's perspective. + RfqPolicyType policy_type = 11; + + // peer is the counterparty peer's public key as a hex string. + string peer = 12; + + // asset_specifier identifies the asset that was swapped. + AssetSpec asset_spec = 13; + + // rate is the exchange rate that was used for this swap. + FixedPoint rate = 14; +} diff --git a/taprpc/rfqrpc/rfq.swagger.json b/taprpc/rfqrpc/rfq.swagger.json index 71636f8f78..c0d1785ba6 100644 --- a/taprpc/rfqrpc/rfq.swagger.json +++ b/taprpc/rfqrpc/rfq.swagger.json @@ -180,6 +180,101 @@ ] } }, + "/v1/taproot-assets/rfq/forwardinghistory": { + "get": { + "summary": "tapcli: `rfq forwardinghistory`\nForwardingHistory is used to query for completed asset forwarding events.\nThese are historical records of HTLCs that were settled successfully.", + "operationId": "Rfq_ForwardingHistory", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcForwardingHistoryResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "min_timestamp", + "description": "min_timestamp is the minimum Unix timestamp in seconds. Only forwarding\nevents that settled at or after this time are returned. If not set, there\nis no lower bound.", + "in": "query", + "required": false, + "type": "string", + "format": "uint64" + }, + { + "name": "max_timestamp", + "description": "max_timestamp is the maximum Unix timestamp in seconds. Only forwarding\nevents that settled at or before this time are returned. If not set,\nthere is no upper bound.", + "in": "query", + "required": false, + "type": "string", + "format": "uint64" + }, + { + "name": "peer", + "description": "peer is the optional counterparty peer's public key. If set,\nonly forwarding events involving this peer are returned.", + "in": "query", + "required": false, + "type": "string", + "format": "byte" + }, + { + "name": "asset_specifier.asset_id", + "description": "The 32-byte asset ID specified as raw bytes (gRPC only).", + "in": "query", + "required": false, + "type": "string", + "format": "byte" + }, + { + "name": "asset_specifier.asset_id_str", + "description": "The 32-byte asset ID encoded as a hex string (use this for REST).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "asset_specifier.group_key", + "description": "The 32-byte asset group key specified as raw bytes (gRPC only).", + "in": "query", + "required": false, + "type": "string", + "format": "byte" + }, + { + "name": "asset_specifier.group_key_str", + "description": "The 32-byte asset group key encoded as hex string (use this for\nREST).", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "limit", + "description": "limit is the maximum number of records to return.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + }, + { + "name": "offset", + "description": "offset is the number of records to skip for pagination.", + "in": "query", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "Rfq" + ] + } + }, "/v1/taproot-assets/rfq/ntfs": { "post": { "summary": "SubscribeRfqEventNtfns is used to subscribe to RFQ events.", @@ -694,6 +789,95 @@ }, "description": "FixedPoint is a scaled integer representation of a fractional number.\n\nThis type consists of two integer fields: a coefficient and a scale.\nUsing this format enables precise and consistent representation of fractional\nnumbers while avoiding floating-point data types, which are prone to\nprecision errors.\n\nThe relationship between the fractional representation and its fixed-point\nrepresentation is expressed as:\n```\nV = F_c / (10^F_s)\n```\nwhere:\n\n* `V` is the fractional value.\n\n* `F_c` is the coefficient component of the fixed-point representation. It is\n the scaled-up fractional value represented as an integer.\n\n* `F_s` is the scale component. It is an integer specifying how\n many decimal places `F_c` should be divided by to obtain the fractional\n representation." }, + "rfqrpcForwardingEvent": { + "type": "object", + "properties": { + "rfq_id": { + "type": "string", + "format": "byte", + "description": "rfq_id is the unique identifier of the RFQ session that governed this\nforward." + }, + "chan_id_in": { + "type": "string", + "format": "uint64", + "description": "chan_id_in is the short channel ID of the incoming channel." + }, + "chan_id_out": { + "type": "string", + "format": "uint64", + "description": "chan_id_out is the short channel ID of the outgoing channel." + }, + "htlc_id": { + "type": "string", + "format": "uint64", + "description": "htlc_id is the HTLC ID on the incoming channel." + }, + "opened_at": { + "type": "string", + "format": "uint64", + "description": "opened_at is the Unix timestamp in seconds when the forward was\ninitiated." + }, + "settled_at": { + "type": "string", + "format": "uint64", + "description": "settled_at is the Unix timestamp in seconds when the forward settled.\nA value of 0 means the forward has not settled yet." + }, + "failed_at": { + "type": "string", + "format": "uint64", + "description": "failed_at is the Unix timestamp in seconds when the forward failed.\nA value of 0 means the forward has not failed." + }, + "asset_amt": { + "type": "string", + "format": "uint64", + "description": "asset_amt is the asset amount that was swapped." + }, + "amt_in_msat": { + "type": "string", + "format": "uint64", + "description": "amt_in_msat is the actual amount received on the incoming channel in\nmillisatoshis." + }, + "amt_out_msat": { + "type": "string", + "format": "uint64", + "description": "amt_out_msat is the actual amount sent on the outgoing channel in\nmillisatoshis." + }, + "policy_type": { + "$ref": "#/definitions/rfqrpcRfqPolicyType", + "description": "policy_type indicates whether this was a sale or purchase of assets\nfrom the edge node's perspective." + }, + "peer": { + "type": "string", + "description": "peer is the counterparty peer's public key as a hex string." + }, + "asset_spec": { + "$ref": "#/definitions/rfqrpcAssetSpec", + "description": "asset_specifier identifies the asset that was swapped." + }, + "rate": { + "$ref": "#/definitions/rfqrpcFixedPoint", + "description": "rate is the exchange rate that was used for this swap." + } + } + }, + "rfqrpcForwardingHistoryResponse": { + "type": "object", + "properties": { + "forwards": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/rfqrpcForwardingEvent" + }, + "description": "forwards is the list of completed asset forwarding events matching the\nquery." + }, + "total_count": { + "type": "string", + "format": "int64", + "description": "total_count is the total number of forwards matching the filter (before\npagination is applied). This can be used for pagination." + } + } + }, "rfqrpcInvalidQuoteResponse": { "type": "object", "properties": { @@ -905,6 +1089,15 @@ } } }, + "rfqrpcRfqPolicyType": { + "type": "string", + "enum": [ + "RFQ_POLICY_TYPE_SALE", + "RFQ_POLICY_TYPE_PURCHASE" + ], + "default": "RFQ_POLICY_TYPE_SALE", + "description": "RfqPolicyType indicates the type of policy of an RFQ session.\n\n - RFQ_POLICY_TYPE_SALE: RFQ_POLICY_TYPE_SALE indicates that the RFQ session was a sale.\n - RFQ_POLICY_TYPE_PURCHASE: RFQ_POLICY_TYPE_PURCHASE indicates that the RFQ session was a purchase." + }, "rfqrpcSubscribeRfqEventNtfnsRequest": { "type": "object" }, diff --git a/taprpc/rfqrpc/rfq.yaml b/taprpc/rfqrpc/rfq.yaml index 2610bc7c24..9079f6095e 100644 --- a/taprpc/rfqrpc/rfq.yaml +++ b/taprpc/rfqrpc/rfq.yaml @@ -34,6 +34,9 @@ http: - selector: rfqrpc.Rfq.QueryPeerAcceptedQuotes get: "/v1/taproot-assets/rfq/quotes/peeraccepted" + - selector: rfqrpc.Rfq.ForwardingHistory + get: "/v1/taproot-assets/rfq/forwardinghistory" + - selector: rfqrpc.Rfq.SubscribeRfqEventNtfns post: "/v1/taproot-assets/rfq/ntfs" body: "*" \ No newline at end of file diff --git a/taprpc/rfqrpc/rfq_grpc.pb.go b/taprpc/rfqrpc/rfq_grpc.pb.go index f3adbe75dd..3f2826b6b8 100644 --- a/taprpc/rfqrpc/rfq_grpc.pb.go +++ b/taprpc/rfqrpc/rfq_grpc.pb.go @@ -63,6 +63,10 @@ type RfqClient interface { QueryPeerAcceptedQuotes(ctx context.Context, in *QueryPeerAcceptedQuotesRequest, opts ...grpc.CallOption) (*QueryPeerAcceptedQuotesResponse, error) // SubscribeRfqEventNtfns is used to subscribe to RFQ events. SubscribeRfqEventNtfns(ctx context.Context, in *SubscribeRfqEventNtfnsRequest, opts ...grpc.CallOption) (Rfq_SubscribeRfqEventNtfnsClient, error) + // tapcli: `rfq forwardinghistory` + // ForwardingHistory is used to query for completed asset forwarding events. + // These are historical records of HTLCs that were settled successfully. + ForwardingHistory(ctx context.Context, in *ForwardingHistoryRequest, opts ...grpc.CallOption) (*ForwardingHistoryResponse, error) } type rfqClient struct { @@ -150,6 +154,15 @@ func (x *rfqSubscribeRfqEventNtfnsClient) Recv() (*RfqEvent, error) { return m, nil } +func (c *rfqClient) ForwardingHistory(ctx context.Context, in *ForwardingHistoryRequest, opts ...grpc.CallOption) (*ForwardingHistoryResponse, error) { + out := new(ForwardingHistoryResponse) + err := c.cc.Invoke(ctx, "/rfqrpc.Rfq/ForwardingHistory", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RfqServer is the server API for Rfq service. // All implementations must embed UnimplementedRfqServer // for forward compatibility @@ -199,6 +212,10 @@ type RfqServer interface { QueryPeerAcceptedQuotes(context.Context, *QueryPeerAcceptedQuotesRequest) (*QueryPeerAcceptedQuotesResponse, error) // SubscribeRfqEventNtfns is used to subscribe to RFQ events. SubscribeRfqEventNtfns(*SubscribeRfqEventNtfnsRequest, Rfq_SubscribeRfqEventNtfnsServer) error + // tapcli: `rfq forwardinghistory` + // ForwardingHistory is used to query for completed asset forwarding events. + // These are historical records of HTLCs that were settled successfully. + ForwardingHistory(context.Context, *ForwardingHistoryRequest) (*ForwardingHistoryResponse, error) mustEmbedUnimplementedRfqServer() } @@ -224,6 +241,9 @@ func (UnimplementedRfqServer) QueryPeerAcceptedQuotes(context.Context, *QueryPee func (UnimplementedRfqServer) SubscribeRfqEventNtfns(*SubscribeRfqEventNtfnsRequest, Rfq_SubscribeRfqEventNtfnsServer) error { return status.Errorf(codes.Unimplemented, "method SubscribeRfqEventNtfns not implemented") } +func (UnimplementedRfqServer) ForwardingHistory(context.Context, *ForwardingHistoryRequest) (*ForwardingHistoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ForwardingHistory not implemented") +} func (UnimplementedRfqServer) mustEmbedUnimplementedRfqServer() {} // UnsafeRfqServer may be embedded to opt out of forward compatibility for this service. @@ -348,6 +368,24 @@ func (x *rfqSubscribeRfqEventNtfnsServer) Send(m *RfqEvent) error { return x.ServerStream.SendMsg(m) } +func _Rfq_ForwardingHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ForwardingHistoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RfqServer).ForwardingHistory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rfqrpc.Rfq/ForwardingHistory", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RfqServer).ForwardingHistory(ctx, req.(*ForwardingHistoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Rfq_ServiceDesc is the grpc.ServiceDesc for Rfq service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -375,6 +413,10 @@ var Rfq_ServiceDesc = grpc.ServiceDesc{ MethodName: "QueryPeerAcceptedQuotes", Handler: _Rfq_QueryPeerAcceptedQuotes_Handler, }, + { + MethodName: "ForwardingHistory", + Handler: _Rfq_ForwardingHistory_Handler, + }, }, Streams: []grpc.StreamDesc{ {