diff --git a/accounts/checkers.go b/accounts/checkers.go index 8fdb8c9b3..e4eff8935 100644 --- a/accounts/checkers.go +++ b/accounts/checkers.go @@ -522,6 +522,7 @@ func checkSend(ctx context.Context, chainParams *chaincfg.Params, } // The invoice is optional. + var paymentHash lntypes.Hash if len(invoice) > 0 { payReq, err := zpay32.Decode(invoice, chainParams) if err != nil { @@ -531,6 +532,10 @@ func checkSend(ctx context.Context, chainParams *chaincfg.Params, if payReq.MilliSat != nil && *payReq.MilliSat > sendAmt { sendAmt = *payReq.MilliSat } + + if payReq.PaymentHash != nil { + paymentHash = *payReq.PaymentHash + } } // We also add the max fee to the amount to check. This might mean that @@ -549,6 +554,14 @@ func checkSend(ctx context.Context, chainParams *chaincfg.Params, return fmt.Errorf("error validating account balance: %w", err) } + emptyHash := lntypes.Hash{} + if paymentHash != emptyHash { + err = service.AssociatePayment(acct.ID, paymentHash, sendAmt) + if err != nil { + return fmt.Errorf("error associating payment: %w", err) + } + } + return nil } diff --git a/accounts/checkers_test.go b/accounts/checkers_test.go index 64ce53a0b..2b37b9493 100644 --- a/accounts/checkers_test.go +++ b/accounts/checkers_test.go @@ -68,6 +68,12 @@ func (m *mockService) AssociateInvoice(id AccountID, hash lntypes.Hash) error { return nil } +func (m *mockService) AssociatePayment(id AccountID, paymentHash lntypes.Hash, + amt lnwire.MilliSatoshi) error { + + return nil +} + func (m *mockService) TrackPayment(_ AccountID, hash lntypes.Hash, amt lnwire.MilliSatoshi) error { diff --git a/accounts/interface.go b/accounts/interface.go index efbefc442..8fec489e2 100644 --- a/accounts/interface.go +++ b/accounts/interface.go @@ -247,6 +247,9 @@ type Service interface { // so we never need to debit the amount from the account. RemovePayment(hash lntypes.Hash) error - // IsRunning returns true if the service can be used. - IsRunning() bool + // AssociatePayment associates a payment (hash) with the given account, + // ensuring that the payment will be tracked for a user when LiT is + // restarted. + AssociatePayment(id AccountID, paymentHash lntypes.Hash, + fullAmt lnwire.MilliSatoshi) error } diff --git a/accounts/service.go b/accounts/service.go index bdf671730..661faac8f 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -416,6 +416,40 @@ func (s *InterceptorService) AssociateInvoice(id AccountID, return s.store.UpdateAccount(account) } +// AssociatePayment associates a payment (hash) with the given account, +// ensuring that the payment will be tracked for a user when LiT is +// restarted. +func (s *InterceptorService) AssociatePayment(id AccountID, + paymentHash lntypes.Hash, fullAmt lnwire.MilliSatoshi) error { + + s.Lock() + defer s.Unlock() + + account, err := s.store.Account(id) + if err != nil { + return err + } + + // If the payment is already associated with the account, we don't need + // to associate it again. + _, ok := account.Payments[paymentHash] + if ok { + return nil + } + + // Associate the payment with the account and store it. + account.Payments[paymentHash] = &PaymentEntry{ + Status: lnrpc.Payment_UNKNOWN, + FullAmount: fullAmt, + } + + if err := s.store.UpdateAccount(account); err != nil { + return fmt.Errorf("error updating account: %w", err) + } + + return nil +} + // invoiceUpdate credits the account an invoice was registered with, in case the // invoice was settled. // @@ -527,13 +561,33 @@ func (s *InterceptorService) TrackPayment(id AccountID, hash lntypes.Hash, return nil } - // Okay, we haven't tracked this payment before. So let's now associate - // the account with it. account.Payments[hash] = &PaymentEntry{ Status: lnrpc.Payment_UNKNOWN, FullAmount: fullAmt, } + if err := s.store.UpdateAccount(account); err != nil { + if !ok { + // In the rare case that the payment isn't associated + // with an account yet, and we fail to update the + // account we will not be tracking the payment, even if + // track the service is restarted. Therefore the node + // runner needs to manually check if the payment was + // made and debit the account if that's the case. + errStr := "critical error: failed to store the " + + "payment with hash %v for user with account " + + "id %v. Manual intervention required! " + + "Verify if the payment was executed, and " + + "manually update the user account balance by " + + "subtracting the payment amount if it was" + + mainChanErr := s.disableAndErrorfUnsafe( + errStr, hash, id, + ) + + s.mainErrCallback(mainChanErr) + } + return fmt.Errorf("error updating account: %w", err) }