• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 14562617218

20 Apr 2025 07:24PM UTC coverage: 58.549% (-0.03%) from 58.575%
14562617218

Pull #9744

github

web-flow
Merge d204fa44b into cb481df81
Pull Request #9744: [bug]: don't warn user about existing bbolt DB if SQLite files exist

0 of 35 new or added lines in 1 file covered. (0.0%)

520 existing lines in 12 files now uncovered.

97338 of 166250 relevant lines covered (58.55%)

1.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/invoices/sql_store.go
1
package invoices
2

3
import (
4
        "context"
5
        "crypto/sha256"
6
        "database/sql"
7
        "errors"
8
        "fmt"
9
        "math"
10
        "strconv"
11
        "time"
12

13
        "github.com/davecgh/go-spew/spew"
14
        "github.com/lightningnetwork/lnd/clock"
15
        "github.com/lightningnetwork/lnd/graph/db/models"
16
        "github.com/lightningnetwork/lnd/lntypes"
17
        "github.com/lightningnetwork/lnd/lnwire"
18
        "github.com/lightningnetwork/lnd/record"
19
        "github.com/lightningnetwork/lnd/sqldb"
20
        "github.com/lightningnetwork/lnd/sqldb/sqlc"
21
)
22

23
const (
24
        // defaultQueryPaginationLimit is used in the LIMIT clause of the SQL
25
        // queries to limit the number of rows returned.
26
        defaultQueryPaginationLimit = 100
27

28
        // invoiceScanBatchSize is the number we use limiting the logging output
29
        // when scanning invoices.
30
        invoiceScanBatchSize = 1000
31
)
32

33
// SQLInvoiceQueries is an interface that defines the set of operations that can
34
// be executed against the invoice SQL database.
35
type SQLInvoiceQueries interface { //nolint:interfacebloat
36
        InsertInvoice(ctx context.Context, arg sqlc.InsertInvoiceParams) (int64,
37
                error)
38

39
        // TODO(bhandras): remove this once migrations have been separated out.
40
        InsertMigratedInvoice(ctx context.Context,
41
                arg sqlc.InsertMigratedInvoiceParams) (int64, error)
42

43
        InsertInvoiceFeature(ctx context.Context,
44
                arg sqlc.InsertInvoiceFeatureParams) error
45

46
        InsertInvoiceHTLC(ctx context.Context,
47
                arg sqlc.InsertInvoiceHTLCParams) (int64, error)
48

49
        InsertInvoiceHTLCCustomRecord(ctx context.Context,
50
                arg sqlc.InsertInvoiceHTLCCustomRecordParams) error
51

52
        FilterInvoices(ctx context.Context,
53
                arg sqlc.FilterInvoicesParams) ([]sqlc.Invoice, error)
54

55
        GetInvoice(ctx context.Context,
56
                arg sqlc.GetInvoiceParams) ([]sqlc.Invoice, error)
57

58
        GetInvoiceByHash(ctx context.Context, hash []byte) (sqlc.Invoice,
59
                error)
60

61
        GetInvoiceBySetID(ctx context.Context, setID []byte) ([]sqlc.Invoice,
62
                error)
63

64
        GetInvoiceFeatures(ctx context.Context,
65
                invoiceID int64) ([]sqlc.InvoiceFeature, error)
66

67
        GetInvoiceHTLCCustomRecords(ctx context.Context,
68
                invoiceID int64) ([]sqlc.GetInvoiceHTLCCustomRecordsRow, error)
69

70
        GetInvoiceHTLCs(ctx context.Context,
71
                invoiceID int64) ([]sqlc.InvoiceHtlc, error)
72

73
        UpdateInvoiceState(ctx context.Context,
74
                arg sqlc.UpdateInvoiceStateParams) (sql.Result, error)
75

76
        UpdateInvoiceAmountPaid(ctx context.Context,
77
                arg sqlc.UpdateInvoiceAmountPaidParams) (sql.Result, error)
78

79
        NextInvoiceSettleIndex(ctx context.Context) (int64, error)
80

81
        UpdateInvoiceHTLC(ctx context.Context,
82
                arg sqlc.UpdateInvoiceHTLCParams) error
83

84
        DeleteInvoice(ctx context.Context, arg sqlc.DeleteInvoiceParams) (
85
                sql.Result, error)
86

87
        DeleteCanceledInvoices(ctx context.Context) (sql.Result, error)
88

89
        // AMP sub invoice specific methods.
90
        UpsertAMPSubInvoice(ctx context.Context,
91
                arg sqlc.UpsertAMPSubInvoiceParams) (sql.Result, error)
92

93
        // TODO(bhandras): remove this once migrations have been separated out.
94
        InsertAMPSubInvoice(ctx context.Context,
95
                arg sqlc.InsertAMPSubInvoiceParams) error
96

97
        UpdateAMPSubInvoiceState(ctx context.Context,
98
                arg sqlc.UpdateAMPSubInvoiceStateParams) error
99

100
        InsertAMPSubInvoiceHTLC(ctx context.Context,
101
                arg sqlc.InsertAMPSubInvoiceHTLCParams) error
102

103
        FetchAMPSubInvoices(ctx context.Context,
104
                arg sqlc.FetchAMPSubInvoicesParams) ([]sqlc.AmpSubInvoice,
105
                error)
106

107
        FetchAMPSubInvoiceHTLCs(ctx context.Context,
108
                arg sqlc.FetchAMPSubInvoiceHTLCsParams) (
109
                []sqlc.FetchAMPSubInvoiceHTLCsRow, error)
110

111
        FetchSettledAMPSubInvoices(ctx context.Context,
112
                arg sqlc.FetchSettledAMPSubInvoicesParams) (
113
                []sqlc.FetchSettledAMPSubInvoicesRow, error)
114

115
        UpdateAMPSubInvoiceHTLCPreimage(ctx context.Context,
116
                arg sqlc.UpdateAMPSubInvoiceHTLCPreimageParams) (sql.Result,
117
                error)
118

119
        // Invoice events specific methods.
120
        OnInvoiceCreated(ctx context.Context,
121
                arg sqlc.OnInvoiceCreatedParams) error
122

123
        OnInvoiceCanceled(ctx context.Context,
124
                arg sqlc.OnInvoiceCanceledParams) error
125

126
        OnInvoiceSettled(ctx context.Context,
127
                arg sqlc.OnInvoiceSettledParams) error
128

129
        OnAMPSubInvoiceCreated(ctx context.Context,
130
                arg sqlc.OnAMPSubInvoiceCreatedParams) error
131

132
        OnAMPSubInvoiceCanceled(ctx context.Context,
133
                arg sqlc.OnAMPSubInvoiceCanceledParams) error
134

135
        OnAMPSubInvoiceSettled(ctx context.Context,
136
                arg sqlc.OnAMPSubInvoiceSettledParams) error
137

138
        // Migration specific methods.
139
        // TODO(bhandras): remove this once migrations have been separated out.
140
        InsertKVInvoiceKeyAndAddIndex(ctx context.Context,
141
                arg sqlc.InsertKVInvoiceKeyAndAddIndexParams) error
142

143
        SetKVInvoicePaymentHash(ctx context.Context,
144
                arg sqlc.SetKVInvoicePaymentHashParams) error
145

146
        GetKVInvoicePaymentHashByAddIndex(ctx context.Context, addIndex int64) (
147
                []byte, error)
148

149
        ClearKVInvoiceHashIndex(ctx context.Context) error
150
}
151

152
var _ InvoiceDB = (*SQLStore)(nil)
153

154
// SQLInvoiceQueriesTxOptions defines the set of db txn options the
155
// SQLInvoiceQueries understands.
156
type SQLInvoiceQueriesTxOptions struct {
157
        // readOnly governs if a read only transaction is needed or not.
158
        readOnly bool
159
}
160

161
// ReadOnly returns true if the transaction should be read only.
162
//
163
// NOTE: This implements the TxOptions.
164
func (a *SQLInvoiceQueriesTxOptions) ReadOnly() bool {
×
165
        return a.readOnly
×
166
}
×
167

168
// NewSQLInvoiceQueryReadTx creates a new read transaction option set.
169
func NewSQLInvoiceQueryReadTx() SQLInvoiceQueriesTxOptions {
×
170
        return SQLInvoiceQueriesTxOptions{
×
171
                readOnly: true,
×
172
        }
×
173
}
×
174

175
// BatchedSQLInvoiceQueries is a version of the SQLInvoiceQueries that's capable
176
// of batched database operations.
177
type BatchedSQLInvoiceQueries interface {
178
        SQLInvoiceQueries
179

180
        sqldb.BatchedTx[SQLInvoiceQueries]
181
}
182

183
// SQLStore represents a storage backend.
184
type SQLStore struct {
185
        db    BatchedSQLInvoiceQueries
186
        clock clock.Clock
187
        opts  SQLStoreOptions
188
}
189

190
// SQLStoreOptions holds the options for the SQL store.
191
type SQLStoreOptions struct {
192
        paginationLimit int
193
}
194

195
// defaultSQLStoreOptions returns the default options for the SQL store.
196
func defaultSQLStoreOptions() SQLStoreOptions {
×
197
        return SQLStoreOptions{
×
198
                paginationLimit: defaultQueryPaginationLimit,
×
199
        }
×
200
}
×
201

202
// SQLStoreOption is a functional option that can be used to optionally modify
203
// the behavior of the SQL store.
204
type SQLStoreOption func(*SQLStoreOptions)
205

206
// WithPaginationLimit sets the pagination limit for the SQL store queries that
207
// paginate results.
208
func WithPaginationLimit(limit int) SQLStoreOption {
×
209
        return func(o *SQLStoreOptions) {
×
210
                o.paginationLimit = limit
×
211
        }
×
212
}
213

214
// NewSQLStore creates a new SQLStore instance given a open
215
// BatchedSQLInvoiceQueries storage backend.
216
func NewSQLStore(db BatchedSQLInvoiceQueries,
217
        clock clock.Clock, options ...SQLStoreOption) *SQLStore {
×
218

×
219
        opts := defaultSQLStoreOptions()
×
220
        for _, applyOption := range options {
×
221
                applyOption(&opts)
×
222
        }
×
223

224
        return &SQLStore{
×
225
                db:    db,
×
226
                clock: clock,
×
227
                opts:  opts,
×
228
        }
×
229
}
230

