Skip to content

Commit e9dcca8

Browse files
committed
paymentsdb: implement InitPayment for sql backend
1 parent 2473b32 commit e9dcca8

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

payments/db/sql_store.go

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type SQLQueries interface {
6161
Payment DB write operations.
6262
*/
6363
InsertPaymentIntent(ctx context.Context, arg sqlc.InsertPaymentIntentParams) (int64, error)
64-
InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) error
64+
InsertPayment(ctx context.Context, arg sqlc.InsertPaymentParams) (int64, error)
6565
InsertPaymentFirstHopCustomRecord(ctx context.Context, arg sqlc.InsertPaymentFirstHopCustomRecordParams) error
6666

6767
InsertHtlcAttempt(ctx context.Context, arg sqlc.InsertHtlcAttemptParams) (int64, error)
@@ -916,3 +916,131 @@ func (s *SQLStore) DeletePayment(paymentHash lntypes.Hash,
916916

917917
return nil
918918
}
919+
920+
// InitPayment creates a new payment record in the database with the given
921+
// payment hash and creation info.
922+
//
923+
// Before creating the payment, this method checks if a payment with the same
924+
// hash already exists and validates whether initialization is allowed based on
925+
// the existing payment's status:
926+
// - StatusInitiated: Returns ErrPaymentExists (payment already created,
927+
// HTLCs may be in flight)
928+
// - StatusInFlight: Returns ErrPaymentInFlight (payment currently being
929+
// attempted)
930+
// - StatusSucceeded: Returns ErrAlreadyPaid (payment already succeeded)
931+
// - StatusFailed: Allows retry by deleting the old payment record and
932+
// creating a new one
933+
//
934+
// If no existing payment is found, a new payment record is created with
935+
// StatusInitiated and stored with all associated metadata.
936+
//
937+
// This method is part of the PaymentControl interface, which is embedded in
938+
// the PaymentWriter interface and ultimately the DB interface, representing
939+
// the first step in the payment lifecycle control flow.
940+
func (s *SQLStore) InitPayment(paymentHash lntypes.Hash,
941+
paymentCreationInfo *PaymentCreationInfo) error {
942+
943+
ctx := context.TODO()
944+
945+
// Create the payment in the database.
946+
err := s.db.ExecTx(ctx, sqldb.WriteTxOpt(), func(db SQLQueries) error {
947+
existingPayment, err := db.FetchPayment(ctx, paymentHash[:])
948+
switch {
949+
// A payment with this hash already exists. We need to check its
950+
// status to see if we can re-initialize.
951+
case err == nil:
952+
paymentStatus, err := computePaymentStatusFromDB(
953+
ctx, db, existingPayment,
954+
)
955+
if err != nil {
956+
return fmt.Errorf("failed to compute payment "+
957+
"status: %w", err)
958+
}
959+
960+
// Check if the payment is initializable otherwise
961+
// we'll return early.
962+
if err := paymentStatus.initializable(); err != nil {
963+
return fmt.Errorf("payment is not "+
964+
"initializable: %w", err)
965+
}
966+
967+
// If the initializable check above passes, then the
968+
// existing payment has failed. So we delete it and
969+
// all of its previous artifacts. We rely on
970+
// cascading deletes to clean up the rest.
971+
err = db.DeletePayment(ctx, existingPayment.Payment.ID)
972+
if err != nil {
973+
return fmt.Errorf("failed to delete "+
974+
"payment: %w", err)
975+
}
976+
977+
// An unexpected error occurred while fetching the payment.
978+
case !errors.Is(err, sql.ErrNoRows):
979+
// Some other error occurred
980+
return fmt.Errorf("failed to check existing "+
981+
"payment: %w", err)
982+
983+
// The payment does not yet exist, so we can proceed.
984+
default:
985+
}
986+
987+
// Insert the payment first to get its ID.
988+
paymentID, err := db.InsertPayment(
989+
ctx, sqlc.InsertPaymentParams{
990+
AmountMsat: int64(
991+
paymentCreationInfo.Value,
992+
),
993+
CreatedAt: paymentCreationInfo.
994+
CreationTime.UTC(),
995+
PaymentIdentifier: paymentHash[:],
996+
},
997+
)
998+
if err != nil {
999+
return fmt.Errorf("failed to insert payment: %w", err)
1000+
}
1001+
1002+
// If there's a payment request, insert the payment intent.
1003+
if len(paymentCreationInfo.PaymentRequest) > 0 {
1004+
_, err = db.InsertPaymentIntent(
1005+
ctx, sqlc.InsertPaymentIntentParams{
1006+
PaymentID: paymentID,
1007+
IntentType: int16(
1008+
PaymentIntentTypeBolt11,
1009+
),
1010+
IntentPayload: paymentCreationInfo.
1011+
PaymentRequest,
1012+
},
1013+
)
1014+
if err != nil {
1015+
return fmt.Errorf("failed to insert "+
1016+
"payment intent: %w", err)
1017+
}
1018+
}
1019+
1020+
firstHopCustomRecords := paymentCreationInfo.
1021+
FirstHopCustomRecords
1022+
1023+
for key, value := range firstHopCustomRecords {
1024+
err = db.InsertPaymentFirstHopCustomRecord(
1025+
ctx,
1026+
sqlc.InsertPaymentFirstHopCustomRecordParams{
1027+
PaymentID: paymentID,
1028+
Key: int64(key),
1029+
Value: value,
1030+
},
1031+
)
1032+
if err != nil {
1033+
return fmt.Errorf("failed to insert "+
1034+
"payment first hop custom "+
1035+
"record: %w", err)
1036+
}
1037+
}
1038+
1039+
return nil
1040+
}, sqldb.NoOpReset)
1041+
if err != nil {
1042+
return fmt.Errorf("failed to initialize payment: %w", err)
1043+
}
1044+
1045+
return nil
1046+
}

sqldb/sqlc/payments.sql.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sqldb/sqlc/querier.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)