diff --git a/accounts/interface.go b/accounts/interface.go index 11d3efe93..3ea10527a 100644 --- a/accounts/interface.go +++ b/accounts/interface.go @@ -225,9 +225,14 @@ type Store interface { AddAccountInvoice(ctx context.Context, id AccountID, hash lntypes.Hash) error - // IncreaseAccountBalance increases the balance of the account with the + // CreditAccount increases the balance of the account with the // given ID by the given amount. - IncreaseAccountBalance(ctx context.Context, id AccountID, + CreditAccount(ctx context.Context, id AccountID, + amount lnwire.MilliSatoshi) error + + // DebitAccount decreases the balance of the account with the + // given ID by the given amount. + DebitAccount(ctx context.Context, id AccountID, amount lnwire.MilliSatoshi) error // UpsertAccountPayment updates or inserts a payment entry for the given diff --git a/accounts/service.go b/accounts/service.go index f84f12500..098c4a8fa 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -563,7 +563,7 @@ func (s *InterceptorService) invoiceUpdate(ctx context.Context, // If we get here, the current account has the invoice associated with // it that was just paid. Credit the amount to the account and update it // in the DB. - err := s.store.IncreaseAccountBalance(ctx, acctID, invoice.AmountPaid) + err := s.store.CreditAccount(ctx, acctID, invoice.AmountPaid) if err != nil { return s.disableAndErrorfUnsafe("error increasing account "+ "balance account: %w", err) diff --git a/accounts/store_kvdb.go b/accounts/store_kvdb.go index 47a7c7ac4..a419017a8 100644 --- a/accounts/store_kvdb.go +++ b/accounts/store_kvdb.go @@ -223,11 +223,11 @@ func (s *BoltStore) AddAccountInvoice(_ context.Context, id AccountID, return s.updateAccount(id, update) } -// IncreaseAccountBalance increases the balance of the account with the given ID +// CreditAccount increases the balance of the account with the given ID // by the given amount. // // NOTE: This is part of the Store interface. -func (s *BoltStore) IncreaseAccountBalance(_ context.Context, id AccountID, +func (s *BoltStore) CreditAccount(_ context.Context, id AccountID, amount lnwire.MilliSatoshi) error { update := func(account *OffChainBalanceAccount) error { @@ -244,6 +244,33 @@ func (s *BoltStore) IncreaseAccountBalance(_ context.Context, id AccountID, return s.updateAccount(id, update) } +// DebitAccount decreases the balance of the account with the given ID +// by the given amount. +// +// NOTE: This is part of the Store interface. +func (s *BoltStore) DebitAccount(_ context.Context, id AccountID, + amount lnwire.MilliSatoshi) error { + + if amount > math.MaxInt64 { + return fmt.Errorf("amount %v exceeds the maximum of %v", + amount, int64(math.MaxInt64)) + } + + update := func(account *OffChainBalanceAccount) error { + if account.CurrentBalance-int64(amount) < 0 { + return fmt.Errorf("cannot debit %v from the account "+ + "balance, as the resulting balance would be "+ + "below 0", int64(amount/1000)) + } + + account.CurrentBalance -= int64(amount) + + return nil + } + + return s.updateAccount(id, update) +} + // UpsertAccountPayment updates or inserts a payment entry for the given // account. Various functional options can be passed to modify the behavior of // the method. The returned boolean is true if the payment was already known diff --git a/accounts/store_test.go b/accounts/store_test.go index 3d44df3e7..d03aafdc9 100644 --- a/accounts/store_test.go +++ b/accounts/store_test.go @@ -2,6 +2,7 @@ package accounts import ( "context" + "github.com/lightningnetwork/lnd/lnwire" "testing" "time" @@ -71,6 +72,14 @@ func TestAccountStore(t *testing.T) { ) require.NoError(t, err) + // Adjust the account balance by first crediting 10000, and then + // debiting 5000. + err = store.CreditAccount(ctx, acct1.ID, lnwire.MilliSatoshi(10000)) + require.NoError(t, err) + + err = store.DebitAccount(ctx, acct1.ID, lnwire.MilliSatoshi(5000)) + require.NoError(t, err) + // Update the in-memory account so that we can compare it with the // account we get from the store. acct1.CurrentBalance = -500 @@ -85,11 +94,30 @@ func TestAccountStore(t *testing.T) { } acct1.Invoices[lntypes.Hash{12, 34, 56, 78}] = struct{}{} acct1.Invoices[lntypes.Hash{34, 56, 78, 90}] = struct{}{} + acct1.CurrentBalance += 10000 + acct1.CurrentBalance -= 5000 dbAccount, err = store.Account(ctx, acct1.ID) require.NoError(t, err) assertEqualAccounts(t, acct1, dbAccount) + // Test that adjusting the balance to exactly 0 should work, while + // adjusting the balance to below 0 should fail. + err = store.DebitAccount( + ctx, acct1.ID, lnwire.MilliSatoshi(acct1.CurrentBalance), + ) + require.NoError(t, err) + + acct1.CurrentBalance = 0 + + dbAccount, err = store.Account(ctx, acct1.ID) + require.NoError(t, err) + assertEqualAccounts(t, acct1, dbAccount) + + // Adjusting the value to below 0 should fail. + err = store.DebitAccount(ctx, acct1.ID, lnwire.MilliSatoshi(1)) + require.ErrorContains(t, err, "balance would be below 0") + // Sleep just a tiny bit to make sure we are never too quick to measure // the expiry, even though the time is nanosecond scale and writing to // the store and reading again should take at least a couple of @@ -262,12 +290,12 @@ func TestAccountUpdateMethods(t *testing.T) { assertInvoices(hash1, hash2) }) - t.Run("IncreaseAccountBalance", func(t *testing.T) { + t.Run("CreditAccount", func(t *testing.T) { store := NewTestDB(t, clock.NewTestClock(time.Now())) // Increasing the balance of an account that doesn't exist // should error out. - err := store.IncreaseAccountBalance(ctx, AccountID{}, 100) + err := store.CreditAccount(ctx, AccountID{}, 100) require.ErrorIs(t, err, ErrAccNotFound) acct, err := store.NewAccount(ctx, 123, time.Time{}, "foo") @@ -284,7 +312,7 @@ func TestAccountUpdateMethods(t *testing.T) { // Increase the balance by 100 and assert that the new balance // is 223. - err = store.IncreaseAccountBalance(ctx, acct.ID, 100) + err = store.CreditAccount(ctx, acct.ID, 100) require.NoError(t, err) assertBalance(223)