231
func makeInsertInvoiceParams(invoice *Invoice, paymentHash lntypes.Hash) (
232
        sqlc.InsertInvoiceParams, error) {
×
233

×
234
        // Precompute the payment request hash so we can use it in the query.
×
235
        var paymentRequestHash []byte
×
236
        if len(invoice.PaymentRequest) > 0 {
×
237
                h := sha256.New()
×
238
                h.Write(invoice.PaymentRequest)
×
239
                paymentRequestHash = h.Sum(nil)
×
240
        }
×
241

242
        params := sqlc.InsertInvoiceParams{
×
243
                Hash:       paymentHash[:],
×
244
                AmountMsat: int64(invoice.Terms.Value),
×
245
                CltvDelta: sqldb.SQLInt32(
×
246
                        invoice.Terms.FinalCltvDelta,
×
247
                ),
×
248
                Expiry: int32(invoice.Terms.Expiry.Seconds()),
×
249
                // Note: keysend invoices don't have a payment request.
×
250
                PaymentRequest: sqldb.SQLStr(string(
×
251
                        invoice.PaymentRequest),
×
252
                ),
×
253
                PaymentRequestHash: paymentRequestHash,
×
254
                State:              int16(invoice.State),
×
255
                AmountPaidMsat:     int64(invoice.AmtPaid),
×
256
                IsAmp:              invoice.IsAMP(),
×
257
                IsHodl:             invoice.HodlInvoice,
×
258
                IsKeysend:          invoice.IsKeysend(),
×
259
                CreatedAt:          invoice.CreationDate.UTC(),
×
260
        }
×
261

×
262
        if invoice.Memo != nil {
×
263
                // Store the memo as a nullable string in the database. Note
×
264
                // that for compatibility reasons, we store the value as a valid
×
265
                // string even if it's empty.
×
266
                params.Memo = sql.NullString{
×
267
                        String: string(invoice.Memo),
×
268
                        Valid:  true,
×
269
                }
×
270
        }
×
271

272
        // Some invoices may not have a preimage, like in the case of HODL
273
        // invoices.
274
        if invoice.Terms.PaymentPreimage != nil {
×
275
                preimage := *invoice.Terms.PaymentPreimage
×
276
                if preimage == UnknownPreimage {
×
277
                        return sqlc.InsertInvoiceParams{},
×
278
                                errors.New("cannot use all-zeroes preimage")
×
279
                }
×
280
                params.Preimage = preimage[:]
×
281
        }
282

283
        // Some non MPP payments may have the default (invalid) value.
284
        if invoice.Terms.PaymentAddr != BlankPayAddr {
×
285
                params.PaymentAddr = invoice.Terms.PaymentAddr[:]
×
286
        }
×
287

288
        return params, nil
×
289
}
290

291
// AddInvoice inserts the targeted invoice into the database. If the invoice has
292
// *any* payment hashes which already exists within the database, then the
293
// insertion will be aborted and rejected due to the strict policy banning any
294
// duplicate payment hashes.
295
//
296
// NOTE: A side effect of this function is that it sets AddIndex on newInvoice.
297
func (i *SQLStore) AddInvoice(ctx context.Context,
298
        newInvoice *Invoice, paymentHash lntypes.Hash) (uint64, error) {
×
299

×
300
        // Make sure this is a valid invoice before trying to store it in our
×
301
        // DB.
×
302
        if err := ValidateInvoice(newInvoice, paymentHash); err != nil {
×
303
                return 0, err
×
304
        }
×
305

306
        var (
×
307
                writeTxOpts SQLInvoiceQueriesTxOptions
×
308
                invoiceID   int64
×
309
        )
×
310

×
311
        insertInvoiceParams, err := makeInsertInvoiceParams(
×
312
                newInvoice, paymentHash,
×
313
        )
×
314
        if err != nil {
×
315
                return 0, err
×
316
        }
×
317

318
        err = i.db.ExecTx(ctx, &writeTxOpts, func(db SQLInvoiceQueries) error {
×
319
                var err error
×
320
                invoiceID, err = db.InsertInvoice(ctx, insertInvoiceParams)
×
321
                if err != nil {
×
322
                        return fmt.Errorf("unable to insert invoice: %w", err)
×
323
                }
×
324

325
                // TODO(positiveblue): if invocies do not have custom features
326
                // maybe just store the "invoice type" and populate the features
327
                // based on that.
328
                for feature := range newInvoice.Terms.Features.Features() {
×
329
                        params := sqlc.InsertInvoiceFeatureParams{
×
330
                                InvoiceID: invoiceID,
×
331
                                Feature:   int32(feature),
×
332
                        }
×
333

×
334
                        err := db.InsertInvoiceFeature(ctx, params)
×
335
                        if err != nil {
×
336
                                return fmt.Errorf("unable to insert invoice "+
×
337
                                        "feature(%v): %w", feature, err)
×
338
                        }
×
339
                }
340

341
                // Finally add a new event for this invoice.
342
                return db.OnInvoiceCreated(ctx, sqlc.OnInvoiceCreatedParams{
×
343
                        AddedAt:   newInvoice.CreationDate.UTC(),
×
344
                        InvoiceID: invoiceID,
×
345
                })
×
346
        }, func() {})
×
347
        if err != nil {
×
348
                mappedSQLErr := sqldb.MapSQLError(err)
×
349
                var uniqueConstraintErr *sqldb.ErrSQLUniqueConstraintViolation
×
350
                if errors.As(mappedSQLErr, &uniqueConstraintErr) {
×
351
                        // Add context to unique constraint errors.
×
352
                        return 0, ErrDuplicateInvoice
×
353
                }
×
354

355
                return 0, fmt.Errorf("unable to add invoice(%v): %w",
×
356
                        paymentHash, err)
×
357
        }
358

359
        newInvoice.AddIndex = uint64(invoiceID)
×
360

×
361
        return newInvoice.AddIndex, nil
×
362
}
363

364
// getInvoiceByRef fetches the invoice with the given reference. The reference
365
// may be a payment hash, a payment address, or a set ID for an AMP sub invoice.
366
func getInvoiceByRef(ctx context.Context,
367
        db SQLInvoiceQueries, ref InvoiceRef) (sqlc.Invoice, error) {
×
368

×
369
        // If the reference is empty, we can't look up the invoice.
×
370
        if ref.PayHash() == nil && ref.PayAddr() == nil && ref.SetID() == nil {
×
371
                return sqlc.Invoice{}, ErrInvoiceNotFound
×
372
        }
×
373

374
        // If the reference is a hash only, we can look up the invoice directly
375
        // by the payment hash which is faster.
376
        if ref.IsHashOnly() {
×
377
                invoice, err := db.GetInvoiceByHash(ctx, ref.PayHash()[:])
×
378
                if errors.Is(err, sql.ErrNoRows) {
×
379
                        return sqlc.Invoice{}, ErrInvoiceNotFound
×
380
                }
×
381

382
                return invoice, err
×
383
        }
384

385
        // Otherwise the reference may include more fields, so we'll need to
386
        // assemble the query parameters based on the fields that are set.
387
        var params sqlc.GetInvoiceParams
×
388

×
389
        if ref.PayHash() != nil {
×
390
                params.Hash = ref.PayHash()[:]
×
391
        }
×
392

393
        // Newer invoices (0.11 and up) are indexed by payment address in
394
        // addition to payment hash, but pre 0.8 invoices do not have one at
395
        // all. Only allow lookups for payment address if it is not a blank
396
        // payment address, which is a special-cased value for legacy keysend
397
        // invoices.
398
        if ref.PayAddr() != nil && *ref.PayAddr() != BlankPayAddr {
×
399
                params.PaymentAddr = ref.PayAddr()[:]
×
400
        }
×
401

402
        // If the reference has a set ID we'll fetch the invoice which has the
403
        // corresponding AMP sub invoice.
404
        if ref.SetID() != nil {
×
405
                params.SetID = ref.SetID()[:]
×
406
        }
×
407

408
        var (
×
409
                rows []sqlc.Invoice
×
410
                err  error
×
411
        )
×
412

×
413
        // We need to split the query based on how we intend to look up the
×
414
        // invoice. If only the set ID is given then we want to have an exact
×
415
        // match on the set ID. If other fields are given, we want to match on
×
416
        // those fields and the set ID but with a less strict join condition.
×
417
        if params.Hash == nil && params.PaymentAddr == nil &&
×
418
                params.SetID != nil {
×
419

×
420
                rows, err = db.GetInvoiceBySetID(ctx, params.SetID)
×
421
        } else {
×
422
                rows, err = db.GetInvoice(ctx, params)
×
423
        }
×
424

425
        switch {
×
426
        case len(rows) == 0:
×
427
                return sqlc.Invoice{}, ErrInvoiceNotFound
×
428

429
        case len(rows) > 1:
×
430
                // In case the reference is ambiguous, meaning it matches more
×
431
                // than        one invoice, we'll return an error.
×
432
                return sqlc.Invoice{}, fmt.Errorf("ambiguous invoice ref: "+
×
433
                        "%s: %s", ref.String(), spew.Sdump(rows))
×
434

435
        case err != nil:
×
436
                return sqlc.Invoice{}, fmt.Errorf("unable to fetch invoice: %w",
×
437
                        err)
×
438
        }
439

440
        return rows[0], nil
×
441
}
442

