Skip to content

accounts: add functionality to update balance by given amount #962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ jobs:
unit_type:
- unit-race
- unit
- unit dbbackend=postgres
- unit dbbackend=sqlite
steps:
- name: git checkout
uses: actions/checkout@v4
Expand Down
33 changes: 33 additions & 0 deletions accounts/interface.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package accounts

import (
"bytes"
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
Expand Down Expand Up @@ -55,6 +57,31 @@ func ParseAccountID(idStr string) (*AccountID, error) {
return &id, nil
}

// ToInt64 converts an AccountID to its int64 representation.
func (a AccountID) ToInt64() (int64, error) {
var value int64
buf := bytes.NewReader(a[:])
if err := binary.Read(buf, byteOrder, &value); err != nil {
return 0, err
}

return value, nil
}

// AccountIDFromInt64 converts an int64 to an AccountID.
func AccountIDFromInt64(value int64) (AccountID, error) {
var (
a = AccountID{}
buf = new(bytes.Buffer)
)
if err := binary.Write(buf, binary.BigEndian, value); err != nil {
return a, err
}
copy(a[:], buf.Bytes())

return a, nil
}

// String returns the string representation of the AccountID.
func (a AccountID) String() string {
return hex.EncodeToString(a[:])
Expand Down Expand Up @@ -241,6 +268,12 @@ type Store interface {
status lnrpc.Payment_PaymentStatus,
options ...UpsertPaymentOption) (bool, error)

// AdjustAccountBalance modifies the given account balance by adding or
// deducting the specified amount, depending on whether isAddition is
// true or false.
AdjustAccountBalance(ctx context.Context, alias AccountID,
amount lnwire.MilliSatoshi, isAddition bool) error

// DeleteAccountPayment removes a payment entry from the account with
// the given ID. It will return the ErrPaymentNotAssociated error if the
// payment is not associated with the account.
Expand Down
47 changes: 47 additions & 0 deletions accounts/rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,53 @@ func (s *RPCServer) UpdateAccount(ctx context.Context,
return marshalAccount(account), nil
}

// UpdateBalance adds or deducts an amount from an existing account in the
Copy link
Member

@ellemouton ellemouton Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just an idea for standard PR flow: we always try to (as much as possible at least) to keep commits ordered such that things are working commit by commit. For 2 reasons: 1) reverting back to any single commit isnt a problem and 2) review story telling.

so the ideal flow for RPC additions is:

  1. add DB/Service functionality along with any tests for those (in small PR world, that could honestly be the whole PR - since that is like 1 unit of code that is easy to reason about and also lets us continue the on the accounts SQL PR while we iterate on the second unit here which is proto/CLI design).
  2. add proto definitions & call the new code from the rpc server code
  3. add CLI calling code

the high level pattern im describing here is:

  1. server implements new functionality but doesnt expose it yet
  2. server exposes new functionality
  3. client starts calling new functionality

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the suggestion! I've tried to follow this with the introduction of the new #973 & #974 PRs.

// account database.
func (s *RPCServer) UpdateBalance(ctx context.Context,
req *litrpc.UpdateAccountBalanceRequest) (*litrpc.Account, error) {

var (
isAddition bool
amount lnwire.MilliSatoshi
)

switch reqType := req.GetUpdate().(type) {
case *litrpc.UpdateAccountBalanceRequest_Add:
log.Infof("[addbalance] id=%s, label=%v, amount=%d",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think of using "credit/debit" instead of "add/deduct"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah definitely! I was struggling with naming, and agree that credit & debit is better.

req.Id, req.Label, reqType.Add)

isAddition = true
amount = lnwire.MilliSatoshi(reqType.Add * 1000)

case *litrpc.UpdateAccountBalanceRequest_Deduct:
log.Infof("[deductbalance] id=%s, label=%v, amount=%d",
req.Id, req.Label, reqType.Deduct)

isAddition = false
amount = lnwire.MilliSatoshi(reqType.Deduct * 1000)
}

if amount <= 0 {
return nil, fmt.Errorf("amount %v must be greater than 0",
int64(amount/1000))
}
Comment on lines +161 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use uints in the proto definitions then?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Addressed in the new PR.


accountID, err := s.findAccount(ctx, req.Id, req.Label)
if err != nil {
return nil, err
}

// Ask the service to update the account.
account, err := s.service.UpdateBalance(
ctx, accountID, amount, isAddition,
)
if err != nil {
return nil, err
}

return marshalAccount(account), nil
}

// ListAccounts returns all accounts that are currently stored in the account
// database.
func (s *RPCServer) ListAccounts(ctx context.Context,
Expand Down
26 changes: 26 additions & 0 deletions accounts/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,32 @@ func (s *InterceptorService) UpdateAccount(ctx context.Context,
return s.store.Account(ctx, accountID)
}

// UpdateBalance adds or deducts an amount from an existing account in the
// account database.
func (s *InterceptorService) UpdateBalance(ctx context.Context,
accountID AccountID, amount lnwire.MilliSatoshi,
isAddition bool) (*OffChainBalanceAccount, error) {

s.Lock()
defer s.Unlock()

// As this function updates account balances, we require that the
// service is running before we execute it.
if !s.isRunningUnsafe() {
// This case can only happen if the service is disabled while
// we're processing a request.
return nil, ErrAccountServiceDisabled
}

// Adjust the balance of the account in the db.
err := s.store.AdjustAccountBalance(ctx, accountID, amount, isAddition)
if err != nil {
return nil, fmt.Errorf("unable to update account: %w", err)
}

return s.store.Account(ctx, accountID)
}

// Account retrieves an account from the bolt DB and un-marshals it. If the
// account cannot be found, then ErrAccNotFound is returned.
func (s *InterceptorService) Account(ctx context.Context,
Expand Down
39 changes: 39 additions & 0 deletions accounts/store_kvdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,45 @@ func (s *BoltStore) UpsertAccountPayment(_ context.Context, id AccountID,
return known, s.updateAccount(id, update)
}

// AdjustAccountBalance modifies the given account balance by adding or
// deducting the specified amount, depending on whether isAddition is true or
// false.
//
// NOTE: This is part of the Store interface.
func (s *BoltStore) AdjustAccountBalance(_ context.Context,
id AccountID, amount lnwire.MilliSatoshi, isAddition bool) error {

update := func(account *OffChainBalanceAccount) error {
if amount > math.MaxInt64 {
return fmt.Errorf("amount %v exceeds the maximum of %v",
int64(amount/1000), int64(math.MaxInt64)/1000)
}

if amount <= 0 {
return fmt.Errorf("amount %v must be greater that 0",
amount)
}

if isAddition {
account.CurrentBalance += int64(amount)
} else {
if account.CurrentBalance-int64(amount) < 0 {
return fmt.Errorf("cannot deduct %v from the "+
"current balance %v, as the resulting "+
"balance would be below 0",
int64(amount/1000),
account.CurrentBalance/1000)
}

account.CurrentBalance -= int64(amount)
}

return nil
}

return s.updateAccount(id, update)
}

// DeleteAccountPayment removes a payment entry from the account with the given
// ID. It will return the ErrPaymentNotAssociated error if the payment is not
// associated with the account.
Expand Down
Loading