From 32eb4c53d7177060d96e004f71764b168723f53d Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 5 Jan 2025 08:57:19 +0200 Subject: [PATCH 01/10] accounts: add SQL store implementation In this commit, we add the SQLStore type which implements the accounts.Store interface. To demonstrate that it works as expected, we also plug this implementation into all the account unit tests to show that they pass against the sqlite and postgres backends. One can use `make unit pkg=accounts tags=test_db_postgres` or `make unit pkg=accounts tags=test_db_sqlite` to test locally. Note that 2 small timestamp related changes are made to the unit tests. This is to compensate for timestamp precision in postgres. --- accounts/interface.go | 27 ++ accounts/store_sql.go | 704 ++++++++++++++++++++++++++++++++++++++ accounts/store_test.go | 6 +- accounts/test_kvdb.go | 2 + accounts/test_postgres.go | 28 ++ accounts/test_sqlite.go | 30 ++ 6 files changed, 794 insertions(+), 3 deletions(-) create mode 100644 accounts/store_sql.go create mode 100644 accounts/test_postgres.go create mode 100644 accounts/test_sqlite.go diff --git a/accounts/interface.go b/accounts/interface.go index 11d3efe93..5677a08fd 100644 --- a/accounts/interface.go +++ b/accounts/interface.go @@ -1,7 +1,9 @@ package accounts import ( + "bytes" "context" + "encoding/binary" "encoding/hex" "errors" "fmt" @@ -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[:]) diff --git a/accounts/store_sql.go b/accounts/store_sql.go new file mode 100644 index 000000000..2498d2aa1 --- /dev/null +++ b/accounts/store_sql.go @@ -0,0 +1,704 @@ +package accounts + +import ( + "context" + "crypto/rand" + "database/sql" + "encoding/hex" + "errors" + "fmt" + "time" + + "github.com/lightninglabs/lightning-terminal/db" + "github.com/lightninglabs/lightning-terminal/db/sqlc" + "github.com/lightningnetwork/lnd/clock" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" +) + +const ( + // addIndexName is the name of the key under which we store the last + // known invoice add index in the accounts_indices table. + addIndexName = "last_add_index" + + // settleIndexName is the name of the key under which we store the + // last known invoice settle index in the accounts_indices table. + settleIndexName = "last_settle_index" +) + +// SQLQueries is a subset of the sqlc.Queries interface that can be used +// to interact with accounts related tables. +// +//nolint:lll +type SQLQueries interface { + AddAccountInvoice(ctx context.Context, arg sqlc.AddAccountInvoiceParams) error + DeleteAccount(ctx context.Context, id int64) error + DeleteAccountPayment(ctx context.Context, arg sqlc.DeleteAccountPaymentParams) error + GetAccount(ctx context.Context, id int64) (sqlc.Account, error) + GetAccountByLabel(ctx context.Context, label sql.NullString) (sqlc.Account, error) + GetAccountIDByAlias(ctx context.Context, alias int64) (int64, error) + GetAccountIndex(ctx context.Context, name string) (int64, error) + GetAccountPayment(ctx context.Context, arg sqlc.GetAccountPaymentParams) (sqlc.AccountPayment, error) + InsertAccount(ctx context.Context, arg sqlc.InsertAccountParams) (int64, error) + ListAccountInvoices(ctx context.Context, id int64) ([]sqlc.AccountInvoice, error) + ListAccountPayments(ctx context.Context, id int64) ([]sqlc.AccountPayment, error) + ListAllAccounts(ctx context.Context) ([]sqlc.Account, error) + SetAccountIndex(ctx context.Context, arg sqlc.SetAccountIndexParams) error + UpdateAccountBalance(ctx context.Context, arg sqlc.UpdateAccountBalanceParams) (int64, error) + UpdateAccountExpiry(ctx context.Context, arg sqlc.UpdateAccountExpiryParams) (int64, error) + UpdateAccountLastUpdate(ctx context.Context, arg sqlc.UpdateAccountLastUpdateParams) (int64, error) + UpsertAccountPayment(ctx context.Context, arg sqlc.UpsertAccountPaymentParams) error + GetAccountInvoice(ctx context.Context, arg sqlc.GetAccountInvoiceParams) (sqlc.AccountInvoice, error) +} + +// BatchedSQLQueries is a version of the SQLActionQueries that's capable +// of batched database operations. +type BatchedSQLQueries interface { + SQLQueries + + db.BatchedTx[SQLQueries] +} + +// SQLStore represents a storage backend. +type SQLStore struct { + // db is all the higher level queries that the SQLStore has access to + // in order to implement all its CRUD logic. + db BatchedSQLQueries + + // DB represents the underlying database connection. + *sql.DB + + clock clock.Clock +} + +// NewSQLStore creates a new SQLStore instance given an open BatchedSQLQueries +// storage backend. +func NewSQLStore(sqlDB *db.BaseDB, clock clock.Clock) *SQLStore { + executor := db.NewTransactionExecutor( + sqlDB, func(tx *sql.Tx) SQLQueries { + return sqlDB.WithTx(tx) + }, + ) + + return &SQLStore{ + db: executor, + DB: sqlDB.DB, + clock: clock, + } +} + +// NewAccount creates and persists a new OffChainBalanceAccount with the given +// balance and a randomly chosen ID. If the given label is not empty, then it +// must be unique; if it is not, then ErrLabelAlreadyExists is returned. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) NewAccount(ctx context.Context, balance lnwire.MilliSatoshi, + expirationDate time.Time, label string) (*OffChainBalanceAccount, + error) { + + // Ensure that if a label is set, it can't be mistaken for a hex + // encoded account ID to avoid confusion and make it easier for the CLI + // to distinguish between the two. + var labelVal sql.NullString + if len(label) > 0 { + if _, err := hex.DecodeString(label); err == nil && + len(label) == hex.EncodedLen(AccountIDLen) { + + return nil, fmt.Errorf("the label '%s' is not allowed "+ + "as it can be mistaken for an account ID", + label) + } + + labelVal = sql.NullString{ + String: label, + Valid: true, + } + } + + var ( + writeTxOpts db.QueriesTxOptions + account *OffChainBalanceAccount + ) + err := s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + // First, find a unique alias (this is what the ID was in the + // kvdb implementation of the DB). + alias, err := uniqueRandomAccountAlias(ctx, db) + if err != nil { + return err + } + + if labelVal.Valid { + _, err = db.GetAccountByLabel(ctx, labelVal) + if err == nil { + return ErrLabelAlreadyExists + } else if !errors.Is(err, sql.ErrNoRows) { + return err + } + } + + id, err := db.InsertAccount(ctx, sqlc.InsertAccountParams{ + Type: int16(TypeInitialBalance), + InitialBalanceMsat: int64(balance), + CurrentBalanceMsat: int64(balance), + Expiration: expirationDate, + LastUpdated: s.clock.Now().UTC(), + Label: labelVal, + Alias: alias, + }) + if err != nil { + return fmt.Errorf("inserting account: %w", err) + } + + account, err = getAndMarshalAccount(ctx, db, id) + if err != nil { + return fmt.Errorf("fetching account: %w", err) + } + + return nil + }) + if err != nil { + return nil, err + } + + return account, nil +} + +// getAndMarshalAccount retrieves the account with the given ID. If the account +// cannot be found, then ErrAccNotFound is returned. +func getAndMarshalAccount(ctx context.Context, db SQLQueries, id int64) ( + *OffChainBalanceAccount, error) { + + dbAcct, err := db.GetAccount(ctx, id) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAccNotFound + } else if err != nil { + return nil, err + } + + return marshalDBAccount(ctx, db, dbAcct) +} + +func marshalDBAccount(ctx context.Context, db SQLQueries, + dbAcct sqlc.Account) (*OffChainBalanceAccount, error) { + + alias, err := AccountIDFromInt64(dbAcct.Alias) + if err != nil { + return nil, err + } + + account := &OffChainBalanceAccount{ + ID: alias, + Type: AccountType(dbAcct.Type), + InitialBalance: lnwire.MilliSatoshi(dbAcct.InitialBalanceMsat), + CurrentBalance: dbAcct.CurrentBalanceMsat, + LastUpdate: dbAcct.LastUpdated.UTC(), + ExpirationDate: dbAcct.Expiration.UTC(), + Invoices: make(AccountInvoices), + Payments: make(AccountPayments), + Label: dbAcct.Label.String, + } + + invoices, err := db.ListAccountInvoices(ctx, dbAcct.ID) + if err != nil { + return nil, err + } + for _, invoice := range invoices { + var hash lntypes.Hash + copy(hash[:], invoice.Hash) + account.Invoices[hash] = struct{}{} + } + + payments, err := db.ListAccountPayments(ctx, dbAcct.ID) + if err != nil { + return nil, err + } + + for _, payment := range payments { + var hash lntypes.Hash + copy(hash[:], payment.Hash) + account.Payments[hash] = &PaymentEntry{ + Status: lnrpc.Payment_PaymentStatus(payment.Status), + FullAmount: lnwire.MilliSatoshi(payment.FullAmountMsat), + } + } + + return account, nil +} + +// uniqueRandomAccountAlias generates a random account alias that is not already +// in use. An account "alias" is a unique 8 byte identifier (which corresponds +// to the AccountID type) that is used to identify accounts in the database. The +// reason for using this alias in addition to the SQL auto-incremented ID is to +// remain backwards compatible with the kvdb implementation of the DB which only +// used the alias. +func uniqueRandomAccountAlias(ctx context.Context, db SQLQueries) (int64, + error) { + + var ( + newAlias AccountID + numTries = 10 + ) + for numTries > 0 { + if _, err := rand.Read(newAlias[:]); err != nil { + return 0, err + } + + newAliasID, err := newAlias.ToInt64() + if err != nil { + return 0, err + } + + _, err = db.GetAccountIDByAlias(ctx, newAliasID) + if errors.Is(err, sql.ErrNoRows) { + // No account found with this new ID, we can use it. + return newAliasID, nil + } else if err != nil { + return 0, err + } + + numTries-- + } + + return 0, fmt.Errorf("couldn't create new account ID") +} + +// AddAccountInvoice adds and invoice hash to the account with the given +// AccountID alias. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) AddAccountInvoice(ctx context.Context, alias AccountID, + hash lntypes.Hash) error { + + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + acctID, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + // First check that this invoice does not already exist. + _, err = db.GetAccountInvoice(ctx, sqlc.GetAccountInvoiceParams{ + AccountID: acctID, + Hash: hash[:], + }) + // If it does, there is nothing left to do. + if err == nil { + return nil + } else if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + err = db.AddAccountInvoice(ctx, sqlc.AddAccountInvoiceParams{ + AccountID: acctID, + Hash: hash[:], + }) + if err != nil { + return err + } + + return s.markAccountUpdated(ctx, db, acctID) + }) +} + +func getAccountIDByAlias(ctx context.Context, db SQLQueries, alias AccountID) ( + int64, error) { + + aliasInt, err := alias.ToInt64() + if err != nil { + return 0, fmt.Errorf("error converting account alias into "+ + "int64: %w", err) + } + + acctID, err := db.GetAccountIDByAlias(ctx, aliasInt) + if errors.Is(err, sql.ErrNoRows) { + return 0, ErrAccNotFound + } + + return acctID, err +} + +// markAccountUpdated is a helper that updates the last updated timestamp of +// the account with the given ID. +func (s *SQLStore) markAccountUpdated(ctx context.Context, + db SQLQueries, id int64) error { + + _, err := db.UpdateAccountLastUpdate( + ctx, sqlc.UpdateAccountLastUpdateParams{ + ID: id, + LastUpdated: s.clock.Now().UTC(), + }, + ) + if errors.Is(err, sql.ErrNoRows) { + return ErrAccNotFound + } + + return err +} + +// UpdateAccountBalanceAndExpiry updates the balance and/or expiry of an +// account. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) UpdateAccountBalanceAndExpiry(ctx context.Context, + alias AccountID, newBalance fn.Option[int64], + newExpiry fn.Option[time.Time]) error { + + var ( + writeTxOpts db.QueriesTxOptions + ) + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + newBalance.WhenSome(func(i int64) { + _, err = db.UpdateAccountBalance( + ctx, sqlc.UpdateAccountBalanceParams{ + ID: id, + CurrentBalanceMsat: i, + }, + ) + }) + if err != nil { + return err + } + + newExpiry.WhenSome(func(t time.Time) { + _, err = db.UpdateAccountExpiry( + ctx, sqlc.UpdateAccountExpiryParams{ + ID: id, + Expiration: t.UTC(), + }, + ) + }) + if err != nil { + return err + } + + return s.markAccountUpdated(ctx, db, id) + }) +} + +// IncreaseAccountBalance increases the balance of the account with the given +// alias by the given amount. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) IncreaseAccountBalance(ctx context.Context, alias AccountID, + amount lnwire.MilliSatoshi) error { + + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + acct, err := db.GetAccount(ctx, id) + if err != nil { + return err + } + + newBalance := acct.CurrentBalanceMsat + int64(amount) + + _, err = db.UpdateAccountBalance( + ctx, sqlc.UpdateAccountBalanceParams{ + ID: id, + CurrentBalanceMsat: newBalance, + }, + ) + if err != nil { + return err + } + + return s.markAccountUpdated(ctx, db, id) + }) +} + +// Account retrieves an account from the SQL store and un-marshals it. If the +// account cannot be found, then ErrAccNotFound is returned. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) Account(ctx context.Context, alias AccountID) ( + *OffChainBalanceAccount, error) { + + var ( + readTxOpts = db.NewQueryReadTx() + account *OffChainBalanceAccount + ) + err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + account, err = getAndMarshalAccount(ctx, db, id) + return err + }) + + return account, err +} + +// Accounts retrieves all accounts from the SQL store and un-marshals them. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) Accounts(ctx context.Context) ([]*OffChainBalanceAccount, + error) { + + var ( + readTxOpts = db.NewQueryReadTx() + accounts []*OffChainBalanceAccount + ) + err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error { + dbAccounts, err := db.ListAllAccounts(ctx) + if err != nil { + return err + } + + accounts = make([]*OffChainBalanceAccount, len(dbAccounts)) + for i, dbAccount := range dbAccounts { + account, err := marshalDBAccount(ctx, db, dbAccount) + if err != nil { + return err + } + + accounts[i] = account + } + + return nil + }) + + return accounts, err +} + +// RemoveAccount finds an account by its ID and removes it from the DB. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) RemoveAccount(ctx context.Context, alias AccountID) error { + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + return db.DeleteAccount(ctx, id) + }) +} + +// 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 +// before the update. This is to be treated as a best-effort indication if an +// error is also returned since the method may error before the boolean can be +// set correctly. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) UpsertAccountPayment(ctx context.Context, alias AccountID, + hash lntypes.Hash, fullAmount lnwire.MilliSatoshi, + status lnrpc.Payment_PaymentStatus, + options ...UpsertPaymentOption) (bool, error) { + + opts := newUpsertPaymentOption() + for _, o := range options { + o(opts) + } + + var ( + writeTxOpts db.QueriesTxOptions + known bool + ) + return known, s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + payment, err := db.GetAccountPayment( + ctx, sqlc.GetAccountPaymentParams{ + AccountID: id, + Hash: hash[:], + }, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return err + } + + known = err == nil + + if known { + currStatus := lnrpc.Payment_PaymentStatus( + payment.Status, + ) + if opts.errIfAlreadySucceeded && + successState(currStatus) { + + return ErrAlreadySucceeded + } + + // If the errIfAlreadyPending option is set, we return + // an error if the payment is already in-flight or + // succeeded. + if opts.errIfAlreadyPending && + currStatus != lnrpc.Payment_FAILED { + + return fmt.Errorf("payment with hash %s is "+ + "already in flight or succeeded "+ + "(status %v)", hash, currStatus) + } + + if opts.usePendingAmount { + fullAmount = lnwire.MilliSatoshi( + payment.FullAmountMsat, + ) + } + } else if opts.errIfUnknown { + return ErrPaymentNotAssociated + } + + err = db.UpsertAccountPayment( + ctx, sqlc.UpsertAccountPaymentParams{ + AccountID: id, + Hash: hash[:], + Status: int16(status), + FullAmountMsat: int64(fullAmount), + }, + ) + if err != nil { + return err + } + + if opts.debitAccount { + acct, err := db.GetAccount(ctx, id) + if errors.Is(err, sql.ErrNoRows) { + return ErrAccNotFound + } else if err != nil { + return err + } + + newBalance := acct.CurrentBalanceMsat - + int64(fullAmount) + + _, err = db.UpdateAccountBalance( + ctx, sqlc.UpdateAccountBalanceParams{ + ID: id, + CurrentBalanceMsat: newBalance, + }, + ) + if errors.Is(err, sql.ErrNoRows) { + return ErrAccNotFound + } else if err != nil { + return err + } + } + + return s.markAccountUpdated(ctx, db, id) + }) +} + +// DeleteAccountPayment removes a payment entry from the account with the given +// ID. It will return an error if the payment is not associated with the +// account. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) DeleteAccountPayment(ctx context.Context, alias AccountID, + hash lntypes.Hash) error { + + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + _, err = db.GetAccountPayment( + ctx, sqlc.GetAccountPaymentParams{ + AccountID: id, + Hash: hash[:], + }, + ) + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("payment with hash %s is not "+ + "associated with this account: %w", hash, + ErrPaymentNotAssociated) + } else if err != nil { + return err + } + + err = db.DeleteAccountPayment( + ctx, sqlc.DeleteAccountPaymentParams{ + AccountID: id, + Hash: hash[:], + }, + ) + if err != nil { + return err + } + + return s.markAccountUpdated(ctx, db, id) + }) +} + +// LastIndexes returns the last invoice add and settle index or +// ErrNoInvoiceIndexKnown if no indexes are known yet. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) LastIndexes(ctx context.Context) (uint64, uint64, error) { + var ( + readTxOpts = db.NewQueryReadTx() + addIndex, settleIndex int64 + ) + err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error { + var err error + addIndex, err = db.GetAccountIndex(ctx, addIndexName) + if errors.Is(err, sql.ErrNoRows) { + return ErrNoInvoiceIndexKnown + } else if err != nil { + return err + } + + settleIndex, err = db.GetAccountIndex(ctx, settleIndexName) + if errors.Is(err, sql.ErrNoRows) { + return ErrNoInvoiceIndexKnown + } + + return err + }) + + return uint64(addIndex), uint64(settleIndex), err +} + +// StoreLastIndexes stores the last invoice add and settle index. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) StoreLastIndexes(ctx context.Context, addIndex, + settleIndex uint64) error { + + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + err := db.SetAccountIndex(ctx, sqlc.SetAccountIndexParams{ + Name: addIndexName, + Value: int64(addIndex), + }) + if err != nil { + return err + } + + return db.SetAccountIndex(ctx, sqlc.SetAccountIndexParams{ + Name: settleIndexName, + Value: int64(settleIndex), + }) + }) +} + +// Close closes the underlying store. +// +// NOTE: This is part of the Store interface. +func (s *SQLStore) Close() error { + return s.DB.Close() +} + +// A compile-time check to ensure that SQLStore implements the Store interface. +var _ Store = (*SQLStore)(nil) diff --git a/accounts/store_test.go b/accounts/store_test.go index 3d44df3e7..92d4fe5ba 100644 --- a/accounts/store_test.go +++ b/accounts/store_test.go @@ -130,8 +130,8 @@ func assertEqualAccounts(t *testing.T, expected, actual.LastUpdate = time.Time{} require.Equal(t, expected, actual) - require.Equal(t, expectedExpiry.UnixNano(), actualExpiry.UnixNano()) - require.Equal(t, expectedUpdate.UnixNano(), actualUpdate.UnixNano()) + require.Equal(t, expectedExpiry.Unix(), actualExpiry.Unix()) + require.Equal(t, expectedUpdate.Unix(), actualUpdate.Unix()) // Restore the old values to not influence the tests. expected.ExpirationDate = expectedExpiry @@ -168,7 +168,7 @@ func TestAccountUpdateMethods(t *testing.T) { require.NoError(t, err) require.EqualValues(t, balance, dbAcct.CurrentBalance) require.WithinDuration( - t, expiry, dbAcct.ExpirationDate, 0, + t, expiry, dbAcct.ExpirationDate, time.Second, ) } diff --git a/accounts/test_kvdb.go b/accounts/test_kvdb.go index 224a8912c..99c3e2ae6 100644 --- a/accounts/test_kvdb.go +++ b/accounts/test_kvdb.go @@ -1,3 +1,5 @@ +//go:build !test_db_sqlite && !test_db_postgres + package accounts import ( diff --git a/accounts/test_postgres.go b/accounts/test_postgres.go new file mode 100644 index 000000000..013c18a04 --- /dev/null +++ b/accounts/test_postgres.go @@ -0,0 +1,28 @@ +//go:build test_db_postgres && !test_db_sqlite + +package accounts + +import ( + "errors" + "testing" + + "github.com/lightninglabs/lightning-terminal/db" + "github.com/lightningnetwork/lnd/clock" +) + +// ErrDBClosed is an error that is returned when a database operation is +// performed on a closed database. +var ErrDBClosed = errors.New("database is closed") + +// NewTestDB is a helper function that creates an BBolt database for testing. +func NewTestDB(t *testing.T, clock clock.Clock) *SQLStore { + return NewSQLStore(db.NewTestPostgresDB(t).BaseDB, clock) +} + +// NewTestDBFromPath is a helper function that creates a new BoltStore with a +// connection to an existing BBolt database for testing. +func NewTestDBFromPath(t *testing.T, dbPath string, + clock clock.Clock) *SQLStore { + + return NewSQLStore(db.NewTestPostgresDB(t).BaseDB, clock) +} diff --git a/accounts/test_sqlite.go b/accounts/test_sqlite.go new file mode 100644 index 000000000..07319268d --- /dev/null +++ b/accounts/test_sqlite.go @@ -0,0 +1,30 @@ +//go:build test_db_sqlite && !test_db_postgres + +package accounts + +import ( + "errors" + "testing" + + "github.com/lightninglabs/lightning-terminal/db" + "github.com/lightningnetwork/lnd/clock" +) + +// ErrDBClosed is an error that is returned when a database operation is +// performed on a closed database. +var ErrDBClosed = errors.New("database is closed") + +// NewTestDB is a helper function that creates an BBolt database for testing. +func NewTestDB(t *testing.T, clock clock.Clock) *SQLStore { + return NewSQLStore(db.NewTestSqliteDB(t).BaseDB, clock) +} + +// NewTestDBFromPath is a helper function that creates a new BoltStore with a +// connection to an existing BBolt database for testing. +func NewTestDBFromPath(t *testing.T, dbPath string, + clock clock.Clock) *SQLStore { + + return NewSQLStore( + db.NewTestSqliteDbHandleFromPath(t, dbPath).BaseDB, clock, + ) +} From 21c067752d152e820de5f5660c45225cc072cbd2 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Sun, 5 Jan 2025 09:32:58 +0200 Subject: [PATCH 02/10] make+GH: helpers for testing against new SQL stores --- .github/workflows/main.yml | 2 ++ make/testing_flags.mk | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a826cf91..62393b938 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -253,6 +253,8 @@ jobs: unit_type: - unit-race - unit + - unit dbbackend=postgres + - unit dbbackend=sqlite steps: - name: git checkout uses: actions/checkout@v4 diff --git a/make/testing_flags.mk b/make/testing_flags.mk index 7687a9431..0370bc29f 100644 --- a/make/testing_flags.mk +++ b/make/testing_flags.mk @@ -24,6 +24,16 @@ UNIT_TARGETED = yes GOLIST = echo '$(PKG)/$(pkg)' endif +# Add the build tag for running unit tests against a postgres DB. +ifeq ($(dbbackend),postgres) +DEV_TAGS += test_db_postgres +endif + +# Add the build tag for running unit tests against a sqlite DB. +ifeq ($(dbbackend),sqlite) +DEV_TAGS += test_db_sqlite +endif + # Add any additional tags that are passed in to make. ifneq ($(tags),) DEV_TAGS += ${tags} From 8c24c1a400f4a2d901422309addce4750b66dbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 17:37:20 +0100 Subject: [PATCH 03/10] litrpc: run `go mod tidy` The current go.sum file was out of sync with the go.mod file. This commit updates the go.sum file by running `go mod tidy` to match the go.mod file. --- litrpc/go.sum | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litrpc/go.sum b/litrpc/go.sum index 8a51a441b..3d4d4561e 100644 --- a/litrpc/go.sum +++ b/litrpc/go.sum @@ -1510,8 +1510,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= From 755068b82ff12d025a3a423596bbb5d94367fa51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 17:46:27 +0100 Subject: [PATCH 04/10] multi: add accounts `UpdateBalance` endpoint This commit introduces the structure for a new endpoint in the accounts subsystem, enabling balance adjustments by adding or deducting a specified amount. This contrasts with the existing `UpdateAccount` implementation, which directly sets the balance to a fixed value. For more details on the drawbacks of the current implementation, see [issue #648](https://github.com/lightninglabs/lightning-terminal/issues/648). The actual implementation of the endpoint will be introduced in subsequent commits. --- accounts/rpcserver.go | 8 + app/src/types/generated/lit-accounts_pb.d.ts | 43 +++ app/src/types/generated/lit-accounts_pb.js | 305 ++++++++++++++++++ .../generated/lit-accounts_pb_service.d.ts | 19 ++ .../generated/lit-accounts_pb_service.js | 40 +++ litrpc/accounts.pb.json.go | 25 ++ litrpc/lit-accounts.pb.go | 305 +++++++++++++----- litrpc/lit-accounts.proto | 28 ++ litrpc/lit-accounts_grpc.pb.go | 42 +++ perms/permissions.go | 4 + proto/lit-accounts.proto | 28 ++ 11 files changed, 762 insertions(+), 85 deletions(-) diff --git a/accounts/rpcserver.go b/accounts/rpcserver.go index 79e3e674b..48f3a229e 100644 --- a/accounts/rpcserver.go +++ b/accounts/rpcserver.go @@ -132,6 +132,14 @@ func (s *RPCServer) UpdateAccount(ctx context.Context, return marshalAccount(account), nil } +// UpdateBalance adds or deducts an amount from an existing account in the +// account database. +func (s *RPCServer) UpdateBalance(ctx context.Context, + req *litrpc.UpdateAccountBalanceRequest) (*litrpc.Account, error) { + + return nil, fmt.Errorf("not implemented") +} + // ListAccounts returns all accounts that are currently stored in the account // database. func (s *RPCServer) ListAccounts(ctx context.Context, diff --git a/app/src/types/generated/lit-accounts_pb.d.ts b/app/src/types/generated/lit-accounts_pb.d.ts index d461827ce..350c43fc2 100644 --- a/app/src/types/generated/lit-accounts_pb.d.ts +++ b/app/src/types/generated/lit-accounts_pb.d.ts @@ -195,6 +195,49 @@ export namespace UpdateAccountRequest { } } +export class UpdateAccountBalanceRequest extends jspb.Message { + getId(): string; + setId(value: string): void; + + getLabel(): string; + setLabel(value: string): void; + + hasAdd(): boolean; + clearAdd(): void; + getAdd(): string; + setAdd(value: string): void; + + hasDeduct(): boolean; + clearDeduct(): void; + getDeduct(): string; + setDeduct(value: string): void; + + getUpdateCase(): UpdateAccountBalanceRequest.UpdateCase; + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): UpdateAccountBalanceRequest.AsObject; + static toObject(includeInstance: boolean, msg: UpdateAccountBalanceRequest): UpdateAccountBalanceRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: UpdateAccountBalanceRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): UpdateAccountBalanceRequest; + static deserializeBinaryFromReader(message: UpdateAccountBalanceRequest, reader: jspb.BinaryReader): UpdateAccountBalanceRequest; +} + +export namespace UpdateAccountBalanceRequest { + export type AsObject = { + id: string, + label: string, + add: string, + deduct: string, + } + + export enum UpdateCase { + UPDATE_NOT_SET = 0, + ADD = 3, + DEDUCT = 4, + } +} + export class ListAccountsRequest extends jspb.Message { serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ListAccountsRequest.AsObject; diff --git a/app/src/types/generated/lit-accounts_pb.js b/app/src/types/generated/lit-accounts_pb.js index 3d118ebbd..139ee3fde 100644 --- a/app/src/types/generated/lit-accounts_pb.js +++ b/app/src/types/generated/lit-accounts_pb.js @@ -34,6 +34,8 @@ goog.exportSymbol('proto.litrpc.ListAccountsRequest', null, global); goog.exportSymbol('proto.litrpc.ListAccountsResponse', null, global); goog.exportSymbol('proto.litrpc.RemoveAccountRequest', null, global); goog.exportSymbol('proto.litrpc.RemoveAccountResponse', null, global); +goog.exportSymbol('proto.litrpc.UpdateAccountBalanceRequest', null, global); +goog.exportSymbol('proto.litrpc.UpdateAccountBalanceRequest.UpdateCase', null, global); goog.exportSymbol('proto.litrpc.UpdateAccountRequest', null, global); /** * Generated by JsPbCodeGenerator. @@ -161,6 +163,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.litrpc.UpdateAccountRequest.displayName = 'proto.litrpc.UpdateAccountRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.litrpc.UpdateAccountBalanceRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_); +}; +goog.inherits(proto.litrpc.UpdateAccountBalanceRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.litrpc.UpdateAccountBalanceRequest.displayName = 'proto.litrpc.UpdateAccountBalanceRequest'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -1643,6 +1666,288 @@ proto.litrpc.UpdateAccountRequest.prototype.setLabel = function(value) { +/** + * Oneof group definitions for this message. Each group defines the field + * numbers belonging to that group. When of these fields' value is set, all + * other fields in the group are cleared. During deserialization, if multiple + * fields are encountered for a group, only the last value seen will be kept. + * @private {!Array>} + * @const + */ +proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_ = [[3,4]]; + +/** + * @enum {number} + */ +proto.litrpc.UpdateAccountBalanceRequest.UpdateCase = { + UPDATE_NOT_SET: 0, + ADD: 3, + DEDUCT: 4 +}; + +/** + * @return {proto.litrpc.UpdateAccountBalanceRequest.UpdateCase} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.getUpdateCase = function() { + return /** @type {proto.litrpc.UpdateAccountBalanceRequest.UpdateCase} */(jspb.Message.computeOneofCase(this, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_[0])); +}; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.UpdateAccountBalanceRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.litrpc.UpdateAccountBalanceRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.UpdateAccountBalanceRequest.toObject = function(includeInstance, msg) { + var f, obj = { + id: jspb.Message.getFieldWithDefault(msg, 1, ""), + label: jspb.Message.getFieldWithDefault(msg, 2, ""), + add: jspb.Message.getFieldWithDefault(msg, 3, "0"), + deduct: jspb.Message.getFieldWithDefault(msg, 4, "0") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.litrpc.UpdateAccountBalanceRequest} + */ +proto.litrpc.UpdateAccountBalanceRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.UpdateAccountBalanceRequest; + return proto.litrpc.UpdateAccountBalanceRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.UpdateAccountBalanceRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.UpdateAccountBalanceRequest} + */ +proto.litrpc.UpdateAccountBalanceRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setId(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; + case 3: + var value = /** @type {string} */ (reader.readInt64String()); + msg.setAdd(value); + break; + case 4: + var value = /** @type {string} */ (reader.readInt64String()); + msg.setDeduct(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.UpdateAccountBalanceRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.litrpc.UpdateAccountBalanceRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.UpdateAccountBalanceRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getId(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeInt64String( + 3, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeInt64String( + 4, + f + ); + } +}; + + +/** + * optional string id = 1; + * @return {string} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.getId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.setId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string label = 2; + * @return {string} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * @param {string} value + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.setLabel = function(value) { + return jspb.Message.setProto3StringField(this, 2, value); +}; + + +/** + * optional int64 add = 3; + * @return {string} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.getAdd = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.setAdd = function(value) { + return jspb.Message.setOneofField(this, 3, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.clearAdd = function() { + return jspb.Message.setOneofField(this, 3, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.hasAdd = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional int64 deduct = 4; + * @return {string} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.getDeduct = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.setDeduct = function(value) { + return jspb.Message.setOneofField(this, 4, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_[0], value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.litrpc.UpdateAccountBalanceRequest} returns this + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.clearDeduct = function() { + return jspb.Message.setOneofField(this, 4, proto.litrpc.UpdateAccountBalanceRequest.oneofGroups_[0], undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.litrpc.UpdateAccountBalanceRequest.prototype.hasDeduct = function() { + return jspb.Message.getField(this, 4) != null; +}; + + + if (jspb.Message.GENERATE_TO_OBJECT) { diff --git a/app/src/types/generated/lit-accounts_pb_service.d.ts b/app/src/types/generated/lit-accounts_pb_service.d.ts index 448e29a9d..e2053de4e 100644 --- a/app/src/types/generated/lit-accounts_pb_service.d.ts +++ b/app/src/types/generated/lit-accounts_pb_service.d.ts @@ -22,6 +22,15 @@ type AccountsUpdateAccount = { readonly responseType: typeof lit_accounts_pb.Account; }; +type AccountsUpdateBalance = { + readonly methodName: string; + readonly service: typeof Accounts; + readonly requestStream: false; + readonly responseStream: false; + readonly requestType: typeof lit_accounts_pb.UpdateAccountBalanceRequest; + readonly responseType: typeof lit_accounts_pb.Account; +}; + type AccountsListAccounts = { readonly methodName: string; readonly service: typeof Accounts; @@ -53,6 +62,7 @@ export class Accounts { static readonly serviceName: string; static readonly CreateAccount: AccountsCreateAccount; static readonly UpdateAccount: AccountsUpdateAccount; + static readonly UpdateBalance: AccountsUpdateBalance; static readonly ListAccounts: AccountsListAccounts; static readonly AccountInfo: AccountsAccountInfo; static readonly RemoveAccount: AccountsRemoveAccount; @@ -108,6 +118,15 @@ export class AccountsClient { requestMessage: lit_accounts_pb.UpdateAccountRequest, callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.Account|null) => void ): UnaryResponse; + updateBalance( + requestMessage: lit_accounts_pb.UpdateAccountBalanceRequest, + metadata: grpc.Metadata, + callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.Account|null) => void + ): UnaryResponse; + updateBalance( + requestMessage: lit_accounts_pb.UpdateAccountBalanceRequest, + callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.Account|null) => void + ): UnaryResponse; listAccounts( requestMessage: lit_accounts_pb.ListAccountsRequest, metadata: grpc.Metadata, diff --git a/app/src/types/generated/lit-accounts_pb_service.js b/app/src/types/generated/lit-accounts_pb_service.js index ed4583b95..b7d072fdb 100644 --- a/app/src/types/generated/lit-accounts_pb_service.js +++ b/app/src/types/generated/lit-accounts_pb_service.js @@ -28,6 +28,15 @@ Accounts.UpdateAccount = { responseType: lit_accounts_pb.Account }; +Accounts.UpdateBalance = { + methodName: "UpdateBalance", + service: Accounts, + requestStream: false, + responseStream: false, + requestType: lit_accounts_pb.UpdateAccountBalanceRequest, + responseType: lit_accounts_pb.Account +}; + Accounts.ListAccounts = { methodName: "ListAccounts", service: Accounts, @@ -124,6 +133,37 @@ AccountsClient.prototype.updateAccount = function updateAccount(requestMessage, }; }; +AccountsClient.prototype.updateBalance = function updateBalance(requestMessage, metadata, callback) { + if (arguments.length === 2) { + callback = arguments[1]; + } + var client = grpc.unary(Accounts.UpdateBalance, { + request: requestMessage, + host: this.serviceHost, + metadata: metadata, + transport: this.options.transport, + debug: this.options.debug, + onEnd: function (response) { + if (callback) { + if (response.status !== grpc.Code.OK) { + var err = new Error(response.statusMessage); + err.code = response.status; + err.metadata = response.trailers; + callback(err, null); + } else { + callback(null, response.message); + } + } + } + }); + return { + cancel: function () { + callback = null; + client.close(); + } + }; +}; + AccountsClient.prototype.listAccounts = function listAccounts(requestMessage, metadata, callback) { if (arguments.length === 2) { callback = arguments[1]; diff --git a/litrpc/accounts.pb.json.go b/litrpc/accounts.pb.json.go index f8cf5c089..08a9dcd3f 100644 --- a/litrpc/accounts.pb.json.go +++ b/litrpc/accounts.pb.json.go @@ -71,6 +71,31 @@ func RegisterAccountsJSONCallbacks(registry map[string]func(ctx context.Context, callback(string(respBytes), nil) } + registry["litrpc.Accounts.UpdateBalance"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &UpdateAccountBalanceRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.UpdateBalance(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["litrpc.Accounts.ListAccounts"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { diff --git a/litrpc/lit-accounts.pb.go b/litrpc/lit-accounts.pb.go index 903a8473e..ce1a05c32 100644 --- a/litrpc/lit-accounts.pb.go +++ b/litrpc/lit-accounts.pb.go @@ -452,6 +452,110 @@ func (x *UpdateAccountRequest) GetLabel() string { return "" } +type UpdateAccountBalanceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ID of the account to update. Either the ID or the label must be set. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // The label of the account to update. If an account has no label, then the ID + // must be used instead. + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + // The balance update must either be an increase or a decrease of the balance + // + // Types that are assignable to Update: + // + // *UpdateAccountBalanceRequest_Add + // *UpdateAccountBalanceRequest_Deduct + Update isUpdateAccountBalanceRequest_Update `protobuf_oneof:"update"` +} + +func (x *UpdateAccountBalanceRequest) Reset() { + *x = UpdateAccountBalanceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_accounts_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateAccountBalanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAccountBalanceRequest) ProtoMessage() {} + +func (x *UpdateAccountBalanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_accounts_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateAccountBalanceRequest.ProtoReflect.Descriptor instead. +func (*UpdateAccountBalanceRequest) Descriptor() ([]byte, []int) { + return file_lit_accounts_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateAccountBalanceRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateAccountBalanceRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (m *UpdateAccountBalanceRequest) GetUpdate() isUpdateAccountBalanceRequest_Update { + if m != nil { + return m.Update + } + return nil +} + +func (x *UpdateAccountBalanceRequest) GetAdd() int64 { + if x, ok := x.GetUpdate().(*UpdateAccountBalanceRequest_Add); ok { + return x.Add + } + return 0 +} + +func (x *UpdateAccountBalanceRequest) GetDeduct() int64 { + if x, ok := x.GetUpdate().(*UpdateAccountBalanceRequest_Deduct); ok { + return x.Deduct + } + return 0 +} + +type isUpdateAccountBalanceRequest_Update interface { + isUpdateAccountBalanceRequest_Update() +} + +type UpdateAccountBalanceRequest_Add struct { + // Increase the account's balance by the given amount. + Add int64 `protobuf:"varint,3,opt,name=add,proto3,oneof"` +} + +type UpdateAccountBalanceRequest_Deduct struct { + // Deducts the given amount from the account balance. + Deduct int64 `protobuf:"varint,4,opt,name=deduct,proto3,oneof"` +} + +func (*UpdateAccountBalanceRequest_Add) isUpdateAccountBalanceRequest_Update() {} + +func (*UpdateAccountBalanceRequest_Deduct) isUpdateAccountBalanceRequest_Update() {} + type ListAccountsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -461,7 +565,7 @@ type ListAccountsRequest struct { func (x *ListAccountsRequest) Reset() { *x = ListAccountsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lit_accounts_proto_msgTypes[6] + mi := &file_lit_accounts_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -474,7 +578,7 @@ func (x *ListAccountsRequest) String() string { func (*ListAccountsRequest) ProtoMessage() {} func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { - mi := &file_lit_accounts_proto_msgTypes[6] + mi := &file_lit_accounts_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -487,7 +591,7 @@ func (x *ListAccountsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAccountsRequest.ProtoReflect.Descriptor instead. func (*ListAccountsRequest) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{6} + return file_lit_accounts_proto_rawDescGZIP(), []int{7} } type ListAccountsResponse struct { @@ -502,7 +606,7 @@ type ListAccountsResponse struct { func (x *ListAccountsResponse) Reset() { *x = ListAccountsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lit_accounts_proto_msgTypes[7] + mi := &file_lit_accounts_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -515,7 +619,7 @@ func (x *ListAccountsResponse) String() string { func (*ListAccountsResponse) ProtoMessage() {} func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { - mi := &file_lit_accounts_proto_msgTypes[7] + mi := &file_lit_accounts_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -528,7 +632,7 @@ func (x *ListAccountsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAccountsResponse.ProtoReflect.Descriptor instead. func (*ListAccountsResponse) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{7} + return file_lit_accounts_proto_rawDescGZIP(), []int{8} } func (x *ListAccountsResponse) GetAccounts() []*Account { @@ -554,7 +658,7 @@ type AccountInfoRequest struct { func (x *AccountInfoRequest) Reset() { *x = AccountInfoRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lit_accounts_proto_msgTypes[8] + mi := &file_lit_accounts_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -567,7 +671,7 @@ func (x *AccountInfoRequest) String() string { func (*AccountInfoRequest) ProtoMessage() {} func (x *AccountInfoRequest) ProtoReflect() protoreflect.Message { - mi := &file_lit_accounts_proto_msgTypes[8] + mi := &file_lit_accounts_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -580,7 +684,7 @@ func (x *AccountInfoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AccountInfoRequest.ProtoReflect.Descriptor instead. func (*AccountInfoRequest) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{8} + return file_lit_accounts_proto_rawDescGZIP(), []int{9} } func (x *AccountInfoRequest) GetId() string { @@ -613,7 +717,7 @@ type RemoveAccountRequest struct { func (x *RemoveAccountRequest) Reset() { *x = RemoveAccountRequest{} if protoimpl.UnsafeEnabled { - mi := &file_lit_accounts_proto_msgTypes[9] + mi := &file_lit_accounts_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -626,7 +730,7 @@ func (x *RemoveAccountRequest) String() string { func (*RemoveAccountRequest) ProtoMessage() {} func (x *RemoveAccountRequest) ProtoReflect() protoreflect.Message { - mi := &file_lit_accounts_proto_msgTypes[9] + mi := &file_lit_accounts_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -639,7 +743,7 @@ func (x *RemoveAccountRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveAccountRequest.ProtoReflect.Descriptor instead. func (*RemoveAccountRequest) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{9} + return file_lit_accounts_proto_rawDescGZIP(), []int{10} } func (x *RemoveAccountRequest) GetId() string { @@ -665,7 +769,7 @@ type RemoveAccountResponse struct { func (x *RemoveAccountResponse) Reset() { *x = RemoveAccountResponse{} if protoimpl.UnsafeEnabled { - mi := &file_lit_accounts_proto_msgTypes[10] + mi := &file_lit_accounts_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -678,7 +782,7 @@ func (x *RemoveAccountResponse) String() string { func (*RemoveAccountResponse) ProtoMessage() {} func (x *RemoveAccountResponse) ProtoReflect() protoreflect.Message { - mi := &file_lit_accounts_proto_msgTypes[10] + mi := &file_lit_accounts_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -691,7 +795,7 @@ func (x *RemoveAccountResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveAccountResponse.ProtoReflect.Descriptor instead. func (*RemoveAccountResponse) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{10} + return file_lit_accounts_proto_rawDescGZIP(), []int{11} } var File_lit_accounts_proto protoreflect.FileDescriptor @@ -749,49 +853,61 @@ var file_lit_accounts_proto_rawDesc = []byte{ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, - 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x22, 0x3a, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, - 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x3c, - 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x17, 0x0a, 0x15, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xed, 0x02, 0x0a, 0x08, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x12, 0x4c, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x3e, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x49, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x12, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, - 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x2e, 0x6c, 0x69, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, - 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, - 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, 0x74, 0x65, 0x72, 0x6d, - 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x7b, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x03, 0x61, + 0x64, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, + 0x18, 0x0a, 0x06, 0x64, 0x65, 0x64, 0x75, 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x48, + 0x00, 0x52, 0x06, 0x64, 0x65, 0x64, 0x75, 0x63, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x14, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, + 0x3a, 0x0a, 0x12, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x3c, 0x0a, 0x14, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x32, 0xb4, 0x03, 0x0a, 0x08, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, + 0x4c, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, + 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x45, 0x0a, + 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x23, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3a, 0x0a, 0x0b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, + 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x4c, 0x0a, 0x0d, 0x52, + 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x6c, + 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, + 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x2d, + 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x61, 0x6c, 0x2f, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -806,19 +922,20 @@ func file_lit_accounts_proto_rawDescGZIP() []byte { return file_lit_accounts_proto_rawDescData } -var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_lit_accounts_proto_goTypes = []any{ - (*CreateAccountRequest)(nil), // 0: litrpc.CreateAccountRequest - (*CreateAccountResponse)(nil), // 1: litrpc.CreateAccountResponse - (*Account)(nil), // 2: litrpc.Account - (*AccountInvoice)(nil), // 3: litrpc.AccountInvoice - (*AccountPayment)(nil), // 4: litrpc.AccountPayment - (*UpdateAccountRequest)(nil), // 5: litrpc.UpdateAccountRequest - (*ListAccountsRequest)(nil), // 6: litrpc.ListAccountsRequest - (*ListAccountsResponse)(nil), // 7: litrpc.ListAccountsResponse - (*AccountInfoRequest)(nil), // 8: litrpc.AccountInfoRequest - (*RemoveAccountRequest)(nil), // 9: litrpc.RemoveAccountRequest - (*RemoveAccountResponse)(nil), // 10: litrpc.RemoveAccountResponse + (*CreateAccountRequest)(nil), // 0: litrpc.CreateAccountRequest + (*CreateAccountResponse)(nil), // 1: litrpc.CreateAccountResponse + (*Account)(nil), // 2: litrpc.Account + (*AccountInvoice)(nil), // 3: litrpc.AccountInvoice + (*AccountPayment)(nil), // 4: litrpc.AccountPayment + (*UpdateAccountRequest)(nil), // 5: litrpc.UpdateAccountRequest + (*UpdateAccountBalanceRequest)(nil), // 6: litrpc.UpdateAccountBalanceRequest + (*ListAccountsRequest)(nil), // 7: litrpc.ListAccountsRequest + (*ListAccountsResponse)(nil), // 8: litrpc.ListAccountsResponse + (*AccountInfoRequest)(nil), // 9: litrpc.AccountInfoRequest + (*RemoveAccountRequest)(nil), // 10: litrpc.RemoveAccountRequest + (*RemoveAccountResponse)(nil), // 11: litrpc.RemoveAccountResponse } var file_lit_accounts_proto_depIdxs = []int32{ 2, // 0: litrpc.CreateAccountResponse.account:type_name -> litrpc.Account @@ -827,16 +944,18 @@ var file_lit_accounts_proto_depIdxs = []int32{ 2, // 3: litrpc.ListAccountsResponse.accounts:type_name -> litrpc.Account 0, // 4: litrpc.Accounts.CreateAccount:input_type -> litrpc.CreateAccountRequest 5, // 5: litrpc.Accounts.UpdateAccount:input_type -> litrpc.UpdateAccountRequest - 6, // 6: litrpc.Accounts.ListAccounts:input_type -> litrpc.ListAccountsRequest - 8, // 7: litrpc.Accounts.AccountInfo:input_type -> litrpc.AccountInfoRequest - 9, // 8: litrpc.Accounts.RemoveAccount:input_type -> litrpc.RemoveAccountRequest - 1, // 9: litrpc.Accounts.CreateAccount:output_type -> litrpc.CreateAccountResponse - 2, // 10: litrpc.Accounts.UpdateAccount:output_type -> litrpc.Account - 7, // 11: litrpc.Accounts.ListAccounts:output_type -> litrpc.ListAccountsResponse - 2, // 12: litrpc.Accounts.AccountInfo:output_type -> litrpc.Account - 10, // 13: litrpc.Accounts.RemoveAccount:output_type -> litrpc.RemoveAccountResponse - 9, // [9:14] is the sub-list for method output_type - 4, // [4:9] is the sub-list for method input_type + 6, // 6: litrpc.Accounts.UpdateBalance:input_type -> litrpc.UpdateAccountBalanceRequest + 7, // 7: litrpc.Accounts.ListAccounts:input_type -> litrpc.ListAccountsRequest + 9, // 8: litrpc.Accounts.AccountInfo:input_type -> litrpc.AccountInfoRequest + 10, // 9: litrpc.Accounts.RemoveAccount:input_type -> litrpc.RemoveAccountRequest + 1, // 10: litrpc.Accounts.CreateAccount:output_type -> litrpc.CreateAccountResponse + 2, // 11: litrpc.Accounts.UpdateAccount:output_type -> litrpc.Account + 2, // 12: litrpc.Accounts.UpdateBalance:output_type -> litrpc.Account + 8, // 13: litrpc.Accounts.ListAccounts:output_type -> litrpc.ListAccountsResponse + 2, // 14: litrpc.Accounts.AccountInfo:output_type -> litrpc.Account + 11, // 15: litrpc.Accounts.RemoveAccount:output_type -> litrpc.RemoveAccountResponse + 10, // [10:16] is the sub-list for method output_type + 4, // [4:10] is the sub-list for method input_type 4, // [4:4] is the sub-list for extension type_name 4, // [4:4] is the sub-list for extension extendee 0, // [0:4] is the sub-list for field type_name @@ -921,7 +1040,7 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[6].Exporter = func(v any, i int) any { - switch v := v.(*ListAccountsRequest); i { + switch v := v.(*UpdateAccountBalanceRequest); i { case 0: return &v.state case 1: @@ -933,7 +1052,7 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[7].Exporter = func(v any, i int) any { - switch v := v.(*ListAccountsResponse); i { + switch v := v.(*ListAccountsRequest); i { case 0: return &v.state case 1: @@ -945,7 +1064,7 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[8].Exporter = func(v any, i int) any { - switch v := v.(*AccountInfoRequest); i { + switch v := v.(*ListAccountsResponse); i { case 0: return &v.state case 1: @@ -957,7 +1076,7 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[9].Exporter = func(v any, i int) any { - switch v := v.(*RemoveAccountRequest); i { + switch v := v.(*AccountInfoRequest); i { case 0: return &v.state case 1: @@ -969,6 +1088,18 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[10].Exporter = func(v any, i int) any { + switch v := v.(*RemoveAccountRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_lit_accounts_proto_msgTypes[11].Exporter = func(v any, i int) any { switch v := v.(*RemoveAccountResponse); i { case 0: return &v.state @@ -981,13 +1112,17 @@ func file_lit_accounts_proto_init() { } } } + file_lit_accounts_proto_msgTypes[6].OneofWrappers = []any{ + (*UpdateAccountBalanceRequest_Add)(nil), + (*UpdateAccountBalanceRequest_Deduct)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lit_accounts_proto_rawDesc, NumEnums: 0, - NumMessages: 11, + NumMessages: 12, NumExtensions: 0, NumServices: 1, }, diff --git a/litrpc/lit-accounts.proto b/litrpc/lit-accounts.proto index 209183a12..2ab204efa 100644 --- a/litrpc/lit-accounts.proto +++ b/litrpc/lit-accounts.proto @@ -25,6 +25,12 @@ service Accounts { */ rpc UpdateAccount (UpdateAccountRequest) returns (Account); + /* litcli: `accounts updatebalance` + UpdateBalance adds or deducts an amount from an existing account in the + account database. + */ + rpc UpdateBalance (UpdateAccountBalanceRequest) returns (Account); + /* litcli: `accounts list` ListAccounts returns all accounts that are currently stored in the account database. @@ -148,6 +154,28 @@ message UpdateAccountRequest { string label = 4; } +message UpdateAccountBalanceRequest { + // The ID of the account to update. Either the ID or the label must be set. + string id = 1; + + /* + The label of the account to update. If an account has no label, then the ID + must be used instead. + */ + string label = 2; + + /* + The balance update must either be an increase or a decrease of the balance + */ + oneof update { + // Increase the account's balance by the given amount. + int64 add = 3; + + // Deducts the given amount from the account balance. + int64 deduct = 4; + } +} + message ListAccountsRequest { } diff --git a/litrpc/lit-accounts_grpc.pb.go b/litrpc/lit-accounts_grpc.pb.go index 2a643f041..8ead53a00 100644 --- a/litrpc/lit-accounts_grpc.pb.go +++ b/litrpc/lit-accounts_grpc.pb.go @@ -34,6 +34,10 @@ type AccountsClient interface { // litcli: `accounts update` // UpdateAccount updates an existing account in the account database. UpdateAccount(ctx context.Context, in *UpdateAccountRequest, opts ...grpc.CallOption) (*Account, error) + // litcli: `accounts updatebalance` + // UpdateBalance adds or deducts an amount from an existing account in the + // account database. + UpdateBalance(ctx context.Context, in *UpdateAccountBalanceRequest, opts ...grpc.CallOption) (*Account, error) // litcli: `accounts list` // ListAccounts returns all accounts that are currently stored in the account // database. @@ -72,6 +76,15 @@ func (c *accountsClient) UpdateAccount(ctx context.Context, in *UpdateAccountReq return out, nil } +func (c *accountsClient) UpdateBalance(ctx context.Context, in *UpdateAccountBalanceRequest, opts ...grpc.CallOption) (*Account, error) { + out := new(Account) + err := c.cc.Invoke(ctx, "/litrpc.Accounts/UpdateBalance", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *accountsClient) ListAccounts(ctx context.Context, in *ListAccountsRequest, opts ...grpc.CallOption) (*ListAccountsResponse, error) { out := new(ListAccountsResponse) err := c.cc.Invoke(ctx, "/litrpc.Accounts/ListAccounts", in, out, opts...) @@ -119,6 +132,10 @@ type AccountsServer interface { // litcli: `accounts update` // UpdateAccount updates an existing account in the account database. UpdateAccount(context.Context, *UpdateAccountRequest) (*Account, error) + // litcli: `accounts updatebalance` + // UpdateBalance adds or deducts an amount from an existing account in the + // account database. + UpdateBalance(context.Context, *UpdateAccountBalanceRequest) (*Account, error) // litcli: `accounts list` // ListAccounts returns all accounts that are currently stored in the account // database. @@ -142,6 +159,9 @@ func (UnimplementedAccountsServer) CreateAccount(context.Context, *CreateAccount func (UnimplementedAccountsServer) UpdateAccount(context.Context, *UpdateAccountRequest) (*Account, error) { return nil, status.Errorf(codes.Unimplemented, "method UpdateAccount not implemented") } +func (UnimplementedAccountsServer) UpdateBalance(context.Context, *UpdateAccountBalanceRequest) (*Account, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateBalance not implemented") +} func (UnimplementedAccountsServer) ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListAccounts not implemented") } @@ -200,6 +220,24 @@ func _Accounts_UpdateAccount_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _Accounts_UpdateBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateAccountBalanceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountsServer).UpdateBalance(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Accounts/UpdateBalance", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountsServer).UpdateBalance(ctx, req.(*UpdateAccountBalanceRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Accounts_ListAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListAccountsRequest) if err := dec(in); err != nil { @@ -269,6 +307,10 @@ var Accounts_ServiceDesc = grpc.ServiceDesc{ MethodName: "UpdateAccount", Handler: _Accounts_UpdateAccount_Handler, }, + { + MethodName: "UpdateBalance", + Handler: _Accounts_UpdateBalance_Handler, + }, { MethodName: "ListAccounts", Handler: _Accounts_ListAccounts_Handler, diff --git a/perms/permissions.go b/perms/permissions.go index d05318613..67802ae0e 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -28,6 +28,10 @@ var ( Entity: "account", Action: "write", }}, + "/litrpc.Accounts/UpdateBalance": {{ + Entity: "account", + Action: "write", + }}, "/litrpc.Accounts/ListAccounts": {{ Entity: "account", Action: "read", diff --git a/proto/lit-accounts.proto b/proto/lit-accounts.proto index bc016819d..a65e89fc9 100644 --- a/proto/lit-accounts.proto +++ b/proto/lit-accounts.proto @@ -25,6 +25,12 @@ service Accounts { */ rpc UpdateAccount (UpdateAccountRequest) returns (Account); + /* litcli: `accounts updatebalance` + UpdateBalance adds or deducts an amount from an existing account in the + account database. + */ + rpc UpdateBalance (UpdateAccountBalanceRequest) returns (Account); + /* litcli: `accounts list` ListAccounts returns all accounts that are currently stored in the account database. @@ -148,6 +154,28 @@ message UpdateAccountRequest { string label = 4; } +message UpdateAccountBalanceRequest { + // The ID of the account to update. Either the ID or the label must be set. + string id = 1; + + /* + The label of the account to update. If an account has no label, then the ID + must be used instead. + */ + string label = 2; + + /* + The balance update must either be an increase or a decrease of the balance + */ + oneof update { + // Increase the account's balance by the given amount. + int64 add = 3 [jstype = JS_STRING]; + + // Deducts the given amount from the account balance. + int64 deduct = 4 [jstype = JS_STRING]; + } +} + message ListAccountsRequest { } From 3cd156943cb02ad63c4c94753784a8e322407b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 18:05:24 +0100 Subject: [PATCH 05/10] litcli: add `updatebalance` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements the `updatebalance` command in `litcli`. The command is implemented with two different subcommands: `add`, which increases an existing off-chain account’s balance, and `deduct`, which decreases it accordingly. --- cmd/litcli/accounts.go | 127 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/cmd/litcli/accounts.go b/cmd/litcli/accounts.go index 3e5468af3..9087aadf0 100644 --- a/cmd/litcli/accounts.go +++ b/cmd/litcli/accounts.go @@ -2,6 +2,7 @@ package main import ( "encoding/hex" + "errors" "fmt" "os" "strconv" @@ -26,6 +27,7 @@ var accountsCommands = []cli.Command{ Subcommands: []cli.Command{ createAccountCommand, updateAccountCommand, + updateBalanceCommands, listAccountsCommand, accountInfoCommand, removeAccountCommand, @@ -232,6 +234,131 @@ func updateAccount(cli *cli.Context) error { return nil } +var updateBalanceCommands = cli.Command{ + Name: "updatebalance", + ShortName: "b", + Usage: "Update the balance of an existing off-chain account.", + Subcommands: []cli.Command{ + addBalanceCommand, + deductBalanceCommand, + }, + Description: "Updates the balance of an existing off-chain account.", +} + +var addBalanceCommand = cli.Command{ + Name: "add", + ShortName: "a", + Usage: "Adds the given amount to an account's balance.", + ArgsUsage: "[id | label] amount", + Description: "Adds the given amount to an existing off-chain " + + "account's balance.", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: idName, + Usage: "The ID of the account to add balance to.", + }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) The unique label of the account.", + }, + cli.Uint64Flag{ + Name: "amount", + Usage: "The amount to add to the account.", + }, + }, + Action: addBalance, +} + +func addBalance(cli *cli.Context) error { + return updateBalance(cli, true) +} + +var deductBalanceCommand = cli.Command{ + Name: "deduct", + ShortName: "d", + Usage: "Deducts the given amount from an account's balance.", + ArgsUsage: "[id | label] amount", + Description: "Deducts the given amount from an existing off-chain " + + "account's balance.", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: idName, + Usage: "The ID of the account to deduct balance from.", + }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) The unique label of the account.", + }, + cli.Uint64Flag{ + Name: "amount", + Usage: "The amount to deduct from the account.", + }, + }, + Action: deductBalance, +} + +func deductBalance(cli *cli.Context) error { + return updateBalance(cli, false) +} + +func updateBalance(cli *cli.Context, add bool) error { + ctx := getContext() + clientConn, cleanup, err := connectClient(cli, false) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewAccountsClient(clientConn) + + id, label, args, err := parseIDOrLabel(cli) + if err != nil { + return err + } + + if (!cli.IsSet("amount") && len(args) != 1) || + (cli.IsSet("amount") && len(args) != 0) { + + return errors.New("invalid number of arguments") + } + + var amount int64 + switch { + case cli.IsSet("amount"): + amount = cli.Int64("amount") + case args.Present(): + amount, err = strconv.ParseInt(args.First(), 10, 64) + if err != nil { + return fmt.Errorf("unable to decode balance %v", err) + } + args = args.Tail() + default: + return errors.New("must set a value for amount") + } + + req := &litrpc.UpdateAccountBalanceRequest{ + Id: id, + Label: label, + } + + if add { + req.Update = &litrpc.UpdateAccountBalanceRequest_Add{ + Add: amount, + } + } else { + req.Update = &litrpc.UpdateAccountBalanceRequest_Deduct{ + Deduct: amount, + } + } + + resp, err := client.UpdateBalance(ctx, req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + var listAccountsCommand = cli.Command{ Name: "list", ShortName: "l", From 078ca5fd031836c6b707a02c036053d6f3c38906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 18:14:07 +0100 Subject: [PATCH 06/10] accounts: implement kvdb `AdjustAccountBalance` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To support updating an existing off-chain account’s balance by a specified amount in the database, we'll implement a corresponding functions for the different stores. This commit introduces the `AdjustAccountBalance` function in the kvdb store. --- accounts/store_kvdb.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/accounts/store_kvdb.go b/accounts/store_kvdb.go index 47a7c7ac4..cddcde183 100644 --- a/accounts/store_kvdb.go +++ b/accounts/store_kvdb.go @@ -307,6 +307,43 @@ 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. +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. From a01233b38a3d795fdf132cc6970aa49dba917d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 18:16:06 +0100 Subject: [PATCH 07/10] accounts: implement sqlc `AdjustAccountBalance` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces the `AdjustAccountBalance` function in the sqlc store, to support updating an existing off-chain account’s balance by a specified amount for sql backends. --- accounts/interface.go | 6 ++++ accounts/store_kvdb.go | 2 ++ accounts/store_sql.go | 59 ++++++++++++++++++++++++++++++++++++ db/sqlc/accounts.sql.go | 23 ++++++++++++++ db/sqlc/querier.go | 1 + db/sqlc/queries/accounts.sql | 9 ++++++ 6 files changed, 100 insertions(+) diff --git a/accounts/interface.go b/accounts/interface.go index 5677a08fd..efe11c0fb 100644 --- a/accounts/interface.go +++ b/accounts/interface.go @@ -268,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. diff --git a/accounts/store_kvdb.go b/accounts/store_kvdb.go index cddcde183..2aa475773 100644 --- a/accounts/store_kvdb.go +++ b/accounts/store_kvdb.go @@ -310,6 +310,8 @@ func (s *BoltStore) UpsertAccountPayment(_ context.Context, id AccountID, // 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 { diff --git a/accounts/store_sql.go b/accounts/store_sql.go index 2498d2aa1..d0711f56d 100644 --- a/accounts/store_sql.go +++ b/accounts/store_sql.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "time" "github.com/lightninglabs/lightning-terminal/db" @@ -34,6 +35,7 @@ const ( //nolint:lll type SQLQueries interface { AddAccountInvoice(ctx context.Context, arg sqlc.AddAccountInvoiceParams) error + AdjustAccountBalance(ctx context.Context, arg sqlc.AdjustAccountBalanceParams) (int64, error) DeleteAccount(ctx context.Context, id int64) error DeleteAccountPayment(ctx context.Context, arg sqlc.DeleteAccountPaymentParams) error GetAccount(ctx context.Context, id int64) (sqlc.Account, error) @@ -598,6 +600,63 @@ func (s *SQLStore) UpsertAccountPayment(ctx context.Context, alias AccountID, }) } +// 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 *SQLStore) AdjustAccountBalance(ctx context.Context, + alias AccountID, amount lnwire.MilliSatoshi, isAddition bool) error { + + if amount > math.MaxInt64 { + return fmt.Errorf("amount %v exceeds the maximum of %v", + amount/1000, int64(math.MaxInt64)/1000) + } + + var writeTxOpts db.QueriesTxOptions + return s.db.ExecTx(ctx, &writeTxOpts, func(db SQLQueries) error { + id, err := getAccountIDByAlias(ctx, db, alias) + if err != nil { + return err + } + + if !isAddition { + acct, err := db.GetAccount(ctx, id) + if err != nil { + return err + } + + if acct.CurrentBalanceMsat-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), + acct.CurrentBalanceMsat/1000) + } + } + + isAdditionInt := int64(0) + if isAddition { + isAdditionInt = 1 + } + + _, err = db.AdjustAccountBalance( + ctx, sqlc.AdjustAccountBalanceParams{ + IsAddition: isAdditionInt, + Amount: int64(amount), + ID: id, + }, + ) + if errors.Is(err, sql.ErrNoRows) { + return ErrAccNotFound + } else if err != nil { + return err + } + + return s.markAccountUpdated(ctx, db, id) + }) +} + // DeleteAccountPayment removes a payment entry from the account with the given // ID. It will return an error if the payment is not associated with the // account. diff --git a/db/sqlc/accounts.sql.go b/db/sqlc/accounts.sql.go index 4deefdb88..dfb5f5bb4 100644 --- a/db/sqlc/accounts.sql.go +++ b/db/sqlc/accounts.sql.go @@ -26,6 +26,29 @@ func (q *Queries) AddAccountInvoice(ctx context.Context, arg AddAccountInvoicePa return err } +const adjustAccountBalance = `-- name: AdjustAccountBalance :one +UPDATE accounts +SET current_balance_msat = current_balance_msat + CASE + WHEN $1 THEN $2 -- If IsAddition is true, add the amount + ELSE -$2 -- If IsAddition is false, subtract the amount +END +WHERE id = $3 +RETURNING id +` + +type AdjustAccountBalanceParams struct { + IsAddition int64 + Amount int64 + ID int64 +} + +func (q *Queries) AdjustAccountBalance(ctx context.Context, arg AdjustAccountBalanceParams) (int64, error) { + row := q.db.QueryRowContext(ctx, adjustAccountBalance, arg.IsAddition, arg.Amount, arg.ID) + var id int64 + err := row.Scan(&id) + return id, err +} + const deleteAccount = `-- name: DeleteAccount :exec DELETE FROM accounts WHERE id = $1 diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index b0265c596..cb41aeb37 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -11,6 +11,7 @@ import ( type Querier interface { AddAccountInvoice(ctx context.Context, arg AddAccountInvoiceParams) error + AdjustAccountBalance(ctx context.Context, arg AdjustAccountBalanceParams) (int64, error) DeleteAccount(ctx context.Context, id int64) error DeleteAccountPayment(ctx context.Context, arg DeleteAccountPaymentParams) error GetAccount(ctx context.Context, id int64) (Account, error) diff --git a/db/sqlc/queries/accounts.sql b/db/sqlc/queries/accounts.sql index 637a49727..e4ceb428a 100644 --- a/db/sqlc/queries/accounts.sql +++ b/db/sqlc/queries/accounts.sql @@ -9,6 +9,15 @@ SET current_balance_msat = $1 WHERE id = $2 RETURNING id; +-- name: AdjustAccountBalance :one +UPDATE accounts +SET current_balance_msat = current_balance_msat + CASE + WHEN sqlc.arg(is_addition) THEN sqlc.arg(amount) -- If IsAddition is true, add the amount + ELSE -sqlc.arg(amount) -- If IsAddition is false, subtract the amount +END +WHERE id = sqlc.arg(id) +RETURNING id; + -- name: UpdateAccountExpiry :one UPDATE accounts SET expiration = $1 From debfa05fc5c7c8ef56be3c2ba4a4271489823b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 18:16:53 +0100 Subject: [PATCH 08/10] accounts: add `AdjustAccountBalance` store tests --- accounts/store_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/accounts/store_test.go b/accounts/store_test.go index 92d4fe5ba..fba02d391 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,18 @@ func TestAccountStore(t *testing.T) { ) require.NoError(t, err) + // Adjust the account balance by first adding 10000, and then deducting + // 5000. + err = store.AdjustAccountBalance( + ctx, acct1.ID, lnwire.MilliSatoshi(10000), true, + ) + require.NoError(t, err) + + err = store.AdjustAccountBalance( + ctx, acct1.ID, lnwire.MilliSatoshi(5000), false, + ) + 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 +98,32 @@ 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.AdjustAccountBalance( + ctx, acct1.ID, lnwire.MilliSatoshi(acct1.CurrentBalance), false, + ) + 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.AdjustAccountBalance( + ctx, acct1.ID, lnwire.MilliSatoshi(1), false, + ) + 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 From 54005b0147f7fe519ef6ca28a5e5d82b87e41784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Wed, 5 Feb 2025 18:18:00 +0100 Subject: [PATCH 09/10] accounts: implement `UpdateBalance` endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With all underlying functionality in place to support updating an existing off-chain account’s balance by a specified amount, this commit implements the `UpdateBalance` endpoint to utilize that functionality. --- accounts/rpcserver.go | 41 ++++++++++++++++++++++++++++++++++++++++- accounts/service.go | 26 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/accounts/rpcserver.go b/accounts/rpcserver.go index 48f3a229e..a66153070 100644 --- a/accounts/rpcserver.go +++ b/accounts/rpcserver.go @@ -137,7 +137,46 @@ func (s *RPCServer) UpdateAccount(ctx context.Context, func (s *RPCServer) UpdateBalance(ctx context.Context, req *litrpc.UpdateAccountBalanceRequest) (*litrpc.Account, error) { - return nil, fmt.Errorf("not implemented") + var ( + isAddition bool + amount lnwire.MilliSatoshi + ) + + switch reqType := req.GetUpdate().(type) { + case *litrpc.UpdateAccountBalanceRequest_Add: + log.Infof("[addbalance] id=%s, label=%v, amount=%d", + 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)) + } + + 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 diff --git a/accounts/service.go b/accounts/service.go index f84f12500..086aaefaf 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -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, From aed7ef30152188f2f96e9042c578b98b7ca0bb40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20Tigerstr=C3=B6m?= Date: Thu, 6 Feb 2025 01:40:45 +0100 Subject: [PATCH 10/10] docs: update release notes --- docs/release-notes/release-notes-0.14.1.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/release-notes/release-notes-0.14.1.md b/docs/release-notes/release-notes-0.14.1.md index 93317f800..054e590a5 100644 --- a/docs/release-notes/release-notes-0.14.1.md +++ b/docs/release-notes/release-notes-0.14.1.md @@ -18,6 +18,11 @@ ### Functional Changes/Additions +* [Add account updatebalance + commands](https://github.com/lightninglabs/lightning-terminal/pull/962) that + allow increasing or decreasing the balance of an off-chain account by a + specified amount. + ### Technical and Architectural Updates * [Add some Makefile @@ -52,4 +57,5 @@ * jiangmencity * Oliver Gugger * Tristav +* Viktor * zhoufanjin \ No newline at end of file