443
// fetchInvoice fetches the common invoice data and the AMP state for the
444
// invoice with the given reference.
445
func fetchInvoice(ctx context.Context, db SQLInvoiceQueries, ref InvoiceRef) (
446
        *Invoice, error) {
×
447

×
448
        // Fetch the invoice from the database.
×
449
        sqlInvoice, err := getInvoiceByRef(ctx, db, ref)
×
450
        if err != nil {
×
451
                return nil, err
×
452
        }
×
453

454
        var (
×
455
                setID         *[32]byte
×
456
                fetchAmpHtlcs bool
×
457
        )
×
458

×
459
        // Now that we got the invoice itself, fetch the HTLCs as requested by
×
460
        // the modifier.
×
461
        switch ref.Modifier() {
×
462
        case DefaultModifier:
×
463
                // By default we'll fetch all AMP HTLCs.
×
464
                setID = nil
×
465
                fetchAmpHtlcs = true
×
466

467
        case HtlcSetOnlyModifier:
×
468
                // In this case we'll fetch all AMP HTLCs for the specified set
×
469
                // id.
×
470
                if ref.SetID() == nil {
×
471
                        return nil, fmt.Errorf("set ID is required to use " +
×
472
                                "the HTLC set only modifier")
×
473
                }
×
474

475
                setID = ref.SetID()
×
476
                fetchAmpHtlcs = true
×
477

478
        case HtlcSetBlankModifier:
×
479
                // No need to fetch any HTLCs.
×
480
                setID = nil
×
481
                fetchAmpHtlcs = false
×
482

483
        default:
×
484
                return nil, fmt.Errorf("unknown invoice ref modifier: %v",
×
485
                        ref.Modifier())
×
486
        }
487

488
        // Fetch the rest of the invoice data and fill the invoice struct.
489
        _, invoice, err := fetchInvoiceData(
×
490
                ctx, db, sqlInvoice, setID, fetchAmpHtlcs,
×
491
        )
×
492
        if err != nil {
×
493
                return nil, err
×
494
        }
×
495

496
        return invoice, nil
×
497
}
498

499
// fetchAmpState fetches the AMP state for the invoice with the given ID.
500
// Optional setID can be provided to fetch the state for a specific AMP HTLC
501
// set. If setID is nil then we'll fetch the state for all AMP sub invoices. If
502
// fetchHtlcs is set to true, the HTLCs for the given set will be fetched as
503
// well.
504
//
505
//nolint:funlen
506
func fetchAmpState(ctx context.Context, db SQLInvoiceQueries, invoiceID int64,
507
        setID *[32]byte, fetchHtlcs bool) (AMPInvoiceState,
508
        HTLCSet, error) {
×
509

×
510
        var paramSetID []byte
×
511
        if setID != nil {
×
512
                paramSetID = setID[:]
×
513
        }
×
514

515
        // First fetch all the AMP sub invoices for this invoice or the one
516
        // matching the provided set ID.
517
        ampInvoiceRows, err := db.FetchAMPSubInvoices(
×
518
                ctx, sqlc.FetchAMPSubInvoicesParams{
×
519
                        InvoiceID: invoiceID,
×
520
                        SetID:     paramSetID,
×
521
                },
×
522
        )
×
523
        if err != nil {
×
524
                return nil, nil, err
×
525
        }
×
526

527
        ampState := make(map[SetID]InvoiceStateAMP)
×
528
        for _, row := range ampInvoiceRows {
×
529
                var rowSetID [32]byte
×
530

×
531
                if len(row.SetID) != 32 {
×
532
                        return nil, nil, fmt.Errorf("invalid set id length: %d",
×
533
                                len(row.SetID))
×
534
                }
×
535

536
                var settleDate time.Time
×
537
                if row.SettledAt.Valid {
×
538
                        settleDate = row.SettledAt.Time.Local()
×
539
                }
×
540

541
                copy(rowSetID[:], row.SetID)
×
542
                ampState[rowSetID] = InvoiceStateAMP{
×
543
                        State:       HtlcState(row.State),
×
544
                        SettleIndex: uint64(row.SettleIndex.Int64),
×
545
                        SettleDate:  settleDate,
×
546
                        InvoiceKeys: make(map[models.CircuitKey]struct{}),
×
547
                }
×
548
        }
549

550
        if !fetchHtlcs {
×
551
                return ampState, nil, nil
×
552
        }
×
553

554
        customRecordRows, err := db.GetInvoiceHTLCCustomRecords(ctx, invoiceID)
×
555
        if err != nil {
×
556
                return nil, nil, fmt.Errorf("unable to get custom records for "+
×
557
                        "invoice HTLCs: %w", err)
×
558
        }
×
559

560
        customRecords := make(map[int64]record.CustomSet, len(customRecordRows))
×
561
        for _, row := range customRecordRows {
×
562
                if _, ok := customRecords[row.HtlcID]; !ok {
×
563
                        customRecords[row.HtlcID] = make(record.CustomSet)
×
564
                }
×
565

566
                value := row.Value
×
567
                if value == nil {
×
568
                        value = []byte{}
×
569
                }
×
570

571
                customRecords[row.HtlcID][uint64(row.Key)] = value
×
572
        }
573

574
        // Now fetch all the AMP HTLCs for this invoice or the one matching the
575
        // provided set ID.
576
        ampHtlcRows, err := db.FetchAMPSubInvoiceHTLCs(
×
577
                ctx, sqlc.FetchAMPSubInvoiceHTLCsParams{
×
578
                        InvoiceID: invoiceID,
×
579
                        SetID:     paramSetID,
×
580
                },
×
581
        )
×
582
        if err != nil {
×
583
                return nil, nil, err
×
584
        }
×
585

586
        ampHtlcs := make(map[models.CircuitKey]*InvoiceHTLC)
×
587
        for _, row := range ampHtlcRows {
×
588
                uint64ChanID, err := strconv.ParseUint(row.ChanID, 10, 64)
×
589
                if err != nil {
×
590
                        return nil, nil, err
×
591
                }
×
592

593
                chanID := lnwire.NewShortChanIDFromInt(uint64ChanID)
×
594

×
595
                if row.HtlcID < 0 {
×
596
                        return nil, nil, fmt.Errorf("invalid HTLC ID "+
×
597
                                "value: %v", row.HtlcID)
×
598
                }
×
599

600
                htlcID := uint64(row.HtlcID)
×
601

×
602
                circuitKey := CircuitKey{
×
603
                        ChanID: chanID,
×
604
                        HtlcID: htlcID,
×
605
                }
×
606

×
607
                htlc := &InvoiceHTLC{
×
608
                        Amt:          lnwire.MilliSatoshi(row.AmountMsat),
×
609
                        AcceptHeight: uint32(row.AcceptHeight),
×
610
                        AcceptTime:   row.AcceptTime.Local(),
×
611
                        Expiry:       uint32(row.ExpiryHeight),
×
612
                        State:        HtlcState(row.State),
×
613
                }
×
614

×
615
                if row.TotalMppMsat.Valid {
×
616
                        htlc.MppTotalAmt = lnwire.MilliSatoshi(
×
617
                                row.TotalMppMsat.Int64,
×
618
                        )
×
619
                }
×
620

621
                if row.ResolveTime.Valid {
×
622
                        htlc.ResolveTime = row.ResolveTime.Time.Local()
×
623
                }
×
624

625
                var (
×
626
                        rootShare [32]byte
×
627
                        setID     [32]byte
×
628
                )
×
629

×
630
                if len(row.RootShare) != 32 {
×
631
                        return nil, nil, fmt.Errorf("invalid root share "+
×
632
                                "length: %d", len(row.RootShare))
×
633
                }
×
634
                copy(rootShare[:], row.RootShare)
×
635

×
636
                if len(row.SetID) != 32 {
×
637
                        return nil, nil, fmt.Errorf("invalid set ID length: %d",
×
638
                                len(row.SetID))
×
639
                }
×
640
                copy(setID[:], row.SetID)
×
641

×
642
                if row.ChildIndex < 0 || row.ChildIndex > math.MaxUint32 {
×
643
                        return nil, nil, fmt.Errorf("invalid child index "+
×
644
                                "value: %v", row.ChildIndex)
×
645
                }
×
646

647
                ampRecord := record.NewAMP(
×
648
                        rootShare, setID, uint32(row.ChildIndex),
×
649
                )
×
650

×
651
                htlc.AMP = &InvoiceHtlcAMPData{
×
652
                        Record: *ampRecord,
×
653
                }
×
654

×
655
                if len(row.Hash) != 32 {
×
656
                        return nil, nil, fmt.Errorf("invalid hash length: %d",
×
657
                                len(row.Hash))
×
658
                }
×
659
                copy(htlc.AMP.Hash[:], row.Hash)
×
660

×
661
                if row.Preimage != nil {
×
662
                        preimage, err := lntypes.MakePreimage(row.Preimage)
×
663
                        if err != nil {
×
664
                                return nil, nil, err
×
665
                        }
×
666

667
                        htlc.AMP.Preimage = &preimage
×
668
                }
669

670
                if _, ok := customRecords[row.ID]; ok {
×
671
                        htlc.CustomRecords = customRecords[row.ID]
×
672
                } else {
×
673
                        htlc.CustomRecords = make(record.CustomSet)
×
674
                }
×
675

676
                ampHtlcs[circuitKey] = htlc
×
677
        }
678

679
        if len(ampHtlcs) > 0 {
×
680
                for setID := range ampState {
×
681
                        var amtPaid lnwire.MilliSatoshi
×
682
                        invoiceKeys := make(
×
683
                                map[models.CircuitKey]struct{},
×
684
                        )
×
685

×
686
                        for key, htlc := range ampHtlcs {
×
687
                                if htlc.AMP.Record.SetID() != setID {
×
688
                                        continue
×
689
                                }
690

691
                                invoiceKeys[key] = struct{}{}
×
692

×
693
                                if htlc.State != HtlcStateCanceled {
×
694
                                        amtPaid += htlc.Amt
×
695
                                }
×
696
                        }
697

698
                        setState := ampState[setID]
×
699
                        setState.InvoiceKeys = invoiceKeys
×
700
                        setState.AmtPaid = amtPaid
×
701
                        ampState[setID] = setState
×
702
                }
703
        }
704

705
        return ampState, ampHtlcs, nil
×
706
}
707

