diff --git a/sqldb/sqlc/migrations/000009_payments.up.sql b/sqldb/sqlc/migrations/000009_payments.up.sql index 0d85b497b..65094a15e 100644 --- a/sqldb/sqlc/migrations/000009_payments.up.sql +++ b/sqldb/sqlc/migrations/000009_payments.up.sql @@ -2,43 +2,12 @@ -- Payment System Schema Migration -- ───────────────────────────────────────────── -- This migration creates the complete payment system schema including: --- - Payment intents (BOLT 11/12 invoices, offers) +-- - Payment intents (only BOLT 11 invoices for now) -- - Payment attempts and HTLC tracking -- - Route hops and custom TLV records -- - Resolution tracking for settled/failed payments -- ───────────────────────────────────────────── --- ───────────────────────────────────────────── --- Payment Intents Table --- ───────────────────────────────────────────── --- Stores the descriptor of what the payment is paying for. --- Depending on the type, the payload might contain: --- - BOLT 11 invoice data --- - BOLT 12 offer data --- - NULL for legacy hash-only/keysend style payments --- ───────────────────────────────────────────── - -CREATE TABLE IF NOT EXISTS payment_intents ( - -- Primary key for the intent record - id INTEGER PRIMARY KEY, - - -- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_offer) - -- Uses SMALLINT (int16) for efficient storage of enum values - intent_type SMALLINT NOT NULL, - - -- The serialized payload for the payment intent - -- Content depends on type - could be invoice, offer, or NULL - intent_payload BLOB -); - --- Index for efficient querying by intent type -CREATE INDEX IF NOT EXISTS idx_payment_intents_type -ON payment_intents(intent_type); - --- Unique constraint for deduplication of payment intents -CREATE UNIQUE INDEX IF NOT EXISTS idx_payment_intents_unique -ON payment_intents(intent_type, intent_payload); - -- ───────────────────────────────────────────── -- Payments Table -- ───────────────────────────────────────────── @@ -55,10 +24,6 @@ CREATE TABLE IF NOT EXISTS payments ( -- Primary key for the payment record id INTEGER PRIMARY KEY, - -- Optional reference to the payment intent this payment was derived from - -- Links to BOLT 11 invoice, BOLT 12 offer, etc. - intent_id BIGINT REFERENCES payment_intents (id), - -- The amount of the payment in millisatoshis amount_msat BIGINT NOT NULL, @@ -70,20 +35,59 @@ CREATE TABLE IF NOT EXISTS payments ( -- For AMP: the setID -- For future intent types: any unique payment-level key payment_identifier BLOB NOT NULL, - + -- The reason for payment failure (only set if payment has failed) -- Integer enum type indicating failure reason fail_reason INTEGER, -- Ensure payment identifiers are unique across all payments - CONSTRAINT idx_payments_payment_identifier_unique + CONSTRAINT idx_payments_payment_identifier_unique UNIQUE (payment_identifier) ); -- Index for efficient querying by creation time (for chronological ordering) -CREATE INDEX IF NOT EXISTS idx_payments_created_at +CREATE INDEX IF NOT EXISTS idx_payments_created_at ON payments(created_at); +-- ───────────────────────────────────────────── +-- Payment Intents Table +-- ───────────────────────────────────────────── +-- Stores the descriptor of what the payment is paying for. +-- Depending on the type, the payload might contain: +-- - BOLT 11 invoice data +-- - BOLT 12 offer data +-- - NULL for legacy hash-only/keysend style payments +-- ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS payment_intents ( + -- Primary key for the intent record + id INTEGER PRIMARY KEY, + + -- Reference to the payment this intent belongs to (one-to-one relationship) + -- When the payment is deleted, the intent is automatically deleted + payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE, + + -- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_invoice) + -- Uses SMALLINT (int16) for efficient storage of enum values + intent_type SMALLINT NOT NULL, + + -- The serialized payload for the payment intent + -- Content depends on type - could be invoice, offer, or NULL + intent_payload BLOB, + + -- Ensure one-to-one relationship: each payment has at most one intent. + -- Currently we only support one intent per payment this makes sure we do + -- not accidentally pay the same request multiple times. This currently + -- only has bolt 11 payment requests/invoices. But in the future this can + -- also include BOLT 12 offers/invoices. + CONSTRAINT idx_payment_intents_payment_id_unique + UNIQUE (payment_id) +); + +-- Index for efficient querying by intent type +CREATE INDEX IF NOT EXISTS idx_payment_intents_type +ON payment_intents(intent_type); + -- ───────────────────────────────────────────── -- Payment HTLC Attempts Table -- ───────────────────────────────────────────── diff --git a/sqldb/sqlc/models.go b/sqldb/sqlc/models.go index 899c572e5..0e04cd467 100644 --- a/sqldb/sqlc/models.go +++ b/sqldb/sqlc/models.go @@ -211,7 +211,6 @@ type MigrationTracker struct { type Payment struct { ID int64 - IntentID sql.NullInt64 AmountMsat int64 CreatedAt time.Time PaymentIdentifier []byte @@ -264,6 +263,7 @@ type PaymentHtlcAttemptResolution struct { type PaymentIntent struct { ID int64 + PaymentID int64 IntentType int16 IntentPayload []byte } diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index 83c1c7f1f..e28e8a524 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -317,11 +317,11 @@ func (q *Queries) FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds [ const fetchPayment = `-- name: FetchPayment :one SELECT - p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE p.payment_identifier = $1 ` @@ -336,7 +336,6 @@ func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (F var i FetchPaymentRow err := row.Scan( &i.Payment.ID, - &i.Payment.IntentID, &i.Payment.AmountMsat, &i.Payment.CreatedAt, &i.Payment.PaymentIdentifier, @@ -398,11 +397,11 @@ func (q *Queries) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, pa const fetchPaymentsByIDs = `-- name: FetchPaymentsByIDs :many SELECT - p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE p.id IN (/*SLICE:payment_ids*/?) ` @@ -433,7 +432,6 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([ var i FetchPaymentsByIDsRow if err := rows.Scan( &i.Payment.ID, - &i.Payment.IntentID, &i.Payment.AmountMsat, &i.Payment.CreatedAt, &i.Payment.PaymentIdentifier, @@ -510,11 +508,11 @@ const filterPayments = `-- name: FilterPayments :many */ SELECT - p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, + p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason, i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE ( p.id > $1 OR $1 IS NULL @@ -572,7 +570,6 @@ func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams) var i FilterPaymentsRow if err := rows.Scan( &i.Payment.ID, - &i.Payment.IntentID, &i.Payment.AmountMsat, &i.Payment.CreatedAt, &i.Payment.PaymentIdentifier, diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index ce43a3e29..a94ba1f1f 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -9,7 +9,7 @@ SELECT i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE ( p.id > sqlc.narg('index_offset_get') OR sqlc.narg('index_offset_get') IS NULL @@ -37,7 +37,7 @@ SELECT i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE p.payment_identifier = $1; -- name: FetchPaymentsByIDs :many @@ -46,7 +46,7 @@ SELECT i.intent_type AS "intent_type", i.intent_payload AS "intent_payload" FROM payments p -LEFT JOIN payment_intents i ON i.id = p.intent_id +LEFT JOIN payment_intents i ON i.payment_id = p.id WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/); -- name: CountPayments :one