diff --git a/accounts/checkers.go b/accounts/checkers.go index b7694a326..8fdb8c9b3 100644 --- a/accounts/checkers.go +++ b/accounts/checkers.go @@ -525,7 +525,7 @@ func checkSend(ctx context.Context, chainParams *chaincfg.Params, if len(invoice) > 0 { payReq, err := zpay32.Decode(invoice, chainParams) if err != nil { - return fmt.Errorf("error decoding pay req: %v", err) + return fmt.Errorf("error decoding pay req: %w", err) } if payReq.MilliSat != nil && *payReq.MilliSat > sendAmt { @@ -546,7 +546,7 @@ func checkSend(ctx context.Context, chainParams *chaincfg.Params, err = service.CheckBalance(acct.ID, sendAmt) if err != nil { - return fmt.Errorf("error validating account balance: %v", err) + return fmt.Errorf("error validating account balance: %w", err) } return nil @@ -609,7 +609,7 @@ func checkSendToRoute(ctx context.Context, service Service, err = service.CheckBalance(acct.ID, sendAmt) if err != nil { - return fmt.Errorf("error validating account balance: %v", err) + return fmt.Errorf("error validating account balance: %w", err) } return nil diff --git a/accounts/checkers_test.go b/accounts/checkers_test.go index dfac8d0ac..481609c91 100644 --- a/accounts/checkers_test.go +++ b/accounts/checkers_test.go @@ -41,14 +41,14 @@ type mockService struct { acctBalanceMsat lnwire.MilliSatoshi trackedInvoices map[lntypes.Hash]AccountID - trackedPayments map[lntypes.Hash]*PaymentEntry + trackedPayments AccountPayments } func newMockService() *mockService { return &mockService{ acctBalanceMsat: 0, trackedInvoices: make(map[lntypes.Hash]AccountID), - trackedPayments: make(map[lntypes.Hash]*PaymentEntry), + trackedPayments: make(AccountPayments), } } @@ -68,7 +68,7 @@ func (m *mockService) AssociateInvoice(id AccountID, hash lntypes.Hash) error { return nil } -func (m *mockService) TrackPayment(id AccountID, hash lntypes.Hash, +func (m *mockService) TrackPayment(_ AccountID, hash lntypes.Hash, amt lnwire.MilliSatoshi) error { m.trackedPayments[hash] = &PaymentEntry{ @@ -403,8 +403,8 @@ func TestAccountCheckers(t *testing.T) { acct := &OffChainBalanceAccount{ ID: testID, Type: TypeInitialBalance, - Invoices: make(map[lntypes.Hash]struct{}), - Payments: make(map[lntypes.Hash]*PaymentEntry), + Invoices: make(AccountInvoices), + Payments: make(AccountPayments), } ctx := AddToContext( context.Background(), KeyAccount, acct, diff --git a/accounts/interceptor.go b/accounts/interceptor.go index 7676c2d6c..d6dff8dfe 100644 --- a/accounts/interceptor.go +++ b/accounts/interceptor.go @@ -153,7 +153,7 @@ func parseRPCMessage(msg *lnrpc.RPCMessage) (proto.Message, error) { // No, it's a normal message. parsedMsg, err := mid.ParseProtobuf(msg.TypeName, msg.Serialized) if err != nil { - return nil, fmt.Errorf("error parsing proto of type %v: %v", + return nil, fmt.Errorf("error parsing proto of type %v: %w", msg.TypeName, err) } diff --git a/accounts/interface.go b/accounts/interface.go index 288d8e041..879d9a51a 100644 --- a/accounts/interface.go +++ b/accounts/interface.go @@ -44,7 +44,7 @@ func ParseAccountID(idStr string) (*AccountID, error) { idBytes, err := hex.DecodeString(idStr) if err != nil { - return nil, fmt.Errorf("error decoding account ID: %v", err) + return nil, fmt.Errorf("error decoding account ID: %w", err) } var id AccountID @@ -67,6 +67,12 @@ type PaymentEntry struct { FullAmount lnwire.MilliSatoshi } +// AccountInvoices is the set of invoices that are associated with an account. +type AccountInvoices map[lntypes.Hash]struct{} + +// AccountPayments is the set of payments that are associated with an account. +type AccountPayments map[lntypes.Hash]*PaymentEntry + // OffChainBalanceAccount holds all information that is needed to keep track of // a user's off-chain account balance. This balance can only be spent by paying // invoices. @@ -99,11 +105,15 @@ type OffChainBalanceAccount struct { // Invoices is a list of all invoices that are associated with the // account. - Invoices map[lntypes.Hash]struct{} + Invoices AccountInvoices // Payments is a list of all payments that are associated with the // account and the last status we were aware of. - Payments map[lntypes.Hash]*PaymentEntry + Payments AccountPayments + + // Label is an optional label that can be set for the account. If it is + // not empty then it must be unique. + Label string } // HasExpired returns true if the account has an expiration date set and that @@ -180,8 +190,8 @@ var ( type Store interface { // NewAccount creates a new OffChainBalanceAccount with the given // balance and a randomly chosen ID. - NewAccount(balance lnwire.MilliSatoshi, - expirationDate time.Time) (*OffChainBalanceAccount, error) + NewAccount(balance lnwire.MilliSatoshi, expirationDate time.Time, + label string) (*OffChainBalanceAccount, error) // UpdateAccount writes an account to the database, overwriting the // existing one if it exists. diff --git a/accounts/rpcserver.go b/accounts/rpcserver.go index 0dafdb9a8..22135f634 100644 --- a/accounts/rpcserver.go +++ b/accounts/rpcserver.go @@ -50,8 +50,8 @@ func (s *RPCServer) CreateAccount(ctx context.Context, req *litrpc.CreateAccountRequest) (*litrpc.CreateAccountResponse, error) { - log.Infof("[createaccount] balance=%d, expiration=%d", - req.AccountBalance, req.ExpirationDate) + log.Infof("[createaccount] label=%v, balance=%d, expiration=%d", + req.Label, req.AccountBalance, req.ExpirationDate) var ( balanceMsat lnwire.MilliSatoshi @@ -70,9 +70,11 @@ func (s *RPCServer) CreateAccount(ctx context.Context, balanceMsat = lnwire.NewMSatFromSatoshis(balance) // Create the actual account in the macaroon account store. - account, err := s.service.NewAccount(balanceMsat, expirationDate) + account, err := s.service.NewAccount( + balanceMsat, expirationDate, req.Label, + ) if err != nil { - return nil, fmt.Errorf("unable to create account: %v", err) + return nil, fmt.Errorf("unable to create account: %w", err) } var rootKeyIdSuffix [4]byte @@ -91,12 +93,12 @@ func (s *RPCServer) CreateAccount(ctx context.Context, }}, }) if err != nil { - return nil, fmt.Errorf("error baking account macaroon: %v", err) + return nil, fmt.Errorf("error baking account macaroon: %w", err) } macBytes, err := hex.DecodeString(macHex) if err != nil { - return nil, fmt.Errorf("error decoding account macaroon: %v", + return nil, fmt.Errorf("error decoding account macaroon: %w", err) } @@ -110,16 +112,13 @@ func (s *RPCServer) CreateAccount(ctx context.Context, func (s *RPCServer) UpdateAccount(_ context.Context, req *litrpc.UpdateAccountRequest) (*litrpc.Account, error) { - log.Infof("[updateaccount] id=%s, balance=%d, expiration=%d", req.Id, - req.AccountBalance, req.ExpirationDate) + log.Infof("[updateaccount] id=%s, label=%v, balance=%d, expiration=%d", + req.Id, req.Label, req.AccountBalance, req.ExpirationDate) - // Account ID is always a hex string, convert it to our account ID type. - var accountID AccountID - decoded, err := hex.DecodeString(req.Id) + accountID, err := s.findAccount(req.Id, req.Label) if err != nil { - return nil, fmt.Errorf("error decoding account ID: %v", err) + return nil, err } - copy(accountID[:], decoded) // Ask the service to update the account. account, err := s.service.UpdateAccount( @@ -142,7 +141,7 @@ func (s *RPCServer) ListAccounts(context.Context, // Retrieve all accounts from the macaroon account store. accts, err := s.service.Accounts() if err != nil { - return nil, fmt.Errorf("unable to list accounts: %v", err) + return nil, fmt.Errorf("unable to list accounts: %w", err) } // Map the response into the proper response type and return it. @@ -158,30 +157,89 @@ func (s *RPCServer) ListAccounts(context.Context, }, nil } +// AccountInfo returns the account with the given ID or label. +func (s *RPCServer) AccountInfo(_ context.Context, + req *litrpc.AccountInfoRequest) (*litrpc.Account, error) { + + log.Infof("[accountinfo] id=%v, label=%v", req.Id, req.Label) + + accountID, err := s.findAccount(req.Id, req.Label) + if err != nil { + return nil, err + } + + dbAccount, err := s.service.Account(accountID) + if err != nil { + return nil, fmt.Errorf("error retrieving account: %w", err) + } + + return marshalAccount(dbAccount), nil +} + // RemoveAccount removes the given account from the account database. func (s *RPCServer) RemoveAccount(_ context.Context, req *litrpc.RemoveAccountRequest) (*litrpc.RemoveAccountResponse, error) { - log.Infof("[removeaccount] id=%v", req.Id) + log.Infof("[removeaccount] id=%v, label=%v", req.Id, req.Label) - // Account ID is always a hex string, convert it to our account ID type. - var accountID AccountID - decoded, err := hex.DecodeString(req.Id) + accountID, err := s.findAccount(req.Id, req.Label) if err != nil { - return nil, fmt.Errorf("error decoding account ID: %v", err) + return nil, err } - copy(accountID[:], decoded) // Now remove the account. err = s.service.RemoveAccount(accountID) if err != nil { - return nil, fmt.Errorf("error removing account: %v", err) + return nil, fmt.Errorf("error removing account: %w", err) } return &litrpc.RemoveAccountResponse{}, nil } +// findAccount finds an account by its ID or label. +func (s *RPCServer) findAccount(id string, label string) (AccountID, error) { + switch { + case id != "" && label != "": + return AccountID{}, fmt.Errorf("either account ID or label " + + "must be specified, not both") + + case id != "": + // Account ID is always a hex string, convert it to our account + // ID type. + var accountID AccountID + decoded, err := hex.DecodeString(id) + if err != nil { + return AccountID{}, fmt.Errorf("error decoding "+ + "account ID: %w", err) + } + copy(accountID[:], decoded) + + return accountID, nil + + case label != "": + // We need to find the account by its label. + accounts, err := s.service.Accounts() + if err != nil { + return AccountID{}, fmt.Errorf("unable to list "+ + "accounts: %w", err) + } + + for _, acct := range accounts { + if acct.Label == label { + return acct.ID, nil + } + } + + return AccountID{}, fmt.Errorf("unable to find account "+ + "with label '%s'", label) + + default: + return AccountID{}, fmt.Errorf("either account ID or label " + + "must be specified") + } +} + // marshalAccount converts an account into its RPC counterpart. func marshalAccount(acct *OffChainBalanceAccount) *litrpc.Account { rpcAccount := &litrpc.Account{ @@ -196,6 +254,7 @@ func marshalAccount(acct *OffChainBalanceAccount) *litrpc.Account { Payments: make( []*litrpc.AccountPayment, 0, len(acct.Payments), ), + Label: acct.Label, } for hash := range acct.Invoices { diff --git a/accounts/service.go b/accounts/service.go index 3e20acabd..3f6f77812 100644 --- a/accounts/service.go +++ b/accounts/service.go @@ -96,7 +96,7 @@ func (s *InterceptorService) Start(lightningClient lndclient.LightningClient, // also track payments that aren't in a final state yet. existingAccounts, err := s.store.Accounts() if err != nil { - return fmt.Errorf("error querying existing accounts: %v", err) + return fmt.Errorf("error querying existing accounts: %w", err) } for _, acct := range existingAccounts { acct := acct @@ -109,15 +109,13 @@ func (s *InterceptorService) Start(lightningClient lndclient.LightningClient, // state of being in-flight. for hash, entry := range acct.Payments { entry := entry - if entry.Status == lnrpc.Payment_IN_FLIGHT || - entry.Status == lnrpc.Payment_UNKNOWN { - + if !successState(entry.Status) { err := s.TrackPayment( acct.ID, hash, entry.FullAmount, ) if err != nil { return fmt.Errorf("error tracking "+ - "payment: %v", err) + "payment: %w", err) } } } @@ -146,7 +144,7 @@ func (s *InterceptorService) Start(lightningClient lndclient.LightningClient, s.currentSettleIndex = 0 default: - return fmt.Errorf("error determining last invoice indexes: %v", + return fmt.Errorf("error determining last invoice indexes: %w", err) } @@ -157,7 +155,7 @@ func (s *InterceptorService) Start(lightningClient lndclient.LightningClient, }, ) if err != nil { - return fmt.Errorf("error subscribing invoices: %v", err) + return fmt.Errorf("error subscribing invoices: %w", err) } s.wg.Add(1) @@ -209,15 +207,26 @@ func (s *InterceptorService) Start(lightningClient lndclient.LightningClient, return nil } +// Stop shuts down the account service. +func (s *InterceptorService) Stop() error { + s.contextCancel() + close(s.quit) + + s.wg.Wait() + + return s.store.Close() +} + // NewAccount creates a new OffChainBalanceAccount with the given balance and a // randomly chosen ID. func (s *InterceptorService) NewAccount(balance lnwire.MilliSatoshi, - expirationDate time.Time) (*OffChainBalanceAccount, error) { + expirationDate time.Time, label string) (*OffChainBalanceAccount, + error) { s.Lock() defer s.Unlock() - return s.store.NewAccount(balance, expirationDate) + return s.store.NewAccount(balance, expirationDate, label) } // UpdateAccount writes an account to the database, overwriting the existing one @@ -230,7 +239,7 @@ func (s *InterceptorService) UpdateAccount(accountID AccountID, accountBalance, account, err := s.store.Account(accountID) if err != nil { - return nil, fmt.Errorf("error fetching account: %v", err) + return nil, fmt.Errorf("error fetching account: %w", err) } // If the expiration date was set, parse it as a unix time stamp. A @@ -254,7 +263,7 @@ func (s *InterceptorService) UpdateAccount(accountID AccountID, accountBalance, // Create the actual account in the macaroon account store. err = s.store.UpdateAccount(account) if err != nil { - return nil, fmt.Errorf("unable to update account: %v", err) + return nil, fmt.Errorf("unable to update account: %w", err) } return account, nil @@ -394,7 +403,7 @@ func (s *InterceptorService) invoiceUpdate(invoice *lndclient.Invoice) error { account, err := s.store.Account(acctID) if err != nil { - return fmt.Errorf("error fetching account: %v", err) + return fmt.Errorf("error fetching account: %w", err) } // If we get here, the current account has the invoice associated with @@ -402,7 +411,7 @@ func (s *InterceptorService) invoiceUpdate(invoice *lndclient.Invoice) error { // in the DB. account.CurrentBalance += int64(invoice.AmountPaid) if err := s.store.UpdateAccount(account); err != nil { - return fmt.Errorf("error updating account: %v", err) + return fmt.Errorf("error updating account: %w", err) } // We've now fully processed the invoice and don't need to keep it @@ -430,15 +439,13 @@ func (s *InterceptorService) TrackPayment(id AccountID, hash lntypes.Hash, // is a reference in the account with the given state. account, err := s.store.Account(id) if err != nil { - return fmt.Errorf("error fetching account: %v", err) + return fmt.Errorf("error fetching account: %w", err) } // If the account already stored a terminal state, we also don't need to // track the payment again. entry, ok := account.Payments[hash] - if ok && (entry.Status == lnrpc.Payment_SUCCEEDED || - entry.Status == lnrpc.Payment_FAILED) { - + if ok && successState(entry.Status) { return nil } @@ -449,7 +456,7 @@ func (s *InterceptorService) TrackPayment(id AccountID, hash lntypes.Hash, FullAmount: fullAmt, } if err := s.store.UpdateAccount(account); err != nil { - return fmt.Errorf("error updating account: %v", err) + return fmt.Errorf("error updating account: %w", err) } // And start the long-running TrackPayment RPC. @@ -565,7 +572,7 @@ func (s *InterceptorService) paymentUpdate(hash lntypes.Hash, FullAmount: fullAmount, } if err := s.store.UpdateAccount(account); err != nil { - return terminalState, fmt.Errorf("error updating account: %v", + return terminalState, fmt.Errorf("error updating account: %w", err) } @@ -617,12 +624,7 @@ func (s *InterceptorService) removePayment(hash lntypes.Hash, return s.store.UpdateAccount(account) } -// Stop shuts down the account service. -func (s *InterceptorService) Stop() error { - s.contextCancel() - close(s.quit) - - s.wg.Wait() - - return s.store.Close() +// successState returns true if a payment was completed successfully. +func successState(status lnrpc.Payment_PaymentStatus) bool { + return status == lnrpc.Payment_SUCCEEDED } diff --git a/accounts/service_test.go b/accounts/service_test.go index 7a259ce91..3b0b604ae 100644 --- a/accounts/service_test.go +++ b/accounts/service_test.go @@ -186,10 +186,10 @@ func TestAccountService(t *testing.T) { ID: testID, Type: TypeInitialBalance, CurrentBalance: 1234, - Invoices: map[lntypes.Hash]struct{}{ + Invoices: AccountInvoices{ testHash: {}, }, - Payments: make(map[lntypes.Hash]*PaymentEntry), + Payments: make(AccountPayments), } err := s.store.UpdateAccount(acct) @@ -206,12 +206,14 @@ func TestAccountService(t *testing.T) { }, { name: "startup do not track completed payments", setup: func(t *testing.T, lnd *mockLnd, s *InterceptorService) { - acct, err := s.store.NewAccount(1234, testExpiration) + acct, err := s.store.NewAccount( + 1234, testExpiration, "", + ) require.NoError(t, err) acct.Invoices[testHash] = struct{}{} acct.Payments[testHash] = &PaymentEntry{ - Status: lnrpc.Payment_FAILED, + Status: lnrpc.Payment_SUCCEEDED, FullAmount: 1234, } @@ -233,10 +235,10 @@ func TestAccountService(t *testing.T) { ID: testID, Type: TypeInitialBalance, CurrentBalance: 1234, - Invoices: map[lntypes.Hash]struct{}{ + Invoices: AccountInvoices{ testHash: {}, }, - Payments: map[lntypes.Hash]*PaymentEntry{ + Payments: AccountPayments{ testHash: { Status: lnrpc.Payment_IN_FLIGHT, FullAmount: 1234, @@ -360,10 +362,10 @@ func TestAccountService(t *testing.T) { ID: testID, Type: TypeInitialBalance, CurrentBalance: 1234, - Invoices: map[lntypes.Hash]struct{}{ + Invoices: AccountInvoices{ testHash: {}, }, - Payments: make(map[lntypes.Hash]*PaymentEntry), + Payments: make(AccountPayments), } err := s.store.UpdateAccount(acct) @@ -398,10 +400,10 @@ func TestAccountService(t *testing.T) { ID: testID, Type: TypeInitialBalance, CurrentBalance: 5000, - Invoices: map[lntypes.Hash]struct{}{ + Invoices: AccountInvoices{ testHash: {}, }, - Payments: map[lntypes.Hash]*PaymentEntry{ + Payments: AccountPayments{ testHash: { Status: lnrpc.Payment_IN_FLIGHT, FullAmount: 2000, diff --git a/accounts/store.go b/accounts/store.go index e8de511d2..37b048907 100644 --- a/accounts/store.go +++ b/accounts/store.go @@ -4,13 +4,13 @@ import ( "bytes" "crypto/rand" "encoding/binary" + "encoding/hex" "fmt" "os" "time" "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/kvdb" - "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "go.etcd.io/bbolt" ) @@ -104,12 +104,39 @@ func (s *BoltStore) Close() error { // NewAccount creates a new OffChainBalanceAccount with the given balance and a // randomly chosen ID. func (s *BoltStore) NewAccount(balance lnwire.MilliSatoshi, - expirationDate time.Time) (*OffChainBalanceAccount, error) { + expirationDate time.Time, label string) (*OffChainBalanceAccount, + error) { if balance == 0 { return nil, fmt.Errorf("a new account cannot have balance of 0") } + // If a label is set, it must be unique, as we use it to identify the + // account in some of the RPCs. It also 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. + 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) + } + + accounts, err := s.Accounts() + if err != nil { + return nil, fmt.Errorf("error checking label "+ + "uniqueness: %w", err) + } + for _, account := range accounts { + if account.Label == label { + return nil, fmt.Errorf("an account with the "+ + "label '%s' already exists", label) + } + } + } + // First, create a new instance of an account. Currently, only the type // TypeInitialBalance is supported. account := &OffChainBalanceAccount{ @@ -118,8 +145,9 @@ func (s *BoltStore) NewAccount(balance lnwire.MilliSatoshi, CurrentBalance: int64(balance), ExpirationDate: expirationDate, LastUpdate: time.Now(), - Invoices: make(map[lntypes.Hash]struct{}), - Payments: make(map[lntypes.Hash]*PaymentEntry), + Invoices: make(AccountInvoices), + Payments: make(AccountPayments), + Label: label, } // Try storing the account in the account database, so we can keep track diff --git a/accounts/store_test.go b/accounts/store_test.go index bd9a37257..145935406 100644 --- a/accounts/store_test.go +++ b/accounts/store_test.go @@ -18,11 +18,11 @@ func TestAccountStore(t *testing.T) { // An initial balance of 0 is not allowed, but later we can reach a // zero balance. - _, err = store.NewAccount(0, time.Time{}) + _, err = store.NewAccount(0, time.Time{}, "") require.ErrorContains(t, err, "cannot have balance of 0") // Create an account that does not expire. - acct1, err := store.NewAccount(123, time.Time{}) + acct1, err := store.NewAccount(123, time.Time{}, "foo") require.NoError(t, err) require.False(t, acct1.HasExpired()) @@ -31,6 +31,14 @@ func TestAccountStore(t *testing.T) { assertEqualAccounts(t, acct1, dbAccount) + // Make sure we cannot create a second account with the same label. + _, err = store.NewAccount(123, time.Time{}, "foo") + require.ErrorContains(t, err, "account with the label 'foo' already") + + // Make sure we cannot set a label that looks like an account ID. + _, err = store.NewAccount(123, time.Time{}, "0011223344556677") + require.ErrorContains(t, err, "is not allowed as it can be mistaken") + // Update all values of the account that we can modify. acct1.CurrentBalance = -500 acct1.ExpirationDate = time.Now() diff --git a/accounts/tlv.go b/accounts/tlv.go index 6c3846692..87cd09e23 100644 --- a/accounts/tlv.go +++ b/accounts/tlv.go @@ -21,6 +21,7 @@ const ( typeExpirationDate tlv.Type = 6 typeInvoices tlv.Type = 7 typePayments tlv.Type = 8 + typeLabel tlv.Type = 9 ) func serializeAccount(account *OffChainBalanceAccount) ([]byte, error) { @@ -34,6 +35,7 @@ func serializeAccount(account *OffChainBalanceAccount) ([]byte, error) { initialBalance = uint64(account.InitialBalance) currentBalance = uint64(account.CurrentBalance) lastUpdate = uint64(account.LastUpdate.UnixNano()) + label = []byte(account.Label) ) tlvRecords := []tlv.Record{ @@ -53,8 +55,9 @@ func serializeAccount(account *OffChainBalanceAccount) ([]byte, error) { tlvRecords = append( tlvRecords, - newHashMapRecord(typeInvoices, &account.Invoices), + newInvoiceEntryMapRecord(typeInvoices, &account.Invoices), newPaymentEntryMapRecord(typePayments, &account.Payments), + tlv.MakePrimitiveRecord(typeLabel, &label), ) tlvStream, err := tlv.NewStream(tlvRecords...) @@ -78,8 +81,9 @@ func deserializeAccount(content []byte) (*OffChainBalanceAccount, error) { currentBalance uint64 lastUpdate uint64 expirationDate uint64 - invoices map[lntypes.Hash]struct{} - payments map[lntypes.Hash]*PaymentEntry + invoices AccountInvoices + payments AccountPayments + label []byte ) tlvStream, err := tlv.NewStream( @@ -89,8 +93,9 @@ func deserializeAccount(content []byte) (*OffChainBalanceAccount, error) { tlv.MakePrimitiveRecord(typeCurrentBalance, ¤tBalance), tlv.MakePrimitiveRecord(typeLastUpdate, &lastUpdate), tlv.MakePrimitiveRecord(typeExpirationDate, &expirationDate), - newHashMapRecord(typeInvoices, &invoices), + newInvoiceEntryMapRecord(typeInvoices, &invoices), newPaymentEntryMapRecord(typePayments, &payments), + tlv.MakePrimitiveRecord(typeLabel, &label), ) if err != nil { return nil, err @@ -108,6 +113,7 @@ func deserializeAccount(content []byte) (*OffChainBalanceAccount, error) { LastUpdate: time.Unix(0, int64(lastUpdate)), Invoices: invoices, Payments: payments, + Label: string(label), } copy(account.ID[:], id) @@ -118,22 +124,23 @@ func deserializeAccount(content []byte) (*OffChainBalanceAccount, error) { return account, nil } -// newHashMapRecord returns a new TLV record for encoding the given map of -// hashes. -func newHashMapRecord(tlvType tlv.Type, - hashMap *map[lntypes.Hash]struct{}) tlv.Record { +// newInvoiceEntryMapRecord returns a new TLV record for encoding the given map +// of invoice hashes. +func newInvoiceEntryMapRecord(tlvType tlv.Type, + invoiceMap *AccountInvoices) tlv.Record { recordSize := func() uint64 { - return uint64(len(*hashMap) * lntypes.HashSize) + return uint64(len(*invoiceMap) * lntypes.HashSize) } return tlv.MakeDynamicRecord( - tlvType, hashMap, recordSize, HashMapEncoder, HashMapDecoder, + tlvType, invoiceMap, recordSize, InvoiceEntryMapEncoder, + InvoiceEntryMapDecoder, ) } -// HashMapEncoder encodes a map of hashes. -func HashMapEncoder(w io.Writer, val any, buf *[8]byte) error { - if t, ok := val.(*map[lntypes.Hash]struct{}); ok { +// InvoiceEntryMapEncoder encodes a map of invoice hashes. +func InvoiceEntryMapEncoder(w io.Writer, val any, buf *[8]byte) error { + if t, ok := val.(*AccountInvoices); ok { if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil { return err } @@ -146,18 +153,20 @@ func HashMapEncoder(w io.Writer, val any, buf *[8]byte) error { } return nil } - return tlv.NewTypeForEncodingErr(val, "*map[lntypes.Hash]struct{}") + return tlv.NewTypeForEncodingErr(val, "*AccountInvoices") } -// HashMapDecoder decodes a map of hashes. -func HashMapDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error { - if typ, ok := val.(*map[lntypes.Hash]struct{}); ok { +// InvoiceEntryMapDecoder decodes a map of invoice hashes. +func InvoiceEntryMapDecoder(r io.Reader, val any, buf *[8]byte, + _ uint64) error { + + if typ, ok := val.(*AccountInvoices); ok { numItems, err := tlv.ReadVarInt(r, buf) if err != nil { return err } - hashes := make(map[lntypes.Hash]struct{}, numItems) + hashes := make(AccountInvoices, numItems) for i := uint64(0); i < numItems; i++ { var item [32]byte if err := tlv.DBytes32(r, &item, buf, 32); err != nil { @@ -168,13 +177,13 @@ func HashMapDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error { *typ = hashes return nil } - return tlv.NewTypeForEncodingErr(val, "*map[lntypes.Hash]struct{}") + return tlv.NewTypeForEncodingErr(val, "*AccountInvoices") } // newPaymentEntryMapRecord returns a new TLV record for encoding the given map // of payment entries. func newPaymentEntryMapRecord(tlvType tlv.Type, - hashMap *map[lntypes.Hash]*PaymentEntry) tlv.Record { + hashMap *AccountPayments) tlv.Record { recordSize := func() uint64 { // We have a 32-byte hash, a single byte for the status and @@ -189,7 +198,7 @@ func newPaymentEntryMapRecord(tlvType tlv.Type, // PaymentEntryMapEncoder encodes a map of payment entries. func PaymentEntryMapEncoder(w io.Writer, val any, buf *[8]byte) error { - if t, ok := val.(*map[lntypes.Hash]*PaymentEntry); ok { + if t, ok := val.(*AccountPayments); ok { if err := tlv.WriteVarInt(w, uint64(len(*t)), buf); err != nil { return err } @@ -214,20 +223,18 @@ func PaymentEntryMapEncoder(w io.Writer, val any, buf *[8]byte) error { } return nil } - return tlv.NewTypeForEncodingErr( - val, "*map[lntypes.Hash]*PaymentEntry", - ) + return tlv.NewTypeForEncodingErr(val, "*AccountPayments") } // PaymentEntryMapDecoder decodes a map of payment entries. func PaymentEntryMapDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error { - if typ, ok := val.(*map[lntypes.Hash]*PaymentEntry); ok { + if typ, ok := val.(*AccountPayments); ok { numItems, err := tlv.ReadVarInt(r, buf) if err != nil { return err } - entries := make(map[lntypes.Hash]*PaymentEntry, numItems) + entries := make(AccountPayments, numItems) for i := uint64(0); i < numItems; i++ { var item [32]byte if err := tlv.DBytes32(r, &item, buf, 32); err != nil { @@ -254,7 +261,5 @@ func PaymentEntryMapDecoder(r io.Reader, val any, buf *[8]byte, _ uint64) error *typ = entries return nil } - return tlv.NewTypeForEncodingErr( - val, "*map[lntypes.Hash]*PaymentEntry", - ) + return tlv.NewTypeForEncodingErr(val, "*AccountPayments") } diff --git a/app/src/types/generated/lit-accounts_pb.d.ts b/app/src/types/generated/lit-accounts_pb.d.ts index 3a2f8d7ac..d461827ce 100644 --- a/app/src/types/generated/lit-accounts_pb.d.ts +++ b/app/src/types/generated/lit-accounts_pb.d.ts @@ -10,6 +10,9 @@ export class CreateAccountRequest extends jspb.Message { getExpirationDate(): string; setExpirationDate(value: string): void; + getLabel(): string; + setLabel(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): CreateAccountRequest.AsObject; static toObject(includeInstance: boolean, msg: CreateAccountRequest): CreateAccountRequest.AsObject; @@ -24,6 +27,7 @@ export namespace CreateAccountRequest { export type AsObject = { accountBalance: string, expirationDate: string, + label: string, } } @@ -81,6 +85,9 @@ export class Account extends jspb.Message { setPaymentsList(value: Array): void; addPayments(value?: AccountPayment, index?: number): AccountPayment; + getLabel(): string; + setLabel(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Account.AsObject; static toObject(includeInstance: boolean, msg: Account): Account.AsObject; @@ -100,6 +107,7 @@ export namespace Account { expirationDate: string, invoicesList: Array, paymentsList: Array, + label: string, } } @@ -165,6 +173,9 @@ export class UpdateAccountRequest extends jspb.Message { getExpirationDate(): string; setExpirationDate(value: string): void; + getLabel(): string; + setLabel(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): UpdateAccountRequest.AsObject; static toObject(includeInstance: boolean, msg: UpdateAccountRequest): UpdateAccountRequest.AsObject; @@ -180,6 +191,7 @@ export namespace UpdateAccountRequest { id: string, accountBalance: string, expirationDate: string, + label: string, } } @@ -221,10 +233,37 @@ export namespace ListAccountsResponse { } } +export class AccountInfoRequest extends jspb.Message { + getId(): string; + setId(value: string): void; + + getLabel(): string; + setLabel(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): AccountInfoRequest.AsObject; + static toObject(includeInstance: boolean, msg: AccountInfoRequest): AccountInfoRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: AccountInfoRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): AccountInfoRequest; + static deserializeBinaryFromReader(message: AccountInfoRequest, reader: jspb.BinaryReader): AccountInfoRequest; +} + +export namespace AccountInfoRequest { + export type AsObject = { + id: string, + label: string, + } +} + export class RemoveAccountRequest extends jspb.Message { getId(): string; setId(value: string): void; + getLabel(): string; + setLabel(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): RemoveAccountRequest.AsObject; static toObject(includeInstance: boolean, msg: RemoveAccountRequest): RemoveAccountRequest.AsObject; @@ -238,6 +277,7 @@ export class RemoveAccountRequest extends jspb.Message { export namespace RemoveAccountRequest { export type AsObject = { id: string, + label: string, } } diff --git a/app/src/types/generated/lit-accounts_pb.js b/app/src/types/generated/lit-accounts_pb.js index 6219b4dd6..59dca2ba5 100644 --- a/app/src/types/generated/lit-accounts_pb.js +++ b/app/src/types/generated/lit-accounts_pb.js @@ -15,6 +15,7 @@ var goog = jspb; var global = Function('return this')(); goog.exportSymbol('proto.litrpc.Account', null, global); +goog.exportSymbol('proto.litrpc.AccountInfoRequest', null, global); goog.exportSymbol('proto.litrpc.AccountInvoice', null, global); goog.exportSymbol('proto.litrpc.AccountPayment', null, global); goog.exportSymbol('proto.litrpc.CreateAccountRequest', null, global); @@ -72,7 +73,8 @@ proto.litrpc.CreateAccountRequest.prototype.toObject = function(opt_includeInsta proto.litrpc.CreateAccountRequest.toObject = function(includeInstance, msg) { var f, obj = { accountBalance: jspb.Message.getFieldWithDefault(msg, 1, "0"), - expirationDate: jspb.Message.getFieldWithDefault(msg, 2, "0") + expirationDate: jspb.Message.getFieldWithDefault(msg, 2, "0"), + label: jspb.Message.getFieldWithDefault(msg, 3, "") }; if (includeInstance) { @@ -117,6 +119,10 @@ proto.litrpc.CreateAccountRequest.deserializeBinaryFromReader = function(msg, re var value = /** @type {string} */ (reader.readInt64String()); msg.setExpirationDate(value); break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; default: reader.skipField(); break; @@ -160,6 +166,13 @@ proto.litrpc.CreateAccountRequest.serializeBinaryToWriter = function(message, wr f ); } + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 3, + f + ); + } }; @@ -193,6 +206,21 @@ proto.litrpc.CreateAccountRequest.prototype.setExpirationDate = function(value) }; +/** + * optional string label = 3; + * @return {string} + */ +proto.litrpc.CreateAccountRequest.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** @param {string} value */ +proto.litrpc.CreateAccountRequest.prototype.setLabel = function(value) { + jspb.Message.setProto3StringField(this, 3, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -465,7 +493,8 @@ proto.litrpc.Account.toObject = function(includeInstance, msg) { invoicesList: jspb.Message.toObjectList(msg.getInvoicesList(), proto.litrpc.AccountInvoice.toObject, includeInstance), paymentsList: jspb.Message.toObjectList(msg.getPaymentsList(), - proto.litrpc.AccountPayment.toObject, includeInstance) + proto.litrpc.AccountPayment.toObject, includeInstance), + label: jspb.Message.getFieldWithDefault(msg, 8, "") }; if (includeInstance) { @@ -532,6 +561,10 @@ proto.litrpc.Account.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,proto.litrpc.AccountPayment.deserializeBinaryFromReader); msg.addPayments(value); break; + case 8: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; default: reader.skipField(); break; @@ -612,6 +645,13 @@ proto.litrpc.Account.serializeBinaryToWriter = function(message, writer) { proto.litrpc.AccountPayment.serializeBinaryToWriter ); } + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 8, + f + ); + } }; @@ -752,6 +792,21 @@ proto.litrpc.Account.prototype.clearPaymentsList = function() { }; +/** + * optional string label = 8; + * @return {string} + */ +proto.litrpc.Account.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** @param {string} value */ +proto.litrpc.Account.prototype.setLabel = function(value) { + jspb.Message.setProto3StringField(this, 8, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -1187,7 +1242,8 @@ proto.litrpc.UpdateAccountRequest.toObject = function(includeInstance, msg) { var f, obj = { id: jspb.Message.getFieldWithDefault(msg, 1, ""), accountBalance: jspb.Message.getFieldWithDefault(msg, 2, "0"), - expirationDate: jspb.Message.getFieldWithDefault(msg, 3, "0") + expirationDate: jspb.Message.getFieldWithDefault(msg, 3, "0"), + label: jspb.Message.getFieldWithDefault(msg, 4, "") }; if (includeInstance) { @@ -1236,6 +1292,10 @@ proto.litrpc.UpdateAccountRequest.deserializeBinaryFromReader = function(msg, re var value = /** @type {string} */ (reader.readInt64String()); msg.setExpirationDate(value); break; + case 4: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; default: reader.skipField(); break; @@ -1286,6 +1346,13 @@ proto.litrpc.UpdateAccountRequest.serializeBinaryToWriter = function(message, wr f ); } + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } }; @@ -1334,6 +1401,21 @@ proto.litrpc.UpdateAccountRequest.prototype.setExpirationDate = function(value) }; +/** + * optional string label = 4; + * @return {string} + */ +proto.litrpc.UpdateAccountRequest.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** @param {string} value */ +proto.litrpc.UpdateAccountRequest.prototype.setLabel = function(value) { + jspb.Message.setProto3StringField(this, 4, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -1619,6 +1701,175 @@ proto.litrpc.ListAccountsResponse.prototype.clearAccountsList = function() { +/** + * 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.AccountInfoRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.litrpc.AccountInfoRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + proto.litrpc.AccountInfoRequest.displayName = 'proto.litrpc.AccountInfoRequest'; +} + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto suitable for use in Soy templates. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. + * @param {boolean=} opt_includeInstance Whether to include the JSPB instance + * for transitional soy proto support: http://goto/soy-param-migration + * @return {!Object} + */ +proto.litrpc.AccountInfoRequest.prototype.toObject = function(opt_includeInstance) { + return proto.litrpc.AccountInfoRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Whether to include the JSPB + * instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.litrpc.AccountInfoRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.AccountInfoRequest.toObject = function(includeInstance, msg) { + var f, obj = { + id: jspb.Message.getFieldWithDefault(msg, 1, ""), + label: jspb.Message.getFieldWithDefault(msg, 2, "") + }; + + 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.AccountInfoRequest} + */ +proto.litrpc.AccountInfoRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.litrpc.AccountInfoRequest; + return proto.litrpc.AccountInfoRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.litrpc.AccountInfoRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.litrpc.AccountInfoRequest} + */ +proto.litrpc.AccountInfoRequest.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; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.litrpc.AccountInfoRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.litrpc.AccountInfoRequest.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.AccountInfoRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.litrpc.AccountInfoRequest.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 + ); + } +}; + + +/** + * optional string id = 1; + * @return {string} + */ +proto.litrpc.AccountInfoRequest.prototype.getId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** @param {string} value */ +proto.litrpc.AccountInfoRequest.prototype.setId = function(value) { + jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional string label = 2; + * @return {string} + */ +proto.litrpc.AccountInfoRequest.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** @param {string} value */ +proto.litrpc.AccountInfoRequest.prototype.setLabel = function(value) { + jspb.Message.setProto3StringField(this, 2, value); +}; + + + /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -1665,7 +1916,8 @@ proto.litrpc.RemoveAccountRequest.prototype.toObject = function(opt_includeInsta */ proto.litrpc.RemoveAccountRequest.toObject = function(includeInstance, msg) { var f, obj = { - id: jspb.Message.getFieldWithDefault(msg, 1, "") + id: jspb.Message.getFieldWithDefault(msg, 1, ""), + label: jspb.Message.getFieldWithDefault(msg, 2, "") }; if (includeInstance) { @@ -1706,6 +1958,10 @@ proto.litrpc.RemoveAccountRequest.deserializeBinaryFromReader = function(msg, re var value = /** @type {string} */ (reader.readString()); msg.setId(value); break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.setLabel(value); + break; default: reader.skipField(); break; @@ -1742,6 +1998,13 @@ proto.litrpc.RemoveAccountRequest.serializeBinaryToWriter = function(message, wr f ); } + f = message.getLabel(); + if (f.length > 0) { + writer.writeString( + 2, + f + ); + } }; @@ -1760,6 +2023,21 @@ proto.litrpc.RemoveAccountRequest.prototype.setId = function(value) { }; +/** + * optional string label = 2; + * @return {string} + */ +proto.litrpc.RemoveAccountRequest.prototype.getLabel = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** @param {string} value */ +proto.litrpc.RemoveAccountRequest.prototype.setLabel = function(value) { + jspb.Message.setProto3StringField(this, 2, value); +}; + + /** * Generated by JsPbCodeGenerator. 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 fe23bbf02..448e29a9d 100644 --- a/app/src/types/generated/lit-accounts_pb_service.d.ts +++ b/app/src/types/generated/lit-accounts_pb_service.d.ts @@ -31,6 +31,15 @@ type AccountsListAccounts = { readonly responseType: typeof lit_accounts_pb.ListAccountsResponse; }; +type AccountsAccountInfo = { + readonly methodName: string; + readonly service: typeof Accounts; + readonly requestStream: false; + readonly responseStream: false; + readonly requestType: typeof lit_accounts_pb.AccountInfoRequest; + readonly responseType: typeof lit_accounts_pb.Account; +}; + type AccountsRemoveAccount = { readonly methodName: string; readonly service: typeof Accounts; @@ -45,6 +54,7 @@ export class Accounts { static readonly CreateAccount: AccountsCreateAccount; static readonly UpdateAccount: AccountsUpdateAccount; static readonly ListAccounts: AccountsListAccounts; + static readonly AccountInfo: AccountsAccountInfo; static readonly RemoveAccount: AccountsRemoveAccount; } @@ -107,6 +117,15 @@ export class AccountsClient { requestMessage: lit_accounts_pb.ListAccountsRequest, callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.ListAccountsResponse|null) => void ): UnaryResponse; + accountInfo( + requestMessage: lit_accounts_pb.AccountInfoRequest, + metadata: grpc.Metadata, + callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.Account|null) => void + ): UnaryResponse; + accountInfo( + requestMessage: lit_accounts_pb.AccountInfoRequest, + callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.Account|null) => void + ): UnaryResponse; removeAccount( requestMessage: lit_accounts_pb.RemoveAccountRequest, 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 3f5dfc7a2..ed4583b95 100644 --- a/app/src/types/generated/lit-accounts_pb_service.js +++ b/app/src/types/generated/lit-accounts_pb_service.js @@ -37,6 +37,15 @@ Accounts.ListAccounts = { responseType: lit_accounts_pb.ListAccountsResponse }; +Accounts.AccountInfo = { + methodName: "AccountInfo", + service: Accounts, + requestStream: false, + responseStream: false, + requestType: lit_accounts_pb.AccountInfoRequest, + responseType: lit_accounts_pb.Account +}; + Accounts.RemoveAccount = { methodName: "RemoveAccount", service: Accounts, @@ -146,6 +155,37 @@ AccountsClient.prototype.listAccounts = function listAccounts(requestMessage, me }; }; +AccountsClient.prototype.accountInfo = function accountInfo(requestMessage, metadata, callback) { + if (arguments.length === 2) { + callback = arguments[1]; + } + var client = grpc.unary(Accounts.AccountInfo, { + 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.removeAccount = function removeAccount(requestMessage, metadata, callback) { if (arguments.length === 2) { callback = arguments[1]; diff --git a/cmd/litcli/accounts.go b/cmd/litcli/accounts.go index 7964e4bae..306ec90e1 100644 --- a/cmd/litcli/accounts.go +++ b/cmd/litcli/accounts.go @@ -7,11 +7,17 @@ import ( "os" "strconv" + "github.com/lightninglabs/lightning-terminal/accounts" "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightningnetwork/lnd/lncfg" "github.com/urfave/cli" ) +const ( + idName = "id" + labelName = "label" +) + var accountsCommands = []cli.Command{ { Name: "accounts", @@ -22,6 +28,7 @@ var accountsCommands = []cli.Command{ createAccountCommand, updateAccountCommand, listAccountsCommand, + accountInfoCommand, removeAccountCommand, }, }, @@ -31,7 +38,7 @@ var createAccountCommand = cli.Command{ Name: "create", ShortName: "c", Usage: "Create a new off-chain account with a balance.", - ArgsUsage: "balance [expiration_date]", + ArgsUsage: "balance [expiration_date] [--label=LABEL] [--save_to=FILE]", Description: ` Adds an entry to the account database. This entry represents an amount of satoshis (account balance) that can be spent using off-chain @@ -61,6 +68,10 @@ var createAccountCommand = cli.Command{ Usage: "store the account macaroon created for the " + "account to the given file", }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) the unique label of the account", + }, }, Action: createAccount, } @@ -111,6 +122,7 @@ func createAccount(ctx *cli.Context) error { req := &litrpc.CreateAccountRequest{ AccountBalance: initialBalance, ExpirationDate: expirationDate, + Label: ctx.String(labelName), } resp, err := client.CreateAccount(ctxb, req) if err != nil { @@ -139,16 +151,20 @@ var updateAccountCommand = cli.Command{ Name: "update", ShortName: "u", Usage: "Update an existing off-chain account.", - ArgsUsage: "id new_balance [new_expiration_date] [--save_to=]", + ArgsUsage: "[id | label] new_balance [new_expiration_date] [--save_to=]", Description: ` Updates an existing off-chain account and sets either a new balance or new expiration date or both. `, Flags: []cli.Flag{ cli.StringFlag{ - Name: "id", + Name: idName, Usage: "the ID of the account to update", }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) the unique label of the account", + }, cli.Int64Flag{ Name: "new_balance", Usage: "the new balance of the account; -1 means do " + @@ -176,29 +192,15 @@ func updateAccount(ctx *cli.Context) error { defer cleanup() client := litrpc.NewAccountsClient(clientConn) + id, label, args, err := parseIDOrLabel(ctx) + if err != nil { + return err + } + var ( - id []byte newBalance int64 expirationDate int64 ) - args := ctx.Args() - - // We parse the ID as hex even though we're supposed to send it as a hex - // encoded string over the RPC. But that way we can verify it's actually - // the id. - switch { - case ctx.IsSet("id"): - id, err = hex.DecodeString(ctx.String("id")) - case args.Present(): - id, err = hex.DecodeString(args.First()) - args = args.Tail() - default: - return fmt.Errorf("id is missing") - } - if err != nil { - return fmt.Errorf("error decoding id: %v", err) - } - switch { case ctx.IsSet("new_balance"): newBalance = ctx.Int64("new_balance") @@ -224,7 +226,8 @@ func updateAccount(ctx *cli.Context) error { } req := &litrpc.UpdateAccountRequest{ - Id: hex.EncodeToString(id), + Id: id, + Label: label, AccountBalance: newBalance, ExpirationDate: expirationDate, } @@ -267,19 +270,71 @@ func listAccounts(ctx *cli.Context) error { return nil } +var accountInfoCommand = cli.Command{ + Name: "info", + ShortName: "i", + Usage: "Show information about a single off-chain account.", + ArgsUsage: "[id | label]", + Description: ` + Returns a single account entry from the account database. + `, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: idName, + Usage: "the ID of the account", + }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) the unique label of the account", + }, + }, + Action: accountInfo, +} + +func accountInfo(ctx *cli.Context) error { + ctxb := context.Background() + clientConn, cleanup, err := connectClient(ctx) + if err != nil { + return err + } + defer cleanup() + client := litrpc.NewAccountsClient(clientConn) + + id, label, _, err := parseIDOrLabel(ctx) + if err != nil { + return err + } + + req := &litrpc.AccountInfoRequest{ + Id: id, + Label: label, + } + resp, err := client.AccountInfo(ctxb, req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + var removeAccountCommand = cli.Command{ Name: "remove", ShortName: "r", Usage: "Removes an off-chain account from the database.", - ArgsUsage: "id", + ArgsUsage: "[id | label]", Description: ` Removes an account entry from the account database. `, Flags: []cli.Flag{ cli.StringFlag{ - Name: "id", + Name: idName, Usage: "the ID of the account", }, + cli.StringFlag{ + Name: labelName, + Usage: "(optional) the unique label of the account", + }, }, Action: removeAccount, } @@ -293,29 +348,59 @@ func removeAccount(ctx *cli.Context) error { defer cleanup() client := litrpc.NewAccountsClient(clientConn) - var accountID string + id, label, _, err := parseIDOrLabel(ctx) + if err != nil { + return err + } + + req := &litrpc.RemoveAccountRequest{ + Id: id, + Label: label, + } + _, err = client.RemoveAccount(ctxb, req) + return err +} + +// parseIDOrLabel parses either the id or label from the command line. +func parseIDOrLabel(ctx *cli.Context) (string, string, cli.Args, error) { + var ( + accountID string + label string + ) args := ctx.Args() switch { - case ctx.IsSet("id"): - accountID = ctx.String("id") + case ctx.IsSet(idName) && ctx.IsSet(labelName): + return "", "", nil, fmt.Errorf("either account ID or label " + + "must be specified, not both") + + case ctx.IsSet(idName): + accountID = ctx.String(idName) + + case ctx.IsSet(labelName): + label = ctx.String(labelName) + case args.Present(): accountID = args.First() args = args.Tail() - default: - return fmt.Errorf("id argument missing") - } - if len(accountID) == 0 { - return fmt.Errorf("id argument missing") - } - if _, err := hex.DecodeString(accountID); err != nil { - return err - } + // Since we have a positional argument, we cannot be sure it's + // an ID. So we check if it's an ID by trying to hex decode it + // and by checking the length. This will break if the user + // chooses labels that are also valid hex encoded IDs. But since + // the label is supposed to be human-readable, this should be + // unlikely. + _, err := hex.DecodeString(accountID) + if len(accountID) != hex.EncodedLen(accounts.AccountIDLen) || + err != nil { + + label = accountID + accountID = "" + } - req := &litrpc.RemoveAccountRequest{ - Id: accountID, + default: + return "", "", nil, fmt.Errorf("id argument missing") } - _, err = client.RemoveAccount(ctxb, req) - return err + + return accountID, label, args, nil } diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index c2389af63..79d0b0677 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -2,7 +2,8 @@ package itest import ( "context" - "io/ioutil" + "fmt" + "os" "strings" "testing" "time" @@ -89,18 +90,22 @@ func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort, // authentication mechanism. rawConn, err := connectRPC(ctxt, hostPort, tlsCertPath) require.NoError(t.t, err) - defer rawConn.Close() + defer func() { + require.NoError(t.t, rawConn.Close()) + }() - macBytes, err := ioutil.ReadFile(macPath) + macBytes, err := os.ReadFile(macPath) require.NoError(t.t, err) ctxm := macaroonContext(ctxt, macBytes) acctClient := litrpc.NewAccountsClient(rawConn) // Create a new account with a balance of 50k sats. const acctBalance uint64 = 50_000 + acctLabel := fmt.Sprintf("test account %d", runNumber) acctResp, err := acctClient.CreateAccount( ctxm, &litrpc.CreateAccountRequest{ AccountBalance: acctBalance, + Label: acctLabel, }, ) require.NoError(t.t, err) @@ -108,6 +113,18 @@ func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort, require.Greater(t.t, len(acctResp.Account.Id), 12) require.EqualValues(t.t, acctBalance, acctResp.Account.CurrentBalance) require.EqualValues(t.t, acctBalance, acctResp.Account.InitialBalance) + require.Equal(t.t, acctLabel, acctResp.Account.Label) + + // Make sure we can also query the account by its name. + infoResp, err := acctClient.AccountInfo( + ctxm, &litrpc.AccountInfoRequest{ + Label: acctLabel, + }, + ) + require.Equal(t.t, acctResp.Account.Id, infoResp.Id) + require.EqualValues(t.t, acctBalance, infoResp.CurrentBalance) + require.EqualValues(t.t, acctBalance, infoResp.InitialBalance) + require.Equal(t.t, acctLabel, infoResp.Label) // Now we got a new macaroon that has the account caveat attached to it. ctxa := macaroonContext(ctxt, acctResp.Macaroon) @@ -191,7 +208,9 @@ func testAccountRestrictionsLNC(ctxm context.Context, t *harnessTest, rawLNCConn, err := connectMailboxWithPairingPhrase(ctxt, connectPhrase) require.NoError(t.t, err) - defer rawLNCConn.Close() + defer func() { + require.NoError(t.t, rawLNCConn.Close()) + }() lightningClient := lnrpc.NewLightningClient(rawLNCConn) diff --git a/litrpc/accounts.pb.json.go b/litrpc/accounts.pb.json.go index 5c8f724eb..f8cf5c089 100644 --- a/litrpc/accounts.pb.json.go +++ b/litrpc/accounts.pb.json.go @@ -96,6 +96,31 @@ func RegisterAccountsJSONCallbacks(registry map[string]func(ctx context.Context, callback(string(respBytes), nil) } + registry["litrpc.Accounts.AccountInfo"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &AccountInfoRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewAccountsClient(conn) + resp, err := client.AccountInfo(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.RemoveAccount"] = 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 a3aedfd9e..ff99e25fb 100644 --- a/litrpc/lit-accounts.pb.go +++ b/litrpc/lit-accounts.pb.go @@ -30,6 +30,9 @@ type CreateAccountRequest struct { AccountBalance uint64 `protobuf:"varint,1,opt,name=account_balance,json=accountBalance,proto3" json:"account_balance,omitempty"` // The expiration date of the account as a timestamp. Set to 0 to never expire. ExpirationDate int64 `protobuf:"varint,2,opt,name=expiration_date,json=expirationDate,proto3" json:"expiration_date,omitempty"` + // An optional label to identify the account. If the label is not empty, then + // it must be unique, otherwise it couldn't be used to query a single account. + Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` } func (x *CreateAccountRequest) Reset() { @@ -78,6 +81,13 @@ func (x *CreateAccountRequest) GetExpirationDate() int64 { return 0 } +func (x *CreateAccountRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type CreateAccountResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -156,6 +166,9 @@ type Account struct { // The list of payments made by the account. A payment made by an account will // debit the account balance if it is settled. Payments []*AccountPayment `protobuf:"bytes,7,rep,name=payments,proto3" json:"payments,omitempty"` + // An optional label to identify the account. If this is not empty, then it is + // guaranteed to be unique. + Label string `protobuf:"bytes,8,opt,name=label,proto3" json:"label,omitempty"` } func (x *Account) Reset() { @@ -239,6 +252,13 @@ func (x *Account) GetPayments() []*AccountPayment { return nil } +func (x *Account) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type AccountInvoice struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -360,13 +380,16 @@ type UpdateAccountRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The ID of the account to update. + // 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 new account balance to set. Set to -1 to not update the balance. AccountBalance int64 `protobuf:"varint,2,opt,name=account_balance,json=accountBalance,proto3" json:"account_balance,omitempty"` // The new account expiry to set. Set to -1 to not update the expiry. Set to 0 // to never expire. ExpirationDate int64 `protobuf:"varint,3,opt,name=expiration_date,json=expirationDate,proto3" json:"expiration_date,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,4,opt,name=label,proto3" json:"label,omitempty"` } func (x *UpdateAccountRequest) Reset() { @@ -422,6 +445,13 @@ func (x *UpdateAccountRequest) GetExpirationDate() int64 { return 0 } +func (x *UpdateAccountRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type ListAccountsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -508,19 +538,82 @@ func (x *ListAccountsResponse) GetAccounts() []*Account { return nil } +type AccountInfoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The hexadecimal ID of the account to remove. 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 remove. 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"` +} + +func (x *AccountInfoRequest) Reset() { + *x = AccountInfoRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_lit_accounts_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AccountInfoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountInfoRequest) ProtoMessage() {} + +func (x *AccountInfoRequest) ProtoReflect() protoreflect.Message { + mi := &file_lit_accounts_proto_msgTypes[8] + 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 AccountInfoRequest.ProtoReflect.Descriptor instead. +func (*AccountInfoRequest) Descriptor() ([]byte, []int) { + return file_lit_accounts_proto_rawDescGZIP(), []int{8} +} + +func (x *AccountInfoRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *AccountInfoRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type RemoveAccountRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The hexadecimal ID of the account to remove. + // The hexadecimal ID of the account to remove. 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 remove. 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"` } func (x *RemoveAccountRequest) Reset() { *x = RemoveAccountRequest{} 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) } @@ -533,7 +626,7 @@ func (x *RemoveAccountRequest) String() string { func (*RemoveAccountRequest) ProtoMessage() {} func (x *RemoveAccountRequest) 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 { @@ -546,7 +639,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{8} + return file_lit_accounts_proto_rawDescGZIP(), []int{9} } func (x *RemoveAccountRequest) GetId() string { @@ -556,6 +649,13 @@ func (x *RemoveAccountRequest) GetId() string { return "" } +func (x *RemoveAccountRequest) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + type RemoveAccountResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -565,7 +665,7 @@ type RemoveAccountResponse struct { func (x *RemoveAccountResponse) Reset() { *x = RemoveAccountResponse{} 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) } @@ -578,7 +678,7 @@ func (x *RemoveAccountResponse) String() string { func (*RemoveAccountResponse) ProtoMessage() {} func (x *RemoveAccountResponse) 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 { @@ -591,94 +691,107 @@ func (x *RemoveAccountResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RemoveAccountResponse.ProtoReflect.Descriptor instead. func (*RemoveAccountResponse) Descriptor() ([]byte, []int) { - return file_lit_accounts_proto_rawDescGZIP(), []int{9} + return file_lit_accounts_proto_rawDescGZIP(), []int{10} } var File_lit_accounts_proto protoreflect.FileDescriptor var file_lit_accounts_proto_rawDesc = []byte{ 0x0a, 0x12, 0x6c, 0x69, 0x74, 0x2d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x68, 0x0a, 0x14, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x22, 0x7e, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x22, 0x5e, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x29, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, - 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x61, - 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, 0x9d, 0x02, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x69, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x62, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, - 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, - 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x32, - 0x0a, 0x08, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x08, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, - 0x65, 0x73, 0x12, 0x32, 0x0a, 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x08, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x24, 0x0a, 0x0e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x5b, 0x0a, 0x0e, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, - 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, - 0x73, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x75, 0x6c, 0x6c, - 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, - 0x75, 0x6c, 0x6c, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x78, 0x0a, 0x14, 0x55, 0x70, 0x64, + 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x5e, 0x0a, 0x15, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x61, 0x72, 0x6f, 0x6f, 0x6e, 0x22, 0xb3, 0x02, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x69, 0x74, + 0x69, 0x61, 0x6c, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x44, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x52, 0x08, + 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x08, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6c, 0x69, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, + 0x65, 0x6c, 0x22, 0x24, 0x0a, 0x0e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x76, + 0x6f, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x5b, 0x0a, 0x0e, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x41, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8e, 0x01, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 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, 0x27, + 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, + 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, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x61, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x65, 0x78, - 0x70, 0x69, 0x72, 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, 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, - 0x26, 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, 0x22, 0x17, 0x0a, 0x15, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 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, - 0x32, 0xb1, 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, 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, + 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, } var ( @@ -693,7 +806,7 @@ func file_lit_accounts_proto_rawDescGZIP() []byte { return file_lit_accounts_proto_rawDescData } -var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_lit_accounts_proto_goTypes = []interface{}{ (*CreateAccountRequest)(nil), // 0: litrpc.CreateAccountRequest (*CreateAccountResponse)(nil), // 1: litrpc.CreateAccountResponse @@ -703,27 +816,30 @@ var file_lit_accounts_proto_goTypes = []interface{}{ (*UpdateAccountRequest)(nil), // 5: litrpc.UpdateAccountRequest (*ListAccountsRequest)(nil), // 6: litrpc.ListAccountsRequest (*ListAccountsResponse)(nil), // 7: litrpc.ListAccountsResponse - (*RemoveAccountRequest)(nil), // 8: litrpc.RemoveAccountRequest - (*RemoveAccountResponse)(nil), // 9: litrpc.RemoveAccountResponse + (*AccountInfoRequest)(nil), // 8: litrpc.AccountInfoRequest + (*RemoveAccountRequest)(nil), // 9: litrpc.RemoveAccountRequest + (*RemoveAccountResponse)(nil), // 10: litrpc.RemoveAccountResponse } var file_lit_accounts_proto_depIdxs = []int32{ - 2, // 0: litrpc.CreateAccountResponse.account:type_name -> litrpc.Account - 3, // 1: litrpc.Account.invoices:type_name -> litrpc.AccountInvoice - 4, // 2: litrpc.Account.payments:type_name -> litrpc.AccountPayment - 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.RemoveAccount:input_type -> litrpc.RemoveAccountRequest - 1, // 8: litrpc.Accounts.CreateAccount:output_type -> litrpc.CreateAccountResponse - 2, // 9: litrpc.Accounts.UpdateAccount:output_type -> litrpc.Account - 7, // 10: litrpc.Accounts.ListAccounts:output_type -> litrpc.ListAccountsResponse - 9, // 11: litrpc.Accounts.RemoveAccount:output_type -> litrpc.RemoveAccountResponse - 8, // [8:12] is the sub-list for method output_type - 4, // [4:8] 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 + 2, // 0: litrpc.CreateAccountResponse.account:type_name -> litrpc.Account + 3, // 1: litrpc.Account.invoices:type_name -> litrpc.AccountInvoice + 4, // 2: litrpc.Account.payments:type_name -> litrpc.AccountPayment + 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 + 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 } func init() { file_lit_accounts_proto_init() } @@ -829,7 +945,7 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RemoveAccountRequest); i { + switch v := v.(*AccountInfoRequest); i { case 0: return &v.state case 1: @@ -841,6 +957,18 @@ func file_lit_accounts_proto_init() { } } file_lit_accounts_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + 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[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RemoveAccountResponse); i { case 0: return &v.state @@ -859,7 +987,7 @@ func file_lit_accounts_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_lit_accounts_proto_rawDesc, NumEnums: 0, - NumMessages: 10, + NumMessages: 11, NumExtensions: 0, NumServices: 1, }, diff --git a/litrpc/lit-accounts.pb.gw.go b/litrpc/lit-accounts.pb.gw.go index e8b727f59..a7c2ef530 100644 --- a/litrpc/lit-accounts.pb.gw.go +++ b/litrpc/lit-accounts.pb.gw.go @@ -151,6 +151,10 @@ func local_request_Accounts_ListAccounts_0(ctx context.Context, marshaler runtim } +var ( + filter_Accounts_RemoveAccount_0 = &utilities.DoubleArray{Encoding: map[string]int{"id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} +) + func request_Accounts_RemoveAccount_0(ctx context.Context, marshaler runtime.Marshaler, client AccountsClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq RemoveAccountRequest var metadata runtime.ServerMetadata @@ -172,6 +176,13 @@ func request_Accounts_RemoveAccount_0(ctx context.Context, marshaler runtime.Mar return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Accounts_RemoveAccount_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.RemoveAccount(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err @@ -198,6 +209,13 @@ func local_request_Accounts_RemoveAccount_0(ctx context.Context, marshaler runti return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Accounts_RemoveAccount_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.RemoveAccount(ctx, &protoReq) return msg, metadata, err diff --git a/litrpc/lit-accounts.proto b/litrpc/lit-accounts.proto index cdd112ace..209183a12 100644 --- a/litrpc/lit-accounts.proto +++ b/litrpc/lit-accounts.proto @@ -31,6 +31,11 @@ service Accounts { */ rpc ListAccounts (ListAccountsRequest) returns (ListAccountsResponse); + /* litcli: `accounts info` + AccountInfo returns the account with the given ID or label. + */ + rpc AccountInfo (AccountInfoRequest) returns (Account); + /* litcli: `accounts remove` RemoveAccount removes the given account from the account database. */ @@ -48,6 +53,12 @@ message CreateAccountRequest { The expiration date of the account as a timestamp. Set to 0 to never expire. */ int64 expiration_date = 2; + + /* + An optional label to identify the account. If the label is not empty, then + it must be unique, otherwise it couldn't be used to query a single account. + */ + string label = 3; } message CreateAccountResponse { @@ -89,6 +100,12 @@ message Account { debit the account balance if it is settled. */ repeated AccountPayment payments = 7; + + /* + An optional label to identify the account. If this is not empty, then it is + guaranteed to be unique. + */ + string label = 8; } message AccountInvoice { @@ -112,7 +129,7 @@ message AccountPayment { } message UpdateAccountRequest { - // The ID of the account to update. + // The ID of the account to update. Either the ID or the label must be set. string id = 1; // The new account balance to set. Set to -1 to not update the balance. @@ -123,6 +140,12 @@ message UpdateAccountRequest { to never expire. */ int64 expiration_date = 3; + + /* + The label of the account to update. If an account has no label, then the ID + must be used instead. + */ + string label = 4; } message ListAccountsRequest { @@ -133,9 +156,32 @@ message ListAccountsResponse { repeated Account accounts = 1; } +message AccountInfoRequest { + /* + The hexadecimal ID of the account to remove. Either the ID or the label must + be set. + */ + string id = 1; + + /* + The label of the account to remove. If an account has no label, then the ID + must be used instead. + */ + string label = 2; +} + message RemoveAccountRequest { - // The hexadecimal ID of the account to remove. + /* + The hexadecimal ID of the account to remove. Either the ID or the label must + be set. + */ string id = 1; + + /* + The label of the account to remove. If an account has no label, then the ID + must be used instead. + */ + string label = 2; } message RemoveAccountResponse { diff --git a/litrpc/lit-accounts.swagger.json b/litrpc/lit-accounts.swagger.json index 18bfb60fe..1630aaf54 100644 --- a/litrpc/lit-accounts.swagger.json +++ b/litrpc/lit-accounts.swagger.json @@ -92,10 +92,17 @@ "parameters": [ { "name": "id", - "description": "The hexadecimal ID of the account to remove.", + "description": "The hexadecimal ID of the account to remove. Either the ID or the label must\nbe set.", "in": "path", "required": true, "type": "string" + }, + { + "name": "label", + "description": "The label of the account to remove. If an account has no label, then the ID\nmust be used instead.", + "in": "query", + "required": false, + "type": "string" } ], "tags": [ @@ -122,7 +129,7 @@ "parameters": [ { "name": "id", - "description": "The ID of the account to update.", + "description": "The ID of the account to update. Either the ID or the label must be set.", "in": "path", "required": true, "type": "string" @@ -143,6 +150,10 @@ "type": "string", "format": "int64", "description": "The new account expiry to set. Set to -1 to not update the expiry. Set to 0\nto never expire." + }, + "label": { + "type": "string", + "description": "The label of the account to update. If an account has no label, then the ID\nmust be used instead." } } } @@ -195,6 +206,10 @@ "$ref": "#/definitions/litrpcAccountPayment" }, "description": "The list of payments made by the account. A payment made by an account will\ndebit the account balance if it is settled." + }, + "label": { + "type": "string", + "description": "An optional label to identify the account. If this is not empty, then it is\nguaranteed to be unique." } } }, @@ -239,6 +254,10 @@ "type": "string", "format": "int64", "description": "The expiration date of the account as a timestamp. Set to 0 to never expire." + }, + "label": { + "type": "string", + "description": "An optional label to identify the account. If the label is not empty, then\nit must be unique, otherwise it couldn't be used to query a single account." } } }, diff --git a/litrpc/lit-accounts_grpc.pb.go b/litrpc/lit-accounts_grpc.pb.go index a12496481..2a643f041 100644 --- a/litrpc/lit-accounts_grpc.pb.go +++ b/litrpc/lit-accounts_grpc.pb.go @@ -38,6 +38,9 @@ type AccountsClient interface { // ListAccounts returns all accounts that are currently stored in the account // database. ListAccounts(ctx context.Context, in *ListAccountsRequest, opts ...grpc.CallOption) (*ListAccountsResponse, error) + // litcli: `accounts info` + // AccountInfo returns the account with the given ID or label. + AccountInfo(ctx context.Context, in *AccountInfoRequest, opts ...grpc.CallOption) (*Account, error) // litcli: `accounts remove` // RemoveAccount removes the given account from the account database. RemoveAccount(ctx context.Context, in *RemoveAccountRequest, opts ...grpc.CallOption) (*RemoveAccountResponse, error) @@ -78,6 +81,15 @@ func (c *accountsClient) ListAccounts(ctx context.Context, in *ListAccountsReque return out, nil } +func (c *accountsClient) AccountInfo(ctx context.Context, in *AccountInfoRequest, opts ...grpc.CallOption) (*Account, error) { + out := new(Account) + err := c.cc.Invoke(ctx, "/litrpc.Accounts/AccountInfo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *accountsClient) RemoveAccount(ctx context.Context, in *RemoveAccountRequest, opts ...grpc.CallOption) (*RemoveAccountResponse, error) { out := new(RemoveAccountResponse) err := c.cc.Invoke(ctx, "/litrpc.Accounts/RemoveAccount", in, out, opts...) @@ -111,6 +123,9 @@ type AccountsServer interface { // ListAccounts returns all accounts that are currently stored in the account // database. ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error) + // litcli: `accounts info` + // AccountInfo returns the account with the given ID or label. + AccountInfo(context.Context, *AccountInfoRequest) (*Account, error) // litcli: `accounts remove` // RemoveAccount removes the given account from the account database. RemoveAccount(context.Context, *RemoveAccountRequest) (*RemoveAccountResponse, error) @@ -130,6 +145,9 @@ func (UnimplementedAccountsServer) UpdateAccount(context.Context, *UpdateAccount func (UnimplementedAccountsServer) ListAccounts(context.Context, *ListAccountsRequest) (*ListAccountsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListAccounts not implemented") } +func (UnimplementedAccountsServer) AccountInfo(context.Context, *AccountInfoRequest) (*Account, error) { + return nil, status.Errorf(codes.Unimplemented, "method AccountInfo not implemented") +} func (UnimplementedAccountsServer) RemoveAccount(context.Context, *RemoveAccountRequest) (*RemoveAccountResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RemoveAccount not implemented") } @@ -200,6 +218,24 @@ func _Accounts_ListAccounts_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Accounts_AccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AccountInfoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AccountsServer).AccountInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/litrpc.Accounts/AccountInfo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AccountsServer).AccountInfo(ctx, req.(*AccountInfoRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Accounts_RemoveAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RemoveAccountRequest) if err := dec(in); err != nil { @@ -237,6 +273,10 @@ var Accounts_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListAccounts", Handler: _Accounts_ListAccounts_Handler, }, + { + MethodName: "AccountInfo", + Handler: _Accounts_AccountInfo_Handler, + }, { MethodName: "RemoveAccount", Handler: _Accounts_RemoveAccount_Handler, diff --git a/perms/permissions.go b/perms/permissions.go index f945f26e9..91692be65 100644 --- a/perms/permissions.go +++ b/perms/permissions.go @@ -32,6 +32,10 @@ var ( Entity: "account", Action: "read", }}, + "/litrpc.Accounts/AccountInfo": {{ + Entity: "account", + Action: "read", + }}, "/litrpc.Accounts/RemoveAccount": {{ Entity: "account", Action: "write", diff --git a/proto/lit-accounts.proto b/proto/lit-accounts.proto index 20eca3db9..bc016819d 100644 --- a/proto/lit-accounts.proto +++ b/proto/lit-accounts.proto @@ -31,6 +31,11 @@ service Accounts { */ rpc ListAccounts (ListAccountsRequest) returns (ListAccountsResponse); + /* litcli: `accounts info` + AccountInfo returns the account with the given ID or label. + */ + rpc AccountInfo (AccountInfoRequest) returns (Account); + /* litcli: `accounts remove` RemoveAccount removes the given account from the account database. */ @@ -48,6 +53,12 @@ message CreateAccountRequest { The expiration date of the account as a timestamp. Set to 0 to never expire. */ int64 expiration_date = 2 [jstype = JS_STRING]; + + /* + An optional label to identify the account. If the label is not empty, then + it must be unique, otherwise it couldn't be used to query a single account. + */ + string label = 3; } message CreateAccountResponse { @@ -89,6 +100,12 @@ message Account { debit the account balance if it is settled. */ repeated AccountPayment payments = 7; + + /* + An optional label to identify the account. If this is not empty, then it is + guaranteed to be unique. + */ + string label = 8; } message AccountInvoice { @@ -112,7 +129,7 @@ message AccountPayment { } message UpdateAccountRequest { - // The ID of the account to update. + // The ID of the account to update. Either the ID or the label must be set. string id = 1; // The new account balance to set. Set to -1 to not update the balance. @@ -123,6 +140,12 @@ message UpdateAccountRequest { to never expire. */ int64 expiration_date = 3 [jstype = JS_STRING]; + + /* + The label of the account to update. If an account has no label, then the ID + must be used instead. + */ + string label = 4; } message ListAccountsRequest { @@ -133,9 +156,32 @@ message ListAccountsResponse { repeated Account accounts = 1; } +message AccountInfoRequest { + /* + The hexadecimal ID of the account to remove. Either the ID or the label must + be set. + */ + string id = 1; + + /* + The label of the account to remove. If an account has no label, then the ID + must be used instead. + */ + string label = 2; +} + message RemoveAccountRequest { - // The hexadecimal ID of the account to remove. + /* + The hexadecimal ID of the account to remove. Either the ID or the label must + be set. + */ string id = 1; + + /* + The label of the account to remove. If an account has no label, then the ID + must be used instead. + */ + string label = 2; } message RemoveAccountResponse {