708
// LookupInvoice attempts to look up an invoice corresponding the passed in
709
// reference. The reference may be a payment hash, a payment address, or a set
710
// ID for an AMP sub invoice. If the invoice is found, we'll return the complete
711
// invoice. If the invoice is not found, then we'll return an ErrInvoiceNotFound
712
// error.
713
func (i *SQLStore) LookupInvoice(ctx context.Context,
714
        ref InvoiceRef) (Invoice, error) {
×
715

×
716
        var (
×
717
                invoice *Invoice
×
718
                err     error
×
719
        )
×
720

×
721
        readTxOpt := NewSQLInvoiceQueryReadTx()
×
722
        txErr := i.db.ExecTx(ctx, &readTxOpt, func(db SQLInvoiceQueries) error {
×
723
                invoice, err = fetchInvoice(ctx, db, ref)
×
724

×
725
                return err
×
726
        }, func() {})
×
727
        if txErr != nil {
×
728
                return Invoice{}, txErr
×
729
        }
×
730

731
        return *invoice, nil
×
732
}
733

734
// FetchPendingInvoices returns all the invoices that are currently in a
735
// "pending" state. An invoice is pending if it has been created but not yet
736
// settled or canceled.
737
func (i *SQLStore) FetchPendingInvoices(ctx context.Context) (
738
        map[lntypes.Hash]Invoice, error) {
×
739

×
740
        var invoices map[lntypes.Hash]Invoice
×
741

×
742
        readTxOpt := NewSQLInvoiceQueryReadTx()
×
743
        err := i.db.ExecTx(ctx, &readTxOpt, func(db SQLInvoiceQueries) error {
×
744
                return queryWithLimit(func(offset int) (int, error) {
×
745
                        params := sqlc.FilterInvoicesParams{
×
746
                                PendingOnly: true,
×
747
                                NumOffset:   int32(offset),
×
748
                                NumLimit:    int32(i.opts.paginationLimit),
×
749
                                Reverse:     false,
×
750
                        }
×
751

×
752
                        rows, err := db.FilterInvoices(ctx, params)
×
753
                        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
754
                                return 0, fmt.Errorf("unable to get invoices "+
×
755
                                        "from db: %w", err)
×
756
                        }
×
757

758
                        // Load all the information for the invoices.
759
                        for _, row := range rows {
×
760
                                hash, invoice, err := fetchInvoiceData(
×
761
                                        ctx, db, row, nil, true,
×
762
                                )
×
763
                                if err != nil {
×
764
                                        return 0, err
×
765
                                }
×
766

767
                                invoices[*hash] = *invoice
×
768
                        }
769

770
                        return len(rows), nil
×
771
                }, i.opts.paginationLimit)
772
        }, func() {
×
773
                invoices = make(map[lntypes.Hash]Invoice)
×
774
        })
×
775
        if err != nil {
×
776
                return nil, fmt.Errorf("unable to fetch pending invoices: %w",
×
777
                        err)
×
778
        }
×
779

780
        return invoices, nil
×
781
}
782

783
// InvoicesSettledSince can be used by callers to catch up any settled invoices
784
// they missed within the settled invoice time series. We'll return all known
785
// settled invoice that have a settle index higher than the passed idx.
786
//
787
// NOTE: The index starts from 1. As a result we enforce that specifying a value
788
// below the starting index value is a noop.
789
func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) (
790
        []Invoice, error) {
×
791

×
792
        var (
×
793
                invoices       []Invoice
×
794
                start          = time.Now()
×
795
                processedCount int
×
796
        )
×
797

×
798
        if idx == 0 {
×
799
                return invoices, nil
×
800
        }
×
UNCOV
801

×
802
        readTxOpt := NewSQLInvoiceQueryReadTx()
803
        err := i.db.ExecTx(ctx, &readTxOpt, func(db SQLInvoiceQueries) error {
×
804
                err := queryWithLimit(func(offset int) (int, error) {
×
805
                        params := sqlc.FilterInvoicesParams{
×
806
                                SettleIndexGet: sqldb.SQLInt64(idx + 1),
×
807
                                NumOffset:      int32(offset),
×
808
                                NumLimit:       int32(i.opts.paginationLimit),
×
809
                                Reverse:        false,
×
810
                        }
×
811

×
812
                        rows, err := db.FilterInvoices(ctx, params)
×
813
                        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
814
                                return 0, fmt.Errorf("unable to get invoices "+
×
815
                                        "from db: %w", err)
×
816
                        }
×
UNCOV
817

×
818
                        // Load all the information for the invoices.
819
                        for _, row := range rows {
820
                                _, invoice, err := fetchInvoiceData(
×
821
                                        ctx, db, row, nil, true,
×
822
                                )
×
823
                                if err != nil {
×
824
                                        return 0, fmt.Errorf("unable to fetch "+
×
825
                                                "invoice(id=%d) from db: %w",
×
826
                                                row.ID, err)
×
827
                                }
×
UNCOV
828

×
829
                                invoices = append(invoices, *invoice)
830

×
831
                                processedCount++
×
832
                                if processedCount%invoiceScanBatchSize == 0 {
×
833
                                        log.Debugf("Processed %d settled "+
×
834
                                                "invoices since invoice with "+
×
835
                                                "settle index %v",
×
836
                                                processedCount, idx)
×
837
                                }
×
UNCOV
838
                        }
×
UNCOV
839

×
840
                        return len(rows), nil
×
UNCOV
841
                }, i.opts.paginationLimit)
×
842
                if err != nil {
×
843
                        return err
844
                }
UNCOV
845

×
846
                // Now fetch all the AMP sub invoices that were settled since
UNCOV
847
                // the provided index.
×
848
                ampInvoices, err := i.db.FetchSettledAMPSubInvoices(
×
849
                        ctx, sqlc.FetchSettledAMPSubInvoicesParams{
×
850
                                SettleIndexGet: sqldb.SQLInt64(idx + 1),
851
                        },
852
                )
853
                if err != nil {
×
854
                        return err
×
855
                }
×
UNCOV
856

×
857
                for _, ampInvoice := range ampInvoices {
×
858
                        // Convert the row to a sqlc.Invoice so we can use the
×
859
                        // existing fetchInvoiceData function.
×
860
                        sqlInvoice := sqlc.Invoice{
×
861
                                ID:             ampInvoice.ID,
862
                                Hash:           ampInvoice.Hash,
×
863
                                Preimage:       ampInvoice.Preimage,
×
864
                                SettleIndex:    ampInvoice.AmpSettleIndex,
×
865
                                SettledAt:      ampInvoice.AmpSettledAt,
×
866
                                Memo:           ampInvoice.Memo,
×
867
                                AmountMsat:     ampInvoice.AmountMsat,
×
868
                                CltvDelta:      ampInvoice.CltvDelta,
×
869
                                Expiry:         ampInvoice.Expiry,
×
870
                                PaymentAddr:    ampInvoice.PaymentAddr,
×
871
                                PaymentRequest: ampInvoice.PaymentRequest,
×
872
                                State:          ampInvoice.State,
×
873
                                AmountPaidMsat: ampInvoice.AmountPaidMsat,
×
874
                                IsAmp:          ampInvoice.IsAmp,
×
875
                                IsHodl:         ampInvoice.IsHodl,
×
876
                                IsKeysend:      ampInvoice.IsKeysend,
×
877
                                CreatedAt:      ampInvoice.CreatedAt.UTC(),
×
878
                        }
×
879

×
880
                        // Fetch the state and HTLCs for this AMP sub invoice.
×
881
                        _, invoice, err := fetchInvoiceData(
×
882
                                ctx, db, sqlInvoice,
×
883
                                (*[32]byte)(ampInvoice.SetID), true,
×
884
                        )
×
885
                        if err != nil {
×
886
                                return fmt.Errorf("unable to fetch "+
×
887
                                        "AMP invoice(id=%d) from db: %w",
×
888
                                        ampInvoice.ID, err)
×
889
                        }
×
UNCOV
890

×
891
                        invoices = append(invoices, *invoice)
×
892

×
893
                        processedCount++
×
894
                        if processedCount%invoiceScanBatchSize == 0 {
×
895
                                log.Debugf("Processed %d settled invoices "+
896
                                        "since invoice with settle index %v",
×
897
                                        processedCount, idx)
×
898
                        }
×
UNCOV
899
                }
×
UNCOV
900

×
901
                return nil
×
902
        }, func() {
×
903
                invoices = nil
×
904
        })
×
905
        if err != nil {
×
906
                return nil, fmt.Errorf("unable to get invoices settled since "+
×
907
                        "index (excluding) %d: %w", idx, err)
×
908
        }
×
909

910
        elapsed := time.Since(start)
911
        log.Debugf("Completed scanning invoices settled since index %v: "+
×
912
                "total_processed=%d, found_invoices=%d, elapsed=%v",
×
913
                idx, processedCount, len(invoices),
×
914
                elapsed.Round(time.Millisecond))
×
915

×
916
        return invoices, nil
×
UNCOV
917
}
×
UNCOV
918

×
919
// InvoicesAddedSince can be used by callers to seek into the event time series
UNCOV
920
// of all the invoices added in the database. This method will return all
×
UNCOV
921
// invoices with an add index greater than the specified idx.
×
UNCOV
922
//
×
UNCOV
923
// NOTE: The index starts from 1. As a result we enforce that specifying a value
×
UNCOV
924
// below the starting index value is a noop.
×
UNCOV
925
func (i *SQLStore) InvoicesAddedSince(ctx context.Context, idx uint64) (
×
926
        []Invoice, error) {
×
927

928
        var (
929
                result         []Invoice
930
                start          = time.Now()
931
                processedCount int
932
        )
933

934
        if idx == 0 {
935
                return result, nil
936
        }
×
UNCOV
937

×
938
        readTxOpt := NewSQLInvoiceQueryReadTx()
×
939
        err := i.db.ExecTx(ctx, &readTxOpt, func(db SQLInvoiceQueries) error {
×
940
                return queryWithLimit(func(offset int) (int, error) {
×
941
                        params := sqlc.FilterInvoicesParams{
×
942
                                AddIndexGet: sqldb.SQLInt64(idx + 1),
×
943
                                NumOffset:   int32(offset),
×
944
                                NumLimit:    int32(i.opts.paginationLimit),
×
945
                                Reverse:     false,
×
946
                        }
×
947

×
948
                        rows, err := db.FilterInvoices(ctx, params)
949
                        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
950
                                return 0, fmt.Errorf("unable to get invoices "+
×
951
                                        "from db: %w", err)
×
952
                        }
×
UNCOV
953

×
UNCOV
954
                        // Load all the information for the invoices.
×
955
                        for _, row := range rows {
×
956
                                _, invoice, err := fetchInvoiceData(
×
957
                                        ctx, db, row, nil, true,
×
958
                                )
×
959
                                if err != nil {
×
960
                                        return 0, err
×
961
                                }
×
UNCOV
962

×
963
                                result = append(result, *invoice)
×
964

965
                                processedCount++
966
                                if processedCount%invoiceScanBatchSize == 0 {
×
967
                                        log.Debugf("Processed %d invoices "+
×
968
                                                "added since invoice with add "+
×
969
                                                "index %v", processedCount, idx)
×
970
                                }
×
UNCOV
971
                        }
×
UNCOV
972

×
973
                        return len(rows), nil
UNCOV
974
                }, i.opts.paginationLimit)
×
975
        }, func() {
×
976
                result = nil
×
977
        })
×
UNCOV
978

×
979
        if err != nil {
×
980
                return nil, fmt.Errorf("unable to get invoices added since "+
×
981
                        "index %d: %w", idx, err)
×
982
        }
×
UNCOV
983

×
984
        elapsed := time.Since(start)
×
985
        log.Debugf("Completed scanning invoices added since index %v: "+
×
986
                "total_processed=%d, found_invoices=%d, elapsed=%v",
987
                idx, processedCount, len(result),
988
                elapsed.Round(time.Millisecond))
×
989

990
        return result, nil
×
UNCOV
991
}
×
UNCOV
992

×
993
// QueryInvoices allows a caller to query the invoice database for invoices
UNCOV
994
// within the specified add index range.
×
UNCOV
995
func (i *SQLStore) QueryInvoices(ctx context.Context,
×
996
        q InvoiceQuery) (InvoiceSlice, error) {
×
997

×
998
        var invoices []Invoice
999

×
1000
        if q.NumMaxInvoices == 0 {
×
1001
                return InvoiceSlice{}, fmt.Errorf("max invoices must " +
×
1002
                        "be non-zero")
×
1003
        }
×
UNCOV
1004

×
1005
        readTxOpt := NewSQLInvoiceQueryReadTx()
×
1006
        err := i.db.ExecTx(ctx, &readTxOpt, func(db SQLInvoiceQueries) error {
1007
                return queryWithLimit(func(offset int) (int, error) {
1008
                        params := sqlc.FilterInvoicesParams{
1009
                                NumOffset:   int32(offset),
1010
                                NumLimit:    int32(i.opts.paginationLimit),
1011
                                PendingOnly: q.PendingOnly,
×
1012
                                Reverse:     q.Reversed,
×
1013
                        }
×
1014

×
1015
                        if q.Reversed {
×
1016
                                // If the index offset was not set, we want to
×
1017
                                // fetch from the lastest invoice.
×
1018
                                if q.IndexOffset == 0 {
×
1019
                                        params.AddIndexLet = sqldb.SQLInt64(
1020
                                                int64(math.MaxInt64),
×
1021
                                        )
×
1022
                                } else {
×
1023
                                        // The invoice with index offset id must
×
1024
                                        // not be included in the results.
×
1025
                                        params.AddIndexLet = sqldb.SQLInt64(
×
1026
                                                q.IndexOffset - 1,
×
1027
                                        )
×
1028
                                }
×
1029
                        } else {
×
1030
                                // The invoice with index offset id must not be
×
1031
                                // included in the results.
×
1032
                                params.AddIndexGet = sqldb.SQLInt64(
×
1033
                                        q.IndexOffset + 1,
×
1034
                                )
×
1035
                        }
×
UNCOV
1036

×
1037
                        if q.CreationDateStart != 0 {
×
1038
                                params.CreatedAfter = sqldb.SQLTime(
×
1039
                                        time.Unix(q.CreationDateStart, 0).UTC(),
×
1040
                                )
×
1041
                        }
×
UNCOV
1042

×
1043
                        if q.CreationDateEnd != 0 {
×
1044
                                // We need to add 1 to the end date as we're
×
1045
                                // checking less than the end date in SQL.
×
1046
                                params.CreatedBefore = sqldb.SQLTime(
×
1047
                                        time.Unix(q.CreationDateEnd+1, 0).UTC(),
×
1048
                                )
×
1049
                        }
×
UNCOV
1050

×
1051
                        rows, err := db.FilterInvoices(ctx, params)
1052
                        if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
1053
                                return 0, fmt.Errorf("unable to get invoices "+
×
1054
                                        "from db: %w", err)
×
1055
                        }
×
UNCOV
1056

×
1057
                        // Load all the information for the invoices.
1058
                        for _, row := range rows {
×
1059
                                _, invoice, err := fetchInvoiceData(
×
1060
                                        ctx, db, row, nil, true,
×
1061
                                )
×
1062
                                if err != nil {
×
1063
                                        return 0, err
×
1064
                                }
×
1065

1066
                                invoices = append(invoices, *invoice)
×
1067

×
1068
                                if len(invoices) == int(q.NumMaxInvoices) {
×
1069
                                        return 0, nil
×
1070
                                }
×
1071
                        }
1072

1073
                        return len(rows), nil
×
UNCOV
1074
                }, i.opts.paginationLimit)
×
1075
        }, func() {
×
1076
                invoices = nil
×
1077
        })
×
1078
        if err != nil {
×
1079
                return InvoiceSlice{}, fmt.Errorf("unable to query "+
×
1080
                        "invoices: %w", err)
1081
        }
×
UNCOV
1082

×
1083
        if len(invoices) == 0 {
×
1084
                return InvoiceSlice{
×
1085
                        InvoiceQuery: q,
×
1086
                }, nil
1087
        }
UNCOV
1088

×
1089
        // If we iterated through the add index in reverse order, then
UNCOV
1090
        // we'll need to reverse the slice of invoices to return them in
×
UNCOV
1091
        // forward order.
×
1092
        if q.Reversed {
×
1093
                numInvoices := len(invoices)
×
1094
                for i := 0; i < numInvoices/2; i++ {
×
1095
                        reverse := numInvoices - i - 1
×
1096
                        invoices[i], invoices[reverse] =
×
1097
                                invoices[reverse], invoices[i]
1098
                }
×
UNCOV
1099
        }
×
UNCOV
1100

×
1101
        res := InvoiceSlice{
×
1102
                InvoiceQuery:     q,
×
1103
                Invoices:         invoices,
1104
                FirstIndexOffset: invoices[0].AddIndex,
1105
                LastIndexOffset:  invoices[len(invoices)-1].AddIndex,
1106
        }
1107

×
1108
        return res, nil
×
UNCOV
1109
}
×
UNCOV
1110

×
UNCOV
1111
// sqlInvoiceUpdater is the implementation of the InvoiceUpdater interface using
×
UNCOV
1112
// a SQL database as the backend.
×
UNCOV
1113
type sqlInvoiceUpdater struct {
×
1114
        db         SQLInvoiceQueries
1115
        ctx        context.Context //nolint:containedctx
UNCOV
1116
        invoice    *Invoice
×
UNCOV
1117
        updateTime time.Time
×
UNCOV
1118
}
×
UNCOV
1119

×
UNCOV
1120
// AddHtlc adds a new htlc to the invoice.
×
UNCOV
1121
func (s *sqlInvoiceUpdater) AddHtlc(circuitKey models.CircuitKey,
×
1122
        newHtlc *InvoiceHTLC) error {
×
1123

×
1124
        htlcPrimaryKeyID, err := s.db.InsertInvoiceHTLC(
1125
                s.ctx, sqlc.InsertInvoiceHTLCParams{
1126
                        HtlcID: int64(circuitKey.HtlcID),
1127
                        ChanID: strconv.FormatUint(
1128
                                circuitKey.ChanID.ToUint64(), 10,
1129
                        ),
1130
                        AmountMsat: int64(newHtlc.Amt),
1131
                        TotalMppMsat: sql.NullInt64{
1132
                                Int64: int64(newHtlc.MppTotalAmt),
1133
                                Valid: newHtlc.MppTotalAmt != 0,
1134
                        },
1135
                        AcceptHeight: int32(newHtlc.AcceptHeight),
1136
                        AcceptTime:   newHtlc.AcceptTime.UTC(),
1137
                        ExpiryHeight: int32(newHtlc.Expiry),
×
1138
                        State:        int16(newHtlc.State),
×
1139
                        InvoiceID:    int64(s.invoice.AddIndex),
×
1140
                },
×
1141
        )
×
1142
        if err != nil {
×
1143
                return err
×
1144
        }
×
UNCOV
1145

×
1146
        for key, value := range newHtlc.CustomRecords {
×
1147
                err = s.db.InsertInvoiceHTLCCustomRecord(
×
1148
                        s.ctx, sqlc.InsertInvoiceHTLCCustomRecordParams{
×
1149
                                // TODO(bhandras): schema might be wrong here
×
1150
                                // as the custom record key is an uint64.
×
1151
                                Key:    int64(key),
×
1152
                                Value:  value,
×
1153
                                HtlcID: htlcPrimaryKeyID,
×
1154
                        },
×
1155
                )
×
1156
                if err != nil {
×
1157
                        return err
×
1158
                }
×
UNCOV
1159
        }
×
1160

1161
        if newHtlc.AMP != nil {
×
1162
                setID := newHtlc.AMP.Record.SetID()
×
1163

×
1164
                upsertResult, err := s.db.UpsertAMPSubInvoice(
×
1165
                        s.ctx, sqlc.UpsertAMPSubInvoiceParams{
×
1166
                                SetID:     setID[:],
×
1167
                                CreatedAt: s.updateTime.UTC(),
×
1168
                                InvoiceID: int64(s.invoice.AddIndex),
×
1169
                        },
×
1170
                )
×
1171
                if err != nil {
×
1172
                        mappedSQLErr := sqldb.MapSQLError(err)
×
1173
                        var uniqueConstraintErr *sqldb.ErrSQLUniqueConstraintViolation //nolint:ll
×
1174
                        if errors.As(mappedSQLErr, &uniqueConstraintErr) {
1175
                                return ErrDuplicateSetID{
1176
                                        SetID: setID,
×
1177
                                }
×
1178
                        }
×
UNCOV
1179

×
1180
                        return err
×
UNCOV
1181
                }
×
UNCOV
1182

×
UNCOV
1183
                // If we're just inserting the AMP invoice, we'll get a non
×
UNCOV
1184
                // zero rows affected count.
×
1185
                rowsAffected, err := upsertResult.RowsAffected()
×
1186
                if err != nil {
×
1187
                        return err
×
1188
                }
×
1189
                if rowsAffected != 0 {
×
1190
                        // If we're inserting a new AMP invoice, we'll also
×
1191
                        // insert a new invoice event.
×
1192
                        err = s.db.OnAMPSubInvoiceCreated(
×
1193
                                s.ctx, sqlc.OnAMPSubInvoiceCreatedParams{
×
1194
                                        AddedAt:   s.updateTime.UTC(),
1195
                                        InvoiceID: int64(s.invoice.AddIndex),
×
1196
                                        SetID:     setID[:],
1197
                                },
1198
                        )
1199
                        if err != nil {
1200
                                return err
×
1201
                        }
×
UNCOV
1202
                }
×
UNCOV
1203

×
1204
                rootShare := newHtlc.AMP.Record.RootShare()
×
1205

×
1206
                ampHtlcParams := sqlc.InsertAMPSubInvoiceHTLCParams{
×
1207
                        InvoiceID: int64(s.invoice.AddIndex),
×
1208
                        SetID:     setID[:],
×
1209
                        HtlcID:    htlcPrimaryKeyID,
×
1210
                        RootShare: rootShare[:],
×
1211
                        ChildIndex: int64(
×
1212
                                newHtlc.AMP.Record.ChildIndex(),
×
1213
                        ),
×
1214
                        Hash: newHtlc.AMP.Hash[:],
×
1215
                }
×
1216

×
1217
                if newHtlc.AMP.Preimage != nil {
1218
                        ampHtlcParams.Preimage = newHtlc.AMP.Preimage[:]
1219
                }
×
UNCOV
1220

×
1221
                err = s.db.InsertAMPSubInvoiceHTLC(s.ctx, ampHtlcParams)
×
1222
                if err != nil {
×
1223
                        return err
×
1224
                }
×
UNCOV
1225
        }
×
UNCOV
1226

×
1227
        return nil
×
UNCOV
1228
}
×
UNCOV
1229

×
UNCOV
1230
// ResolveHtlc marks an htlc as resolved with the given state.
×
UNCOV
1231
func (s *sqlInvoiceUpdater) ResolveHtlc(circuitKey models.CircuitKey,
×
1232
        state HtlcState, resolveTime time.Time) error {
×
1233

×
1234
        return s.db.UpdateInvoiceHTLC(s.ctx, sqlc.UpdateInvoiceHTLCParams{
×
1235
                HtlcID: int64(circuitKey.HtlcID),
1236
                ChanID: strconv.FormatUint(
×
1237
                        circuitKey.ChanID.ToUint64(), 10,
×
1238
                ),
×
1239
                InvoiceID:   int64(s.invoice.AddIndex),
×
1240
                State:       int16(state),
1241
                ResolveTime: sqldb.SQLTime(resolveTime.UTC()),
1242
        })
×
1243
}
1244

1245
// AddAmpHtlcPreimage adds a preimage of an AMP htlc to the AMP sub invoice
1246
// identified by the setID.
UNCOV
1247
func (s *sqlInvoiceUpdater) AddAmpHtlcPreimage(setID [32]byte,
×
1248
        circuitKey models.CircuitKey, preimage lntypes.Preimage) error {
×
1249

×
1250
        result, err := s.db.UpdateAMPSubInvoiceHTLCPreimage(
×
1251
                s.ctx, sqlc.UpdateAMPSubInvoiceHTLCPreimageParams{
×
1252
                        InvoiceID: int64(s.invoice.AddIndex),
×
1253
                        SetID:     setID[:],
×
1254
                        HtlcID:    int64(circuitKey.HtlcID),
×
1255
                        Preimage:  preimage[:],
×
1256
                        ChanID: strconv.FormatUint(
×
1257
                                circuitKey.ChanID.ToUint64(), 10,
×
1258
                        ),
×
1259
                },
1260
        )
1261
        if err != nil {
1262
                return err
1263
        }
×
UNCOV
1264

×
1265
        rowsAffected, err := result.RowsAffected()
×
1266
        if err != nil {
×
1267
                return err
×
1268
        }
×
1269
        if rowsAffected == 0 {
×
1270
                return ErrInvoiceNotFound
×
1271
        }
×
UNCOV
1272

×
1273
        return nil
×
UNCOV
1274
}
×
UNCOV
1275

×
UNCOV
1276
// UpdateInvoiceState updates the invoice state to the new state.
×
UNCOV
1277
func (s *sqlInvoiceUpdater) UpdateInvoiceState(
×
1278
        newState ContractState, preimage *lntypes.Preimage) error {
×
1279

1280
        var (
×
1281
                settleIndex sql.NullInt64
×
1282
                settledAt   sql.NullTime
×
1283
        )
×
1284

×
1285
        switch newState {
×
1286
        case ContractSettled:
×
1287
                nextSettleIndex, err := s.db.NextInvoiceSettleIndex(s.ctx)
1288
                if err != nil {
×
1289
                        return err
1290
                }
1291

1292
                settleIndex = sqldb.SQLInt64(nextSettleIndex)
1293

×
1294
                // If the invoice is settled, we'll also update the settle time.
×
1295
                settledAt = sqldb.SQLTime(s.updateTime.UTC())
×
1296

×
1297
                err = s.db.OnInvoiceSettled(
×
1298
                        s.ctx, sqlc.OnInvoiceSettledParams{
×
1299
                                AddedAt:   s.updateTime.UTC(),
×
1300
                                InvoiceID: int64(s.invoice.AddIndex),
×
1301
                        },
×
1302
                )
×
1303
                if err != nil {
×
1304
                        return err
×
1305
                }
×
1306

1307
        case ContractCanceled:
×
1308
                err := s.db.OnInvoiceCanceled(
×
1309
                        s.ctx, sqlc.OnInvoiceCanceledParams{
×
1310
                                AddedAt:   s.updateTime.UTC(),
×
1311
                                InvoiceID: int64(s.invoice.AddIndex),
×
1312
                        },
×
1313
                )
×
1314
                if err != nil {
×
1315
                        return err
×
1316
                }
×
UNCOV
1317
        }
×
UNCOV
1318

×
1319
        params := sqlc.UpdateInvoiceStateParams{
×
1320
                ID:          int64(s.invoice.AddIndex),
×
1321
                State:       int16(newState),
1322
                SettleIndex: settleIndex,
×
1323
                SettledAt:   settledAt,
×
1324
        }
×
1325

×
1326
        if preimage != nil {
×
1327
                params.Preimage = preimage[:]
×
1328
        }
×
UNCOV
1329

×
1330
        result, err := s.db.UpdateInvoiceState(s.ctx, params)
×
1331
        if err != nil {
×
1332
                return err
1333
        }
1334
        rowsAffected, err := result.RowsAffected()
×
1335
        if err != nil {
×
1336
                return err
×
1337
        }
×
UNCOV
1338

×
1339
        if rowsAffected == 0 {
×
1340
                return ErrInvoiceNotFound
×
1341
        }
×
UNCOV
1342

×
1343
        if settleIndex.Valid {
×
1344
                s.invoice.SettleIndex = uint64(settleIndex.Int64)
1345
                s.invoice.SettleDate = s.updateTime
×
1346
        }
×
UNCOV
1347

×
1348
        return nil
×
UNCOV
1349
}
×
UNCOV
1350

×
UNCOV
1351
// UpdateInvoiceAmtPaid updates the invoice amount paid to the new amount.
×
UNCOV
1352
func (s *sqlInvoiceUpdater) UpdateInvoiceAmtPaid(
×
1353
        amtPaid lnwire.MilliSatoshi) error {
1354

×
1355
        _, err := s.db.UpdateInvoiceAmountPaid(
×
1356
                s.ctx, sqlc.UpdateInvoiceAmountPaidParams{
×
1357
                        ID:             int64(s.invoice.AddIndex),
1358
                        AmountPaidMsat: int64(amtPaid),
×
1359
                },
×
1360
        )
×
1361

×
1362
        return err
1363
}
×
1364

1365
// UpdateAmpState updates the state of the AMP sub invoice identified by the
1366
// setID.
1367
func (s *sqlInvoiceUpdater) UpdateAmpState(setID [32]byte,
1368
        newState InvoiceStateAMP, _ models.CircuitKey) error {
×
1369

×
1370
        var (
×
1371
                settleIndex sql.NullInt64
×
1372
                settledAt   sql.NullTime
×
1373
        )
×
1374

×
1375
        switch newState.State {
×
1376
        case HtlcStateSettled:
×
1377
                nextSettleIndex, err := s.db.NextInvoiceSettleIndex(s.ctx)
×
1378
                if err != nil {
×
1379
                        return err
1380
                }
1381

1382
                settleIndex = sqldb.SQLInt64(nextSettleIndex)
1383

×
1384
                // If the invoice is settled, we'll also update the settle time.
×
1385
                settledAt = sqldb.SQLTime(s.updateTime.UTC())
×
1386

×
1387
                err = s.db.OnAMPSubInvoiceSettled(
×
1388
                        s.ctx, sqlc.OnAMPSubInvoiceSettledParams{
×
1389
                                AddedAt:   s.updateTime.UTC(),
×
1390
                                InvoiceID: int64(s.invoice.AddIndex),
×
1391
                                SetID:     setID[:],
×
1392
                        },
×
1393
                )
×
1394
                if err != nil {
×
1395
                        return err
×
1396
                }
UNCOV
1397

×
1398
        case HtlcStateCanceled:
×
1399
                err := s.db.OnAMPSubInvoiceCanceled(
×
1400
                        s.ctx, sqlc.OnAMPSubInvoiceCanceledParams{
×
1401
                                AddedAt:   s.updateTime.UTC(),
×
1402
                                InvoiceID: int64(s.invoice.AddIndex),
×
1403
                                SetID:     setID[:],
×
1404
                        },
×
1405
                )
×
1406
                if err != nil {
×
1407
                        return err
×
1408
                }
×
UNCOV
1409
        }
×
UNCOV
1410

×
1411
        err := s.db.UpdateAMPSubInvoiceState(
×
1412
                s.ctx, sqlc.UpdateAMPSubInvoiceStateParams{
1413
                        SetID:       setID[:],
×
1414
                        State:       int16(newState.State),
×
1415
                        SettleIndex: settleIndex,
×
1416
                        SettledAt:   settledAt,
×
1417
                },
×
1418
        )
×
1419
        if err != nil {
×
1420
                return err
×
1421
        }
×
UNCOV
1422

×
1423
        if settleIndex.Valid {
×
1424
                updatedState := s.invoice.AMPState[setID]
1425
                updatedState.SettleIndex = uint64(settleIndex.Int64)
1426
                updatedState.SettleDate = s.updateTime.UTC()
×
1427
                s.invoice.AMPState[setID] = updatedState
×
1428
        }
×
UNCOV
1429

×
1430
        return nil
×
UNCOV
1431
}
×
UNCOV
1432

×
UNCOV
1433
// Finalize finalizes the update before it is written to the database. Note that
×
UNCOV
1434
// we don't use this directly in the SQL implementation, so the function is just
×
UNCOV
1435
// a stub.
×
1436
func (s *sqlInvoiceUpdater) Finalize(_ UpdateType) error {
×
1437
        return nil
1438
}
×
UNCOV
1439

×
UNCOV
1440
// UpdateInvoice attempts to update an invoice corresponding to the passed
×
UNCOV
1441
// reference. If an invoice matching the passed reference doesn't exist within
×
UNCOV
1442
// the database, then the action will fail with  ErrInvoiceNotFound error.
×
UNCOV
1443
//
×
1444
// The update is performed inside the same database transaction that fetches the
UNCOV
1445
// invoice and is therefore atomic. The fields to update are controlled by the
×
1446
// supplied callback.
1447
func (i *SQLStore) UpdateInvoice(ctx context.Context, ref InvoiceRef,
1448
        setID *SetID, callback InvoiceUpdateCallback) (
1449
        *Invoice, error) {
1450

1451
        var updatedInvoice *Invoice
×
1452

×
1453
        txOpt := SQLInvoiceQueriesTxOptions{readOnly: false}
×
1454
        txErr := i.db.ExecTx(ctx, &txOpt, func(db SQLInvoiceQueries) error {
1455
                switch {
1456
                // For the default case we fetch all HTLCs.
1457
                case setID == nil:
1458
                        ref.refModifier = DefaultModifier
1459

1460
                // If the setID is the blank but NOT nil, we set the
1461
                // refModifier to HtlcSetBlankModifier to fetch no HTLC for the
1462
                // AMP invoice.
1463
                case *setID == BlankPayAddr:
1464
                        ref.refModifier = HtlcSetBlankModifier
×
UNCOV
1465

×
UNCOV
1466
                // A setID is provided, we use the refModifier to fetch only
×
UNCOV
1467
                // the HTLCs for the given setID and also make sure we add the
×
UNCOV
1468
                // setID to the ref.
×
1469
                default:
×
1470
                        var setIDBytes [32]byte
×
1471
                        copy(setIDBytes[:], setID[:])
1472
                        ref.setID = &setIDBytes
×
1473

×
1474
                        // We only fetch the HTLCs for the given setID.
1475
                        ref.refModifier = HtlcSetOnlyModifier
1476
                }
1477

1478
                invoice, err := fetchInvoice(ctx, db, ref)
×
1479
                if err != nil {
×
1480
                        return err
1481
                }
1482

1483
                updateTime := i.clock.Now()
1484
                updater := &sqlInvoiceUpdater{
×
1485
                        db:         db,
×
1486
                        ctx:        ctx,
×
1487
                        invoice:    invoice,
×
1488
                        updateTime: updateTime,
×
1489
                }
×
1490

×
1491
                payHash := ref.PayHash()
1492
                updatedInvoice, err = UpdateInvoice(
1493
                        payHash, invoice, updateTime, callback, updater,
×
1494
                )
×
1495

×
1496
                return err
×
1497
        }, func() {})
1498
        if txErr != nil {
×
1499
                // If the invoice is already settled, we'll return the
×
1500
                // (unchanged) invoice and the ErrInvoiceAlreadySettled error.
×
1501
                if errors.Is(txErr, ErrInvoiceAlreadySettled) {
×
1502
                        return updatedInvoice, txErr
×
1503
                }
×
UNCOV
1504

×
1505
                return nil, txErr
×
UNCOV
1506
        }
×
UNCOV
1507

×
1508
        return updatedInvoice, nil
×
UNCOV
1509
}
×
UNCOV
1510

×
UNCOV
1511
// DeleteInvoice attempts to delete the passed invoices and all their related
×
UNCOV
1512
// data from the database in one transaction.
×
UNCOV
1513
func (i *SQLStore) DeleteInvoice(ctx context.Context,
×
1514
        invoicesToDelete []InvoiceDeleteRef) error {
×
1515

×
1516
        // All the InvoiceDeleteRef instances include the add index of the
×
1517
        // invoice. The rest was added to ensure that the invoices were deleted
×
1518
        // properly in the kv database. When we have fully migrated we can
×
1519
        // remove the rest of the fields.
1520
        for _, ref := range invoicesToDelete {
×
1521
                if ref.AddIndex == 0 {
1522
                        return fmt.Errorf("unable to delete invoice using a "+
1523
                                "ref without AddIndex set: %v", ref)
×
1524
                }
1525
        }
1526

1527
        var writeTxOpt SQLInvoiceQueriesTxOptions
1528
        err := i.db.ExecTx(ctx, &writeTxOpt, func(db SQLInvoiceQueries) error {
1529
                for _, ref := range invoicesToDelete {
×
1530
                        params := sqlc.DeleteInvoiceParams{
×
1531
                                AddIndex: sqldb.SQLInt64(ref.AddIndex),
×
1532
                        }
×
1533

×
1534
                        if ref.SettleIndex != 0 {
×
1535
                                params.SettleIndex = sqldb.SQLInt64(
×
1536
                                        ref.SettleIndex,
×
1537
                                )
×
1538
                        }
×
UNCOV
1539

×
1540
                        if ref.PayHash != lntypes.ZeroHash {
1541
                                params.Hash = ref.PayHash[:]
1542
                        }
×
UNCOV
1543

×
1544
                        result, err := db.DeleteInvoice(ctx, params)
×
1545
                        if err != nil {
×
1546
                                return fmt.Errorf("unable to delete "+
×
1547
                                        "invoice(%v): %w", ref.AddIndex, err)
×
1548
                        }
×
1549
                        rowsAffected, err := result.RowsAffected()
×
1550
                        if err != nil {
×
1551
                                return fmt.Errorf("unable to get rows "+
×
1552
                                        "affected: %w", err)
×
1553
                        }
×
1554
                        if rowsAffected == 0 {
1555
                                return fmt.Errorf("%w: %v",
×
1556
                                        ErrInvoiceNotFound, ref.AddIndex)
×
1557
                        }
×
1558
                }
UNCOV
1559

×
1560
                return nil
×
1561
        }, func() {})
×
UNCOV
1562

×
1563
        if err != nil {
×
1564
                return fmt.Errorf("unable to delete invoices: %w", err)
×
1565
        }
×
UNCOV
1566

×
1567
        return nil
×
UNCOV
1568
}
×
UNCOV
1569

×
UNCOV
1570
// DeleteCanceledInvoices removes all canceled invoices from the database.
×
1571
func (i *SQLStore) DeleteCanceledInvoices(ctx context.Context) error {
×
1572
        var writeTxOpt SQLInvoiceQueriesTxOptions
×
1573
        err := i.db.ExecTx(ctx, &writeTxOpt, func(db SQLInvoiceQueries) error {
1574
                _, err := db.DeleteCanceledInvoices(ctx)
1575
                if err != nil {
×
1576
                        return fmt.Errorf("unable to delete canceled "+
×
1577
                                "invoices: %w", err)
1578
                }
×
UNCOV
1579

×
1580
                return nil
×
1581
        }, func() {})
1582
        if err != nil {
×
1583
                return fmt.Errorf("unable to delete invoices: %w", err)
1584
        }
1585

1586
        return nil
×
UNCOV
1587
}
×
UNCOV
1588

×
UNCOV
1589
// fetchInvoiceData fetches additional data for the given invoice. If the
×
UNCOV
1590
// invoice is AMP and the setID is not nil, then it will also fetch the AMP
×
UNCOV
1591
// state and HTLCs for the given setID, otherwise for all AMP sub invoices of
×
UNCOV
1592
// the invoice. If fetchAmpHtlcs is true, it will also fetch the AMP HTLCs.
×
UNCOV
1593
func fetchInvoiceData(ctx context.Context, db SQLInvoiceQueries,
×
1594
        row sqlc.Invoice, setID *[32]byte, fetchAmpHtlcs bool) (*lntypes.Hash,
1595
        *Invoice, error) {
×
1596

×
1597
        // Unmarshal the common data.
×
1598
        hash, invoice, err := unmarshalInvoice(row)
×
1599
        if err != nil {
×
1600
                return nil, nil, fmt.Errorf("unable to unmarshal "+
1601
                        "invoice(id=%d) from db: %w", row.ID, err)
×
1602
        }
1603

1604
        // Fetch the invoice features.
1605
        features, err := getInvoiceFeatures(ctx, db, row.ID)
1606
        if err != nil {
1607
                return nil, nil, err
1608
        }
1609

1610
        invoice.Terms.Features = features
×
1611

×
1612
        // If this is an AMP invoice, we'll need fetch the AMP state along
×
1613
        // with the HTLCs (if requested).
×
1614
        if invoice.IsAMP() {
×
1615
                invoiceID := int64(invoice.AddIndex)
×
1616
                ampState, ampHtlcs, err := fetchAmpState(
×
1617
                        ctx, db, invoiceID, setID, fetchAmpHtlcs,
×
1618
                )
1619
                if err != nil {
1620
                        return nil, nil, err
×
1621
                }
×
UNCOV
1622

×
1623
                invoice.AMPState = ampState
×
1624
                invoice.Htlcs = ampHtlcs
1625

×
1626
                return hash, invoice, nil
×
UNCOV
1627
        }
×
UNCOV
1628

×
UNCOV
1629
        // Otherwise simply fetch the invoice HTLCs.
×
1630
        htlcs, err := getInvoiceHtlcs(ctx, db, row.ID)
×
1631
        if err != nil {
×
1632
                return nil, nil, err
×
1633
        }
×
UNCOV
1634

×
1635
        if len(htlcs) > 0 {
×
1636
                invoice.Htlcs = htlcs
×
1637
        }
UNCOV
1638

×
1639
        return hash, invoice, nil
×
UNCOV
1640
}
×
UNCOV
1641

×
1642
// getInvoiceFeatures fetches the invoice features for the given invoice id.
1643
func getInvoiceFeatures(ctx context.Context, db SQLInvoiceQueries,
1644
        invoiceID int64) (*lnwire.FeatureVector, error) {
1645

×
1646
        rows, err := db.GetInvoiceFeatures(ctx, invoiceID)
×
1647
        if err != nil {
×
1648
                return nil, fmt.Errorf("unable to get invoice features: %w",
×
1649
                        err)
1650
        }
×
UNCOV
1651

×
1652
        features := lnwire.EmptyFeatureVector()
×
1653
        for _, feature := range rows {
1654
                features.Set(lnwire.FeatureBit(feature.Feature))
×
1655
        }
1656

1657
        return features, nil
1658
}
UNCOV
1659

×
UNCOV
1660
// getInvoiceHtlcs fetches the invoice htlcs for the given invoice id.
×
UNCOV
1661
func getInvoiceHtlcs(ctx context.Context, db SQLInvoiceQueries,
×
1662
        invoiceID int64) (map[CircuitKey]*InvoiceHTLC, error) {
×
1663

×
1664
        htlcRows, err := db.GetInvoiceHTLCs(ctx, invoiceID)
×
1665
        if err != nil {
×
1666
                return nil, fmt.Errorf("unable to get invoice htlcs: %w", err)
1667
        }
×
UNCOV
1668

×
UNCOV
1669
        // We have no htlcs to unmarshal.
×
1670
        if len(htlcRows) == 0 {
×
1671
                return nil, nil
1672
        }
×
1673

1674
        crRows, err := db.GetInvoiceHTLCCustomRecords(ctx, invoiceID)
1675
        if err != nil {
1676
                return nil, fmt.Errorf("unable to get custom records for "+
1677
                        "invoice htlcs: %w", err)
×
1678
        }
×
UNCOV
1679

×
1680
        cr := make(map[int64]record.CustomSet, len(crRows))
×
1681
        for _, row := range crRows {
×
1682
                if _, ok := cr[row.HtlcID]; !ok {
×
1683
                        cr[row.HtlcID] = make(record.CustomSet)
1684
                }
UNCOV
1685

×
1686
                value := row.Value
×
1687
                if value == nil {
×
1688
                        value = []byte{}
1689
                }
×
1690
                cr[row.HtlcID][uint64(row.Key)] = value
×
UNCOV
1691
        }
×
UNCOV
1692

×
1693
        htlcs := make(map[CircuitKey]*InvoiceHTLC, len(htlcRows))
×
1694

1695
        for _, row := range htlcRows {
×
1696
                circuiteKey, htlc, err := unmarshalInvoiceHTLC(row)
×
1697
                if err != nil {
×
1698
                        return nil, fmt.Errorf("unable to unmarshal "+
×
1699
                                "htlc(%d): %w", row.ID, err)
×
1700
                }
UNCOV
1701

×
1702
                if customRecords, ok := cr[row.ID]; ok {
×
1703
                        htlc.CustomRecords = customRecords
×
1704
                } else {
×
1705
                        htlc.CustomRecords = make(record.CustomSet)
×
1706
                }
1707

1708
                htlcs[circuiteKey] = htlc
×
UNCOV
1709
        }
×
UNCOV
1710

×
1711
        return htlcs, nil
×
UNCOV
1712
}
×
UNCOV
1713

×
UNCOV
1714
// unmarshalInvoice converts an InvoiceRow to an Invoice.
×
UNCOV
1715
func unmarshalInvoice(row sqlc.Invoice) (*lntypes.Hash, *Invoice,
×
1716
        error) {
1717

×
1718
        var (
×
1719
                settleIndex    int64
×
1720
                settledAt      time.Time
×
1721
                memo           []byte
×
1722
                paymentRequest []byte
1723
                preimage       *lntypes.Preimage
×
1724
                paymentAddr    [32]byte
1725
        )
1726

×
1727
        hash, err := lntypes.MakeHash(row.Hash)
1728
        if err != nil {
1729
                return nil, nil, err
1730
        }
UNCOV
1731

×
1732
        if row.SettleIndex.Valid {
×
1733
                settleIndex = row.SettleIndex.Int64
×
1734
        }
×
UNCOV
1735

×
1736
        if row.SettledAt.Valid {
×
1737
                settledAt = row.SettledAt.Time.Local()
×
1738
        }
×
UNCOV
1739

×
1740
        if row.Memo.Valid {
×
1741
                memo = []byte(row.Memo.String)
×
1742
        }
×
UNCOV
1743

×
UNCOV
1744
        // Keysend payments will have this field empty.
×
1745
        if row.PaymentRequest.Valid {
×
1746
                paymentRequest = []byte(row.PaymentRequest.String)
1747
        } else {
×
1748
                paymentRequest = []byte{}
×
1749
        }
×
1750

UNCOV
1751
        // We may not have the preimage if this a hodl invoice.
×
1752
        if row.Preimage != nil {
×
1753
                preimage = &lntypes.Preimage{}
×
1754
                copy(preimage[:], row.Preimage)
1755
        }
×
UNCOV
1756

×
1757
        copy(paymentAddr[:], row.PaymentAddr)
×
1758

1759
        var cltvDelta int32
1760
        if row.CltvDelta.Valid {
×
1761
                cltvDelta = row.CltvDelta.Int32
×
1762
        }
×
UNCOV
1763

×
1764
        expiry := time.Duration(row.Expiry) * time.Second
×
1765

1766
        invoice := &Invoice{
1767
                SettleIndex:    uint64(settleIndex),
×
1768
                SettleDate:     settledAt,
×
1769
                Memo:           memo,
×
1770
                PaymentRequest: paymentRequest,
×
1771
                CreationDate:   row.CreatedAt.Local(),
1772
                Terms: ContractTerm{
×
1773
                        FinalCltvDelta:  cltvDelta,
×
1774
                        Expiry:          expiry,
×
1775
                        PaymentPreimage: preimage,
×
1776
                        Value:           lnwire.MilliSatoshi(row.AmountMsat),
×
1777
                        PaymentAddr:     paymentAddr,
×
1778
                },
1779
                AddIndex:    uint64(row.ID),
×
1780
                State:       ContractState(row.State),
×
1781
                AmtPaid:     lnwire.MilliSatoshi(row.AmountPaidMsat),
×
1782
                Htlcs:       make(map[models.CircuitKey]*InvoiceHTLC),
×
1783
                AMPState:    AMPInvoiceState{},
×
1784
                HodlInvoice: row.IsHodl,
×
1785
        }
×
1786

×
1787
        return &hash, invoice, nil
×
UNCOV
1788
}
×
UNCOV
1789

×
UNCOV
1790
// unmarshalInvoiceHTLC converts an sqlc.InvoiceHtlc to an InvoiceHTLC.
×
UNCOV
1791
func unmarshalInvoiceHTLC(row sqlc.InvoiceHtlc) (CircuitKey,
×
1792
        *InvoiceHTLC, error) {
×
1793

×
1794
        uint64ChanID, err := strconv.ParseUint(row.ChanID, 10, 64)
×
1795
        if err != nil {
×
1796
                return CircuitKey{}, nil, err
×
1797
        }
×
UNCOV
1798

×
1799
        chanID := lnwire.NewShortChanIDFromInt(uint64ChanID)
×
1800

×
1801
        if row.HtlcID < 0 {
×
1802
                return CircuitKey{}, nil, fmt.Errorf("invalid uint64 "+
×
1803
                        "value: %v", row.HtlcID)
1804
        }
1805

1806
        htlcID := uint64(row.HtlcID)
1807

×
1808
        circuitKey := CircuitKey{
×
1809
                ChanID: chanID,
×
1810
                HtlcID: htlcID,
×
1811
        }
×
1812

×
1813
        htlc := &InvoiceHTLC{
1814
                Amt:          lnwire.MilliSatoshi(row.AmountMsat),
×
1815
                AcceptHeight: uint32(row.AcceptHeight),
×
1816
                AcceptTime:   row.AcceptTime.Local(),
×
1817
                Expiry:       uint32(row.ExpiryHeight),
×
1818
                State:        HtlcState(row.State),
×
1819
        }
×
1820

1821
        if row.TotalMppMsat.Valid {
×
1822
                htlc.MppTotalAmt = lnwire.MilliSatoshi(row.TotalMppMsat.Int64)
×
1823
        }
×
UNCOV
1824

×
1825
        if row.ResolveTime.Valid {
×
1826
                htlc.ResolveTime = row.ResolveTime.Time.Local()
×
1827
        }
×
UNCOV
1828

×
1829
        return circuitKey, htlc, nil
×
UNCOV
1830
}
×
UNCOV
1831

×
UNCOV
1832
// queryWithLimit is a helper method that can be used to query the database
×
UNCOV
1833
// using a limit and offset. The passed query function should return the number
×
UNCOV
1834
// of rows returned and an error if any.
×
1835
func queryWithLimit(query func(int) (int, error), limit int) error {
×
1836
        offset := 0
×
1837
        for {
×
1838
                rows, err := query(offset)
×
1839
                if err != nil {
1840
                        return err
×
1841
                }
×
UNCOV
1842

×
1843
                if rows < limit {
1844
                        return nil
×
1845
                }
1846

1847
                offset += limit
1848
        }
1849
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc