@@ -467,26 +467,7 @@ func (s *InterceptorService) PaymentErrored(ctx context.Context, id AccountID,
467467 "has already started" )
468468 }
469469
470- account , err := s .store .Account (ctx , id )
471- if err != nil {
472- return err
473- }
474-
475- // Check that this payment is actually associated with this account.
476- _ , ok = account .Payments [hash ]
477- if ! ok {
478- return fmt .Errorf ("payment with hash %s is not associated " +
479- "with this account" , hash )
480- }
481-
482- // Delete the payment and update the persisted account.
483- delete (account .Payments , hash )
484-
485- if err := s .store .UpdateAccount (ctx , account ); err != nil {
486- return fmt .Errorf ("error updating account: %w" , err )
487- }
488-
489- return nil
470+ return s .store .DeleteAccountPayment (ctx , id , hash )
490471}
491472
492473// AssociatePayment associates a payment (hash) with the given account,
@@ -498,44 +479,24 @@ func (s *InterceptorService) AssociatePayment(ctx context.Context, id AccountID,
498479 s .Lock ()
499480 defer s .Unlock ()
500481
501- account , err := s .store .Account (ctx , id )
502- if err != nil {
503- return err
504- }
505-
506- // Check if this payment is associated with the account already.
507- _ , ok := account .Payments [paymentHash ]
508- if ok {
509- // We do not allow another payment to the same hash if the
510- // payment is already in-flight or succeeded. This mitigates a
511- // user being able to launch a second RPC-erring payment with
512- // the same hash that would remove the payment from being
513- // tracked. Note that this prevents launching multipart
514- // payments, but allows retrying a payment if it has failed.
515- if account .Payments [paymentHash ].Status !=
516- lnrpc .Payment_FAILED {
517-
518- return fmt .Errorf ("payment with hash %s is already in " +
519- "flight or succeeded (status %v)" , paymentHash ,
520- account .Payments [paymentHash ].Status )
521- }
522-
523- // Otherwise, we fall through to correctly update the payment
524- // amount, in case we have a zero-amount invoice that is
525- // retried.
526- }
527-
528- // Associate the payment with the account and store it.
529- account .Payments [paymentHash ] = & PaymentEntry {
530- Status : lnrpc .Payment_UNKNOWN ,
531- FullAmount : fullAmt ,
532- }
533-
534- if err := s .store .UpdateAccount (ctx , account ); err != nil {
535- return fmt .Errorf ("error updating account: %w" , err )
536- }
482+ // We add the WithErrIfAlreadyPending option to ensure that if the
483+ // payment is already associated with the account, then we return
484+ // an error if the payment is already in-flight or succeeded. This
485+ // prevents a user from being able to launch a second RPC-erring payment
486+ // with the same hash that would remove the payment from being tracked.
487+ //
488+ // NOTE: this prevents launching multipart payments, but allows
489+ // retrying a payment if it has failed.
490+ //
491+ // If the payment is already associated with the account but not in
492+ // flight, we update the payment amount in case we have a zero-amount
493+ // invoice that is retried.
494+ _ , err := s .store .UpsertAccountPayment (
495+ ctx , id , paymentHash , fullAmt , lnrpc .Payment_UNKNOWN ,
496+ WithErrIfAlreadyPending (),
497+ )
537498
538- return nil
499+ return err
539500}
540501
541502// invoiceUpdate credits the account an invoice was registered with, in case the
@@ -629,34 +590,30 @@ func (s *InterceptorService) TrackPayment(ctx context.Context, id AccountID,
629590 return nil
630591 }
631592
632- // Similarly, if we've already processed the payment in the past, there
633- // is a reference in the account with the given state.
634- account , err := s .store .Account (ctx , id )
635- if err != nil {
636- return fmt .Errorf ("error fetching account: %w" , err )
637- }
638-
639593 // If the account already stored a terminal state, we also don't need to
640- // track the payment again.
641- entry , ok := account .Payments [hash ]
642- if ok && successState (entry .Status ) {
643- return nil
594+ // track the payment again. So we add the WithErrIfAlreadySucceeded
595+ // option to ensure that we return an error if the payment has already
596+ // succeeded. We can then match on the ErrAlreadySucceeded error and
597+ // exit early if it is returned.
598+ opts := []UpsertPaymentOption {
599+ WithErrIfAlreadySucceeded (),
644600 }
645601
646602 // There is a case where the passed in fullAmt is zero but the pending
647603 // amount is not. In that case, we should not overwrite the pending
648604 // amount.
649605 if fullAmt == 0 {
650- fullAmt = entry .FullAmount
651- }
652-
653- account .Payments [hash ] = & PaymentEntry {
654- Status : lnrpc .Payment_UNKNOWN ,
655- FullAmount : fullAmt ,
606+ opts = append (opts , WithPendingAmount ())
656607 }
657608
658- if err := s .store .UpdateAccount (ctx , account ); err != nil {
659- if ! ok {
609+ known , err := s .store .UpsertAccountPayment (
610+ ctx , id , hash , fullAmt , lnrpc .Payment_UNKNOWN , opts ... ,
611+ )
612+ if err != nil {
613+ if errors .Is (err , ErrAlreadySucceeded ) {
614+ return nil
615+ }
616+ if ! known {
660617 // In the rare case that the payment isn't associated
661618 // with an account yet, and we fail to update the
662619 // account we will not be tracking the payment, even if
@@ -832,23 +789,14 @@ func (s *InterceptorService) paymentUpdate(ctx context.Context,
832789
833790 // The payment went through! We now need to debit the full amount from
834791 // the account.
835- account , err := s .store .Account (ctx , pendingPayment .accountID )
836- if err != nil {
837- err = s .disableAndErrorfUnsafe ("error fetching account: %w" ,
838- err )
839-
840- return terminalState , err
841- }
842-
843792 fullAmount := status .Value + status .Fee
844793
845- // Update the account and store it in the database.
846- account .CurrentBalance -= int64 (fullAmount )
847- account .Payments [hash ] = & PaymentEntry {
848- Status : lnrpc .Payment_SUCCEEDED ,
849- FullAmount : fullAmount ,
850- }
851- if err := s .store .UpdateAccount (ctx , account ); err != nil {
794+ // Update the persisted account.
795+ _ , err := s .store .UpsertAccountPayment (
796+ ctx , pendingPayment .accountID , hash , fullAmount ,
797+ lnrpc .Payment_SUCCEEDED , WithDebitAccount (),
798+ )
799+ if err != nil {
852800 err = s .disableAndErrorfUnsafe ("error updating account: %w" ,
853801 err )
854802
@@ -892,23 +840,36 @@ func (s *InterceptorService) removePayment(ctx context.Context,
892840 return nil
893841 }
894842
895- account , err := s .store .Account (ctx , pendingPayment .accountID )
896- if err != nil {
897- return err
843+ _ , err := s .store .UpsertAccountPayment (
844+ ctx , pendingPayment .accountID , hash , 0 , status ,
845+ // We don't want the payment to be inserted if it isn't already
846+ // known. So we pass in this option to ensure that the call
847+ // exits early if the payment is unknown.
848+ WithErrIfUnknown (),
849+ // Otherwise, we just want to update the status of the payment
850+ // and use the existing pending amount.
851+ WithPendingAmount (),
852+ )
853+ if err != nil && ! errors .Is (err , ErrPaymentNotAssociated ) {
854+ return fmt .Errorf ("error updating account: %w" , err )
898855 }
899856
900857 pendingPayment .cancel ()
901858 delete (s .pendingPayments , hash )
902859
903- // Have we associated the payment with the account already?
904- _ , ok = account .Payments [hash ]
905- if ! ok {
906- return nil
907- }
860+ return nil
861+ }
908862
909- // If we did, let's set the status correctly in the DB now.
910- account .Payments [hash ].Status = status
911- return s .store .UpdateAccount (ctx , account )
863+ // hasPayment returns true if the payment is currently being tracked by the
864+ // service.
865+ //
866+ // NOTE: this is currently used only for tests.
867+ func (s * InterceptorService ) hasPayment (hash lntypes.Hash ) bool {
868+ s .RLock ()
869+ defer s .RUnlock ()
870+
871+ _ , ok := s .pendingPayments [hash ]
872+ return ok
912873}
913874
914875// successState returns true if a payment was completed successfully.
0 commit comments