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/accounts/interface.go b/accounts/interface.go index 11d3efe93..efe11c0fb 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[:]) @@ -241,6 +268,12 @@ type Store interface { status lnrpc.Payment_PaymentStatus, options ...UpsertPaymentOption) (bool, error) + // AdjustAccountBalance modifies the given account balance by adding or + // deducting the specified amount, depending on whether isAddition is + // true or false. + AdjustAccountBalance(ctx context.Context, alias AccountID, + amount lnwire.MilliSatoshi, isAddition bool) error + // DeleteAccountPayment removes a payment entry from the account with // the given ID. It will return the ErrPaymentNotAssociated error if the // payment is not associated with the account. diff --git a/accounts/rpcserver.go b/accounts/rpcserver.go index 79e3e674b..a66153070 100644 --- a/accounts/rpcserver.go +++ b/accounts/rpcserver.go @@ -132,6 +132,53 @@ func (s *RPCServer) UpdateAccount(ctx context.Context, return marshalAccount(account), nil } +// UpdateBalance adds or deducts an amount from an existing account in the +// account database. +func (s *RPCServer) UpdateBalance(ctx context.Context, + req *litrpc.UpdateAccountBalanceRequest) (*litrpc.Account, error) { + + var ( + isAddition bool + amount lnwire.MilliSatoshi + ) + + switch reqType := req.GetUpdate().(type) { + case *litrpc.UpdateAccountBalanceRequest_Add: + log.Infof("[addbalance] id=%s, label=%v, amount=%d", + 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 // database. func (s *RPCServer) ListAccounts(ctx context.Context, 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, diff --git a/accounts/store_kvdb.go b/accounts/store_kvdb.go index 47a7c7ac4..2aa475773 100644 --- a/accounts/store_kvdb.go +++ b/accounts/store_kvdb.go @@ -307,6 +307,45 @@ func (s *BoltStore) UpsertAccountPayment(_ context.Context, id AccountID, return known, s.updateAccount(id, update) } +// AdjustAccountBalance modifies the given account balance by adding or +// deducting the specified amount, depending on whether isAddition is true or +// false. +// +// NOTE: This is part of the Store interface. +func (s *BoltStore) AdjustAccountBalance(_ context.Context, + id AccountID, amount lnwire.MilliSatoshi, isAddition bool) error { + + update := func(account *OffChainBalanceAccount) error { + if amount > math.MaxInt64 { + return fmt.Errorf("amount %v exceeds the maximum of %v", + int64(amount/1000), int64(math.MaxInt64)/1000) + } + + if amount <= 0 { + return fmt.Errorf("amount %v must be greater that 0", + amount) + } + + if isAddition { + account.CurrentBalance += int64(amount) + } else { + if account.CurrentBalance-int64(amount) < 0 { + return fmt.Errorf("cannot deduct %v from the "+ + "current balance %v, as the resulting "+ + "balance would be below 0", + int64(amount/1000), + account.CurrentBalance/1000) + } + + account.CurrentBalance -= int64(amount) + } + + return nil + } + + return s.updateAccount(id, update) +} + // DeleteAccountPayment removes a payment entry from the account with the given // ID. It will return the ErrPaymentNotAssociated error if the payment is not // associated with the account. diff --git a/accounts/store_sql.go b/accounts/store_sql.go new file mode 100644 index 000000000..d0711f56d --- /dev/null +++ b/accounts/store_sql.go @@ -0,0 +1,763 @@ +package accounts + +import ( + "context" + "crypto/rand" + "database/sql" + "encoding/hex" + "errors" + "fmt" + "math" + "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 + 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) + 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) + }) +} + +// 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. +// +// 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..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 @@ -130,8 +164,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 +202,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, + ) +} 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/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", 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 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 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/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= 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/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} 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 { }