@@ -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+ }
0 commit comments