paymentsdb: implement QueryPayments for sql backend

This commit is contained in:
ziggie committed 2026-02-25 08:02:08 +01:00
1 parent eeca189b87
commit e8fe45fe65
3 files changed
+917

No files matched your search

+272
View File
@@ -0,0 +1,272 @@
package paymentsdb
import (
"bytes"
"fmt"
"strconv"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
"github.com/lightningnetwork/lnd/tlv"
)
// dbPaymentToCreationInfo converts database payment data to the
// PaymentCreationInfo struct.
func dbPaymentToCreationInfo(paymentIdentifier []byte, amountMsat int64,
createdAt time.Time, intentPayload []byte,
firstHopCustomRecords lnwire.CustomRecords) *PaymentCreationInfo {
// This is the payment hash for non-AMP payments and the SetID for AMP
// payments.
var identifier lntypes.Hash
copy(identifier[:], paymentIdentifier)
return &PaymentCreationInfo{
PaymentIdentifier: identifier,
Value: lnwire.MilliSatoshi(amountMsat),
CreationTime: createdAt.Local(),
PaymentRequest: intentPayload,
FirstHopCustomRecords: firstHopCustomRecords,
}
}
// dbAttemptToHTLCAttempt converts a database HTLC attempt to an HTLCAttempt.
func dbAttemptToHTLCAttempt(dbAttempt sqlc.FetchHtlcAttemptsForPaymentsRow,
hops []sqlc.FetchHopsForAttemptsRow,
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
routeCustomRecords []sqlc.PaymentAttemptFirstHopCustomRecord) (
*HTLCAttempt, error) {
// Convert route-level first hop custom records to CustomRecords map.
var firstHopWireCustomRecords lnwire.CustomRecords
if len(routeCustomRecords) > 0 {
firstHopWireCustomRecords = make(lnwire.CustomRecords)
for _, record := range routeCustomRecords {
firstHopWireCustomRecords[uint64(record.Key)] =
record.Value
}
}
// Build the route from the database data.
route, err := dbDataToRoute(
hops, hopCustomRecords, dbAttempt.FirstHopAmountMsat,
dbAttempt.RouteTotalTimeLock, dbAttempt.RouteTotalAmount,
dbAttempt.RouteSourceKey, firstHopWireCustomRecords,
)
if err != nil {
return nil, fmt.Errorf("failed to convert to route: %w",
err)
}
hash, err := lntypes.MakeHash(dbAttempt.PaymentHash)
if err != nil {
return nil, fmt.Errorf("failed to parse payment "+
"hash: %w", err)
}
// Create the attempt info.
var sessionKey [32]byte
copy(sessionKey[:], dbAttempt.SessionKey)
info := HTLCAttemptInfo{
AttemptID: uint64(dbAttempt.AttemptIndex),
sessionKey: sessionKey,
Route: *route,
AttemptTime: dbAttempt.AttemptTime,
Hash: &hash,
}
attempt := &HTLCAttempt{
HTLCAttemptInfo: info,
}
// If there's no resolution type, the attempt is still in-flight.
// Return early without processing settlement or failure info.
if !dbAttempt.ResolutionType.Valid {
return attempt, nil
}
// Add settlement info if present.
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
HTLCAttemptResolutionSettled {
var preimage lntypes.Preimage
copy(preimage[:], dbAttempt.SettlePreimage)
attempt.Settle = &HTLCSettleInfo{
Preimage: preimage,
SettleTime: dbAttempt.ResolutionTime.Time,
}
}
// Add failure info if present.
if HTLCAttemptResolutionType(dbAttempt.ResolutionType.Int32) ==
HTLCAttemptResolutionFailed {
failure := &HTLCFailInfo{
FailTime: dbAttempt.ResolutionTime.Time,
}
if dbAttempt.HtlcFailReason.Valid {
failure.Reason = HTLCFailReason(
dbAttempt.HtlcFailReason.Int32,
)
}
if dbAttempt.FailureSourceIndex.Valid {
failure.FailureSourceIndex = uint32(
dbAttempt.FailureSourceIndex.Int32,
)
}
// Decode the failure message if present.
if len(dbAttempt.FailureMsg) > 0 {
msg, err := lnwire.DecodeFailureMessage(
bytes.NewReader(dbAttempt.FailureMsg), 0,
)
if err != nil {
return nil, fmt.Errorf("failed to decode "+
"failure message: %w", err)
}
failure.Message = msg
}
attempt.Failure = failure
}
return attempt, nil
}
// dbDataToRoute converts database route data to a route.Route.
func dbDataToRoute(hops []sqlc.FetchHopsForAttemptsRow,
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord,
firstHopAmountMsat int64, totalTimeLock int32, totalAmount int64,
sourceKey []byte, firstHopWireCustomRecords lnwire.CustomRecords) (
*route.Route, error) {
if len(hops) == 0 {
return nil, fmt.Errorf("no hops provided")
}
// Hops are already sorted by hop_index from the SQL query.
routeHops := make([]*route.Hop, len(hops))
for i, hop := range hops {
pubKey, err := route.NewVertexFromBytes(hop.PubKey)
if err != nil {
return nil, fmt.Errorf("failed to parse pub key: %w",
err)
}
var channelID uint64
if hop.Scid != "" {
// The SCID is stored as a string representation
// of the uint64.
var err error
channelID, err = strconv.ParseUint(hop.Scid, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse "+
"scid: %w", err)
}
}
routeHop := &route.Hop{
PubKeyBytes: pubKey,
ChannelID: channelID,
OutgoingTimeLock: uint32(hop.OutgoingTimeLock),
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForward),
}
// Add MPP record if present.
if len(hop.MppPaymentAddr) > 0 {
var paymentAddr [32]byte
copy(paymentAddr[:], hop.MppPaymentAddr)
routeHop.MPP = record.NewMPP(
lnwire.MilliSatoshi(hop.MppTotalMsat.Int64),
paymentAddr,
)
}
// Add AMP record if present.
if len(hop.AmpRootShare) > 0 {
var rootShare [32]byte
copy(rootShare[:], hop.AmpRootShare)
var setID [32]byte
copy(setID[:], hop.AmpSetID)
routeHop.AMP = record.NewAMP(
rootShare, setID,
uint32(hop.AmpChildIndex.Int32),
)
}
// Add blinding point if present (only for introduction node).
if len(hop.BlindingPoint) > 0 {
pubKey, err := btcec.ParsePubKey(hop.BlindingPoint)
if err != nil {
return nil, fmt.Errorf("failed to parse "+
"blinding point: %w", err)
}
routeHop.BlindingPoint = pubKey
}
// Add encrypted data if present (for all blinded hops).
if len(hop.EncryptedData) > 0 {
routeHop.EncryptedData = hop.EncryptedData
}
// Add total amount if present (only for final hop in blinded
// route).
if hop.BlindedPathTotalAmt.Valid {
routeHop.TotalAmtMsat = lnwire.MilliSatoshi(
hop.BlindedPathTotalAmt.Int64,
)
}
// Add hop-level custom records.
if records, ok := hopCustomRecords[hop.ID]; ok {
routeHop.CustomRecords = make(
record.CustomSet,
)
for _, rec := range records {
routeHop.CustomRecords[uint64(rec.Key)] =
rec.Value
}
}
// Add metadata if present.
if len(hop.MetaData) > 0 {
routeHop.Metadata = hop.MetaData
}
routeHops[i] = routeHop
}
// Parse the source node public key.
var sourceNode route.Vertex
copy(sourceNode[:], sourceKey)
route := &route.Route{
TotalTimeLock: uint32(totalTimeLock),
TotalAmount: lnwire.MilliSatoshi(totalAmount),
SourcePubKey: sourceNode,
Hops: routeHops,
FirstHopWireCustomRecords: firstHopWireCustomRecords,
}
// Set the first hop amount if it is set.
if firstHopAmountMsat != 0 {
route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
tlv.NewBigSizeT(lnwire.MilliSatoshi(
firstHopAmountMsat,
)),
)
}
return route, nil
}
+569
View File
@@ -2,14 +2,40 @@ package paymentsdb
import (
"context"
"errors"
"fmt"
"math"
"time"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
)
// PaymentIntentType represents the type of payment intent.
type PaymentIntentType int16
const (
// PaymentIntentTypeBolt11 indicates a BOLT11 invoice payment.
PaymentIntentTypeBolt11 PaymentIntentType = 0
)
// HTLCAttemptResolutionType represents the type of HTLC attempt resolution.
type HTLCAttemptResolutionType int32
const (
// HTLCAttemptResolutionSettled indicates the HTLC attempt was settled
// successfully with a preimage.
HTLCAttemptResolutionSettled HTLCAttemptResolutionType = 1
// HTLCAttemptResolutionFailed indicates the HTLC attempt failed.
HTLCAttemptResolutionFailed HTLCAttemptResolutionType = 2
)
// SQLQueries is a subset of the sqlc.Querier interface that can be used to
// execute queries against the SQL payments tables.
//
//nolint:ll
type SQLQueries interface {
/*
Payment DB read operations.
@@ -83,3 +109,546 @@ func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLQueries,
// A compile-time constraint to ensure SQLStore implements DB.
var _ DB = (*SQLStore)(nil)
// fetchPaymentWithCompleteData fetches a payment with all its related data
// including attempts, hops, and custom records from the database.
// This is a convenience wrapper around the batch loading functions for single
// payment operations.
func (s *SQLStore) fetchPaymentWithCompleteData(ctx context.Context,
db SQLQueries, dbPayment sqlc.PaymentAndIntent) (*MPPayment, error) {
payment := dbPayment.GetPayment()
// Load batch data for this single payment.
batchData, err := s.loadPaymentsBatchData(ctx, db, []int64{payment.ID})
if err != nil {
return nil, fmt.Errorf("failed to load batch data: %w", err)
}
// Build the payment from the batch data.
return s.buildPaymentFromBatchData(dbPayment, batchData)
}
// paymentsBatchData holds all the batch-loaded data for multiple payments.
type paymentsBatchData struct {
// paymentCustomRecords maps payment ID to its custom records.
paymentCustomRecords map[int64][]sqlc.PaymentFirstHopCustomRecord
// attempts maps payment ID to its HTLC attempts.
attempts map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow
// hopsByAttempt maps attempt index to its hops.
hopsByAttempt map[int64][]sqlc.FetchHopsForAttemptsRow
// hopCustomRecords maps hop ID to its custom records.
hopCustomRecords map[int64][]sqlc.PaymentHopCustomRecord
// routeCustomRecords maps attempt index to its route-level custom
// records.
routeCustomRecords map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord
}
// loadPaymentCustomRecords loads payment-level custom records for a given
// set of payment IDs.
func (s *SQLStore) loadPaymentCustomRecords(ctx context.Context,
db SQLQueries, paymentIDs []int64,
batchData *paymentsBatchData) error {
return sqldb.ExecuteBatchQuery(
ctx, s.cfg.QueryCfg, paymentIDs,
func(id int64) int64 { return id },
func(ctx context.Context, ids []int64) (
[]sqlc.PaymentFirstHopCustomRecord, error) {
//nolint:ll
records, err := db.FetchPaymentLevelFirstHopCustomRecords(
ctx, ids,
)
return records, err
},
func(ctx context.Context,
record sqlc.PaymentFirstHopCustomRecord) error {
paymentRecords :=
batchData.paymentCustomRecords[record.PaymentID]
batchData.paymentCustomRecords[record.PaymentID] =
append(paymentRecords, record)
return nil
},
)
}
// loadHtlcAttempts loads HTLC attempts for all payments and returns all
// attempt indices.
func (s *SQLStore) loadHtlcAttempts(ctx context.Context, db SQLQueries,
paymentIDs []int64, batchData *paymentsBatchData) ([]int64, error) {
var allAttemptIndices []int64
err := sqldb.ExecuteBatchQuery(
ctx, s.cfg.QueryCfg, paymentIDs,
func(id int64) int64 { return id },
func(ctx context.Context, ids []int64) (
[]sqlc.FetchHtlcAttemptsForPaymentsRow, error) {
return db.FetchHtlcAttemptsForPayments(ctx, ids)
},
func(ctx context.Context,
attempt sqlc.FetchHtlcAttemptsForPaymentsRow) error {
batchData.attempts[attempt.PaymentID] = append(
batchData.attempts[attempt.PaymentID], attempt,
)
allAttemptIndices = append(
allAttemptIndices, attempt.AttemptIndex,
)
return nil
},
)
return allAttemptIndices, err
}
// loadHopsForAttempts loads hops for all attempts and returns all hop IDs.
func (s *SQLStore) loadHopsForAttempts(ctx context.Context, db SQLQueries,
attemptIndices []int64, batchData *paymentsBatchData) ([]int64, error) {
var hopIDs []int64
err := sqldb.ExecuteBatchQuery(
ctx, s.cfg.QueryCfg, attemptIndices,
func(idx int64) int64 { return idx },
func(ctx context.Context, indices []int64) (
[]sqlc.FetchHopsForAttemptsRow, error) {
return db.FetchHopsForAttempts(ctx, indices)
},
func(ctx context.Context,
hop sqlc.FetchHopsForAttemptsRow) error {
attemptHops :=
batchData.hopsByAttempt[hop.HtlcAttemptIndex]
batchData.hopsByAttempt[hop.HtlcAttemptIndex] =
append(attemptHops, hop)
hopIDs = append(hopIDs, hop.ID)
return nil
},
)
return hopIDs, err
}
// loadHopCustomRecords loads hop-level custom records for all hops.
func (s *SQLStore) loadHopCustomRecords(ctx context.Context, db SQLQueries,
hopIDs []int64, batchData *paymentsBatchData) error {
return sqldb.ExecuteBatchQuery(
ctx, s.cfg.QueryCfg, hopIDs,
func(id int64) int64 { return id },
func(ctx context.Context, ids []int64) (
[]sqlc.PaymentHopCustomRecord, error) {
return db.FetchHopLevelCustomRecords(ctx, ids)
},
func(ctx context.Context,
record sqlc.PaymentHopCustomRecord) error {
// TODO(ziggie): Can we get rid of this?
// This has to be in place otherwise the
// comparison will not match.
if record.Value == nil {
record.Value = []byte{}
}
batchData.hopCustomRecords[record.HopID] = append(
batchData.hopCustomRecords[record.HopID],
record,
)
return nil
},
)
}
// loadRouteCustomRecords loads route-level first hop custom records for all
// attempts.
func (s *SQLStore) loadRouteCustomRecords(ctx context.Context, db SQLQueries,
attemptIndices []int64, batchData *paymentsBatchData) error {
return sqldb.ExecuteBatchQuery(
ctx, s.cfg.QueryCfg, attemptIndices,
func(idx int64) int64 { return idx },
func(ctx context.Context, indices []int64) (
[]sqlc.PaymentAttemptFirstHopCustomRecord, error) {
return db.FetchRouteLevelFirstHopCustomRecords(
ctx, indices,
)
},
func(ctx context.Context,
record sqlc.PaymentAttemptFirstHopCustomRecord) error {
idx := record.HtlcAttemptIndex
attemptRecords := batchData.routeCustomRecords[idx]
batchData.routeCustomRecords[idx] =
append(attemptRecords, record)
return nil
},
)
}
// loadPaymentsBatchData loads all related data for multiple payments in batch.
func (s *SQLStore) loadPaymentsBatchData(ctx context.Context, db SQLQueries,
paymentIDs []int64) (*paymentsBatchData, error) {
batchData := &paymentsBatchData{
paymentCustomRecords: make(
map[int64][]sqlc.PaymentFirstHopCustomRecord,
),
attempts: make(
map[int64][]sqlc.FetchHtlcAttemptsForPaymentsRow,
),
hopsByAttempt: make(
map[int64][]sqlc.FetchHopsForAttemptsRow,
),
hopCustomRecords: make(
map[int64][]sqlc.PaymentHopCustomRecord,
),
routeCustomRecords: make(
map[int64][]sqlc.PaymentAttemptFirstHopCustomRecord,
),
}
if len(paymentIDs) == 0 {
return batchData, nil
}
// Load payment-level custom records.
err := s.loadPaymentCustomRecords(ctx, db, paymentIDs, batchData)
if err != nil {
return nil, fmt.Errorf("failed to fetch payment custom "+
"records: %w", err)
}
// Load HTLC attempts and collect attempt indices.
allAttemptIndices, err := s.loadHtlcAttempts(
ctx, db, paymentIDs, batchData,
)
if err != nil {
return nil, fmt.Errorf("failed to fetch HTLC attempts: %w",
err)
}
if len(allAttemptIndices) == 0 {
// No attempts, return early.
return batchData, nil
}
// Load hops for all attempts and collect hop IDs.
hopIDs, err := s.loadHopsForAttempts(
ctx, db, allAttemptIndices, batchData,
)
if err != nil {
return nil, fmt.Errorf("failed to fetch hops for attempts: %w",
err)
}
// Load hop-level custom records if there are any hops.
if len(hopIDs) > 0 {
err = s.loadHopCustomRecords(ctx, db, hopIDs, batchData)
if err != nil {
return nil, fmt.Errorf("failed to fetch hop custom "+
"records: %w", err)
}
}
// Load route-level first hop custom records.
err = s.loadRouteCustomRecords(ctx, db, allAttemptIndices, batchData)
if err != nil {
return nil, fmt.Errorf("failed to fetch route custom "+
"records: %w", err)
}
return batchData, nil
}
// buildPaymentFromBatchData builds a complete MPPayment from a database payment
// and pre-loaded batch data.
func (s *SQLStore) buildPaymentFromBatchData(dbPayment sqlc.PaymentAndIntent,
batchData *paymentsBatchData) (*MPPayment, error) {
// The query will only return BOLT 11 payment intents or intents with
// no intent type set.
paymentIntent := dbPayment.GetPaymentIntent()
paymentRequest := paymentIntent.IntentPayload
payment := dbPayment.GetPayment()
// Get payment-level custom records from batch data.
customRecords := batchData.paymentCustomRecords[payment.ID]
// Convert to the FirstHopCustomRecords map.
var firstHopCustomRecords lnwire.CustomRecords
if len(customRecords) > 0 {
firstHopCustomRecords = make(lnwire.CustomRecords)
for _, record := range customRecords {
firstHopCustomRecords[uint64(record.Key)] = record.Value
}
}
// Convert database payment data to the PaymentCreationInfo struct.
info := dbPaymentToCreationInfo(
payment.PaymentIdentifier, payment.AmountMsat,
payment.CreatedAt, paymentRequest, firstHopCustomRecords,
)
// Get all HTLC attempts from batch data for a given payment.
dbAttempts := batchData.attempts[payment.ID]
// Convert all attempts to HTLCAttempt structs using the pre-loaded
// batch data.
attempts := make([]HTLCAttempt, 0, len(dbAttempts))
for _, dbAttempt := range dbAttempts {
attemptIndex := dbAttempt.AttemptIndex
// Convert the batch row type to the single row type.
attempt, err := dbAttemptToHTLCAttempt(
dbAttempt, batchData.hopsByAttempt[attemptIndex],
batchData.hopCustomRecords,
batchData.routeCustomRecords[attemptIndex],
)
if err != nil {
return nil, fmt.Errorf("failed to convert attempt "+
"%d: %w", attemptIndex, err)
}
attempts = append(attempts, *attempt)
}
// Set the failure reason if present.
//
// TODO(ziggie): Rename it to Payment Memo in the database?
var failureReason *FailureReason
if payment.FailReason.Valid {
reason := FailureReason(payment.FailReason.Int32)
failureReason = &reason
}
mpPayment := &MPPayment{
SequenceNum: uint64(payment.ID),
Info: info,
HTLCs: attempts,
FailureReason: failureReason,
}
// The status and state will be determined by calling
// SetState after construction.
if err := mpPayment.SetState(); err != nil {
return nil, fmt.Errorf("failed to set payment state: %w", err)
}
return mpPayment, nil
}
// QueryPayments queries and retrieves payments from the database with support
// for filtering, pagination, and efficient batch loading of related data.
//
// The function accepts a Query parameter that controls:
// - Pagination: IndexOffset specifies where to start (exclusive), and
// MaxPayments limits the number of results returned
// - Ordering: Reversed flag determines if results are returned in reverse
// chronological order
// - Filtering: CreationDateStart/End filter by creation time, and
// IncludeIncomplete controls whether non-succeeded payments are included
// - Metadata: CountTotal flag determines if the total payment count should
// be calculated
//
// The function optimizes performance by loading all related data (HTLCs,
// sequences, failure reasons, etc.) for multiple payments in a single batch
// query, rather than fetching each payment's data individually.
//
// Returns a Response containing:
// - Payments: the list of matching payments with complete data
// - FirstIndexOffset/LastIndexOffset: pagination cursors for the first and
// last payment in the result set
// - TotalCount: total number of payments in the database (if CountTotal was
// requested, otherwise 0)
//
// This is part of the DB interface.
func (s *SQLStore) QueryPayments(ctx context.Context, query Query) (Response,
error) {
if query.MaxPayments == 0 {
return Response{}, fmt.Errorf("max payments must be non-zero")
}
var (
allPayments []*MPPayment
totalCount int64
initialCursor int64
)
extractCursor := func(
row sqlc.FilterPaymentsRow) int64 {
return row.Payment.ID
}
err := s.db.ExecTx(ctx, sqldb.ReadTxOpt(), func(db SQLQueries) error {
// We first count all payments to determine the total count
// if requested.
if query.CountTotal {
totalPayments, err := db.CountPayments(ctx)
if err != nil {
return fmt.Errorf("failed to count "+
"payments: %w", err)
}
totalCount = totalPayments
}
// collectFunc extracts the payment ID from each payment row.
collectFunc := func(row sqlc.FilterPaymentsRow) (int64,
error) {
return row.Payment.ID, nil
}
// batchDataFunc loads all related data for a batch of payments.
batchDataFunc := func(ctx context.Context, paymentIDs []int64) (
*paymentsBatchData, error) {
return s.loadPaymentsBatchData(ctx, db, paymentIDs)
}
// processPayment processes each payment with the batch-loaded
// data.
processPayment := func(ctx context.Context,
dbPayment sqlc.FilterPaymentsRow,
batchData *paymentsBatchData) error {
// Build the payment from the pre-loaded batch data.
mpPayment, err := s.buildPaymentFromBatchData(
dbPayment, batchData,
)
if err != nil {
return fmt.Errorf("failed to fetch payment "+
"with complete data: %w", err)
}
// To keep compatibility with the old API, we only
// return non-succeeded payments if requested.
if mpPayment.Status != StatusSucceeded &&
!query.IncludeIncomplete {
return nil
}
if uint64(len(allPayments)) >= query.MaxPayments {
return errMaxPaymentsReached
}
allPayments = append(allPayments, mpPayment)
return nil
}
queryFunc := func(ctx context.Context, lastID int64,
limit int32) ([]sqlc.FilterPaymentsRow, error) {
filterParams := sqlc.FilterPaymentsParams{
NumLimit: limit,
Reverse: query.Reversed,
// For now there only BOLT 11 payment intents
// exist.
IntentType: sqldb.SQLInt16(
PaymentIntentTypeBolt11,
),
}
if query.Reversed {
filterParams.IndexOffsetLet = sqldb.SQLInt64(
lastID,
)
} else {
filterParams.IndexOffsetGet = sqldb.SQLInt64(
lastID,
)
}
// Add potential date filters if specified.
if query.CreationDateStart != 0 {
filterParams.CreatedAfter = sqldb.SQLTime(
time.Unix(query.CreationDateStart, 0).
UTC(),
)
}
if query.CreationDateEnd != 0 {
filterParams.CreatedBefore = sqldb.SQLTime(
time.Unix(query.CreationDateEnd, 0).
UTC(),
)
}
return db.FilterPayments(ctx, filterParams)
}
if query.Reversed {
if query.IndexOffset == 0 {
initialCursor = int64(math.MaxInt64)
} else {
initialCursor = int64(query.IndexOffset)
}
} else {
initialCursor = int64(query.IndexOffset)
}
return sqldb.ExecuteCollectAndBatchWithSharedDataQuery(
ctx, s.cfg.QueryCfg, initialCursor, queryFunc,
extractCursor, collectFunc, batchDataFunc,
processPayment,
)
}, func() {
allPayments = nil
})
// We make sure we don't return an error if we reached the maximum
// number of payments. Which is the pagination limit for the query
// itself.
if err != nil && !errors.Is(err, errMaxPaymentsReached) {
return Response{}, fmt.Errorf("failed to query payments: %w",
err)
}
// Handle case where no payments were found
if len(allPayments) == 0 {
return Response{
Payments: allPayments,
FirstIndexOffset: 0,
LastIndexOffset: 0,
TotalCount: uint64(totalCount),
}, nil
}
// If the query was reversed, we need to reverse the payment list
// to match the kvstore behavior and return payments in forward order.
if query.Reversed {
for i, j := 0, len(allPayments)-1; i < j; i, j = i+1, j-1 {
allPayments[i], allPayments[j] = allPayments[j],
allPayments[i]
}
}
return Response{
Payments: allPayments,
FirstIndexOffset: allPayments[0].SequenceNum,
LastIndexOffset: allPayments[len(allPayments)-1].SequenceNum,
TotalCount: uint64(totalCount),
}, nil
}
+76
View File
@@ -167,3 +167,79 @@ func (r GetChannelsBySCIDRangeRow) Node1Pub() []byte {
func (r GetChannelsBySCIDRangeRow) Node2Pub() []byte {
return r.Node2PubKey
}
// PaymentAndIntent is an interface that provides access to a payment and its
// associated payment intent.
type PaymentAndIntent interface {
// GetPayment returns the Payment associated with this interface.
GetPayment() Payment
// GetPaymentIntent returns the PaymentIntent associated with this payment.
GetPaymentIntent() PaymentIntent
}
// GetPayment returns the Payment associated with this interface.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FilterPaymentsRow) GetPayment() Payment {
return r.Payment
}
// GetPaymentIntent returns the PaymentIntent associated with this payment.
// If the payment has no intent (IntentType is NULL), this returns a zero-value
// PaymentIntent.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FilterPaymentsRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}
// GetPayment returns the Payment associated with this interface.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentRow) GetPayment() Payment {
return r.Payment
}
// GetPaymentIntent returns the PaymentIntent associated with this payment.
// If the payment has no intent (IntentType is NULL), this returns a zero-value
// PaymentIntent.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}
// GetPayment returns the Payment associated with this interface.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentsByIDsRow) GetPayment() Payment {
return r.Payment
}
// GetPaymentIntent returns the PaymentIntent associated with this payment.
// If the payment has no intent (IntentType is NULL), this returns a zero-value
// PaymentIntent.
//
// NOTE: This method is part of the PaymentAndIntent interface.
func (r FetchPaymentsByIDsRow) GetPaymentIntent() PaymentIntent {
if !r.IntentType.Valid {
return PaymentIntent{}
}
return PaymentIntent{
IntentType: r.IntentType.Int16,
IntentPayload: r.IntentPayload,
}
}