-
Notifications
You must be signed in to change notification settings - Fork 128
accounts: add account payments subcommand #1316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
f5a1a02
a132fca
5632998
857fb90
009a5c2
d553cb2
6754acc
1abbbe6
0977e37
c6d81a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,18 +5,43 @@ import ( | |||||||||||||||||||||||
| "encoding/hex" | ||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| "github.com/btcsuite/btcd/btcutil" | ||||||||||||||||||||||||
| "github.com/lightninglabs/lightning-terminal/litrpc" | ||||||||||||||||||||||||
| litmac "github.com/lightninglabs/lightning-terminal/macaroons" | ||||||||||||||||||||||||
| "github.com/lightningnetwork/lnd/lnrpc" | ||||||||||||||||||||||||
| "github.com/lightningnetwork/lnd/lnrpc/routerrpc" | ||||||||||||||||||||||||
| "github.com/lightningnetwork/lnd/lntypes" | ||||||||||||||||||||||||
| "github.com/lightningnetwork/lnd/lnwire" | ||||||||||||||||||||||||
| "github.com/lightningnetwork/lnd/macaroons" | ||||||||||||||||||||||||
| "google.golang.org/grpc/codes" | ||||||||||||||||||||||||
| "google.golang.org/grpc/status" | ||||||||||||||||||||||||
| "gopkg.in/macaroon-bakery.v2/bakery/checkers" | ||||||||||||||||||||||||
| "gopkg.in/macaroon.v2" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||
| // DefaultMaxPayments is the default page size for listing payments. | ||||||||||||||||||||||||
| DefaultMaxPayments = 20 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // MaxPaymentsLimit is the maximum page size allowed for listing | ||||||||||||||||||||||||
| // payments. | ||||||||||||||||||||||||
| MaxPaymentsLimit = 50 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // MaxIndexOffset is the maximum index offset allowed. | ||||||||||||||||||||||||
| MaxIndexOffset = 0x7fffffff | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // DefaultTrackPaymentTimeout is the timeout for track payment RPC | ||||||||||||||||||||||||
| // calls. | ||||||||||||||||||||||||
| DefaultTrackPaymentTimeout = 8 * time.Second | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // MaxConcurrentTrackPayments is the maximum number of concurrent track | ||||||||||||||||||||||||
| // payment requests. | ||||||||||||||||||||||||
| MaxConcurrentTrackPayments = 10 | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||
| // ErrServerNotActive indicates that the server has started but hasn't | ||||||||||||||||||||||||
| // fully finished the startup process. | ||||||||||||||||||||||||
|
|
@@ -371,3 +396,223 @@ func marshalAccount(acct *OffChainBalanceAccount) *litrpc.Account { | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return rpcAccount | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // AccountPayments returns the detailed payment history for the given account. | ||||||||||||||||||||||||
| func (s *RPCServer) AccountPayments(ctx context.Context, | ||||||||||||||||||||||||
| req *litrpc.AccountPaymentsRequest) ( | ||||||||||||||||||||||||
| *litrpc.AccountPaymentsResponse, error) { | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if req.GetAccount() == nil { | ||||||||||||||||||||||||
| return nil, fmt.Errorf("account param must be specified") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var id, label string | ||||||||||||||||||||||||
| switch idType := req.Account.Identifier.(type) { | ||||||||||||||||||||||||
| case *litrpc.AccountIdentifier_Id: | ||||||||||||||||||||||||
| id = idType.Id | ||||||||||||||||||||||||
| case *litrpc.AccountIdentifier_Label: | ||||||||||||||||||||||||
| label = idType.Label | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| log.Infof("[accountpayments] id=%s, label=%v, max_payments=%d, "+ | ||||||||||||||||||||||||
| "index_offset=%d, count_total_payments=%v", | ||||||||||||||||||||||||
| id, label, req.MaxPayments, req.IndexOffset, | ||||||||||||||||||||||||
| req.CountTotalPayments) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| accountID, err := s.findAccount(ctx, id, label) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Determine limits. | ||||||||||||||||||||||||
| limit := req.MaxPayments | ||||||||||||||||||||||||
| if limit == 0 { | ||||||||||||||||||||||||
| limit = DefaultMaxPayments | ||||||||||||||||||||||||
| } else if limit > MaxPaymentsLimit { | ||||||||||||||||||||||||
| return nil, fmt.Errorf( | ||||||||||||||||||||||||
| "max_payments cannot exceed %d", MaxPaymentsLimit, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if req.IndexOffset > MaxIndexOffset { | ||||||||||||||||||||||||
| return nil, fmt.Errorf("index_offset out of range") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| offset := req.IndexOffset | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Fetch the paginated payment entries from the store. | ||||||||||||||||||||||||
| paymentsFromStore, err := s.service.store.ListAccountPayments( | ||||||||||||||||||||||||
| ctx, accountID, int32(offset), int32(limit), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| return nil, fmt.Errorf( | ||||||||||||||||||||||||
| "unable to list account payments: %w", err, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Fetch total payment count if requested. | ||||||||||||||||||||||||
| var totalNumPayments uint64 | ||||||||||||||||||||||||
| if req.CountTotalPayments { | ||||||||||||||||||||||||
| total, err := s.service.store.CountAccountPayments( | ||||||||||||||||||||||||
| ctx, accountID, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| return nil, fmt.Errorf( | ||||||||||||||||||||||||
| "unable to count account payments: %w", err, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| totalNumPayments = total | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Fetch the detailed payments concurrently from LND. | ||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||
| wg sync.WaitGroup | ||||||||||||||||||||||||
| mu sync.Mutex | ||||||||||||||||||||||||
| payments = make(map[lntypes.Hash]*lnrpc.Payment) | ||||||||||||||||||||||||
| errs []error | ||||||||||||||||||||||||
| sem = make(chan struct{}, MaxConcurrentTrackPayments) | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
bitromortac marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| rawCtx, _, client := s.service.routerClient.RawClientWithMacAuth(ctx) | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second return value of
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for _, entry := range paymentsFromStore { | ||||||||||||||||||||||||
| entry := entry | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| wg.Add(1) | ||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
| defer wg.Done() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Bounded concurrency using semaphore. | ||||||||||||||||||||||||
| select { | ||||||||||||||||||||||||
| case sem <- struct{}{}: | ||||||||||||||||||||||||
| case <-rawCtx.Done(): | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
bitromortac marked this conversation as resolved.
|
||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
bitromortac marked this conversation as resolved.
|
||||||||||||||||||||||||
| defer func() { <-sem }() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Add a timeout to prevent hanging streaming calls. | ||||||||||||||||||||||||
| trackCtx, trackCancel := context.WithTimeout( | ||||||||||||||||||||||||
| rawCtx, DefaultTrackPaymentTimeout, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| defer trackCancel() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| stream, err := client.TrackPaymentV2( | ||||||||||||||||||||||||
| trackCtx, &routerrpc.TrackPaymentRequest{ | ||||||||||||||||||||||||
| PaymentHash: entry.Hash[:], | ||||||||||||||||||||||||
| NoInflightUpdates: false, | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| // Skip error logging and recording if the | ||||||||||||||||||||||||
| // parent context was cancelled or timed out. | ||||||||||||||||||||||||
| if rawCtx.Err() != nil { | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| sErr, ok := status.FromError(err) | ||||||||||||||||||||||||
| if ok && sErr.Code() == codes.NotFound { | ||||||||||||||||||||||||
| log.Warnf("Payment %x not found in "+ | ||||||||||||||||||||||||
| "lnd, creating placeholder: %v", | ||||||||||||||||||||||||
| entry.Hash[:], err) | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| mu.Lock() | ||||||||||||||||||||||||
| payments[entry.Hash] = | ||||||||||||||||||||||||
| notFoundPayment(entry.Hash) | ||||||||||||||||||||||||
| mu.Unlock() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| log.Errorf("Failed to track payment %x: %v", | ||||||||||||||||||||||||
| entry.Hash[:], err) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| mu.Lock() | ||||||||||||||||||||||||
| errs = append(errs, err) | ||||||||||||||||||||||||
| mu.Unlock() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| payment, err := stream.Recv() | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||
| // Skip error logging and recording if the | ||||||||||||||||||||||||
| // parent context was cancelled or timed out. | ||||||||||||||||||||||||
| if rawCtx.Err() != nil { | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| sErr, ok := status.FromError(err) | ||||||||||||||||||||||||
| if ok && sErr.Code() == codes.NotFound { | ||||||||||||||||||||||||
| log.Warnf("Payment %x not found in "+ | ||||||||||||||||||||||||
| "lnd, creating placeholder: %v", | ||||||||||||||||||||||||
| entry.Hash[:], err) | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| mu.Lock() | ||||||||||||||||||||||||
| payments[entry.Hash] = | ||||||||||||||||||||||||
| notFoundPayment(entry.Hash) | ||||||||||||||||||||||||
| mu.Unlock() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| log.Errorf("Failed to receive payment "+ | ||||||||||||||||||||||||
| "update for %x: %v", entry.Hash[:], err) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| mu.Lock() | ||||||||||||||||||||||||
| errs = append(errs, err) | ||||||||||||||||||||||||
| mu.Unlock() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
bitromortac marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| mu.Lock() | ||||||||||||||||||||||||
| payments[entry.Hash] = payment | ||||||||||||||||||||||||
| mu.Unlock() | ||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| wg.Wait() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Return immediately if the parent context was cancelled or timed out. | ||||||||||||||||||||||||
| if err := rawCtx.Err(); err != nil { | ||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // If there were any errors tracking the payments, return the | ||||||||||||||||||||||||
| // error. | ||||||||||||||||||||||||
| if len(errs) > 0 { | ||||||||||||||||||||||||
| return nil, fmt.Errorf( | ||||||||||||||||||||||||
| "failed to fetch payment details: %w", errs[0], | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Cyberguru1 marked this conversation as resolved.
Comment on lines
+583
to
+587
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When multiple concurrent operations fail, returning only the first error from a non-deterministically ordered slice can make debugging difficult. It would be more informative to log all the errors that occurred.
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var finalPayments []*lnrpc.Payment | ||||||||||||||||||||||||
| for _, entry := range paymentsFromStore { | ||||||||||||||||||||||||
| if p, ok := payments[entry.Hash]; ok { | ||||||||||||||||||||||||
| finalPayments = append(finalPayments, p) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var firstIndexOffset, lastIndexOffset uint64 | ||||||||||||||||||||||||
| if len(finalPayments) > 0 { | ||||||||||||||||||||||||
| firstIndexOffset = offset | ||||||||||||||||||||||||
| lastIndexOffset = offset + uint64(len(finalPayments)) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+596
to
+600
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pagination offsets If any payments are skipped (e.g., because they are not found in LND), basing the offsets on
Using
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return &litrpc.AccountPaymentsResponse{ | ||||||||||||||||||||||||
| Payments: finalPayments, | ||||||||||||||||||||||||
| FirstIndexOffset: firstIndexOffset, | ||||||||||||||||||||||||
| LastIndexOffset: lastIndexOffset, | ||||||||||||||||||||||||
| TotalNumPayments: totalNumPayments, | ||||||||||||||||||||||||
| }, nil | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // notFoundPayment creates a placeholder payment for a desynced entry. | ||||||||||||||||||||||||
| func notFoundPayment(hash lntypes.Hash) *lnrpc.Payment { | ||||||||||||||||||||||||
| return &lnrpc.Payment{ | ||||||||||||||||||||||||
| PaymentHash: hex.EncodeToString(hash[:]), | ||||||||||||||||||||||||
| Status: lnrpc.Payment_UNKNOWN, | ||||||||||||||||||||||||
| FailureReason: lnrpc. | ||||||||||||||||||||||||
| PaymentFailureReason_FAILURE_REASON_NONE, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |||||||||||||||||||||||||
| "math" | ||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||
| "sort" | ||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| "github.com/btcsuite/btcwallet/walletdb" | ||||||||||||||||||||||||||
|
|
@@ -445,6 +446,60 @@ func (s *BoltStore) DeleteAccountPayment(_ context.Context, id AccountID, | |||||||||||||||||||||||||
| return s.updateAccount(id, update) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ListAccountPayments returns a paginated list of payments | ||||||||||||||||||||||||||
| // associated with the given account, sorted in ascending lexicographical | ||||||||||||||||||||||||||
| // order of their payment hash. | ||||||||||||||||||||||||||
| func (s *BoltStore) ListAccountPayments(ctx context.Context, id AccountID, | ||||||||||||||||||||||||||
| offset, limit int32) ([]*AccountPaymentEntry, error) { | ||||||||||||||||||||||||||
|
Comment on lines
+452
to
+453
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a defensive check to ensure that func (s *BoltStore) ListAccountPayments(ctx context.Context, id AccountID,
offset, limit int32) ([]*AccountPaymentEntry, error) {
if offset < 0 {
return nil, fmt.Errorf("offset cannot be negative")
} |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| account, err := s.Account(ctx, id) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var matchedPayments []*AccountPaymentEntry | ||||||||||||||||||||||||||
| for hash, entry := range account.Payments { | ||||||||||||||||||||||||||
| matchedPayments = append(matchedPayments, &AccountPaymentEntry{ | ||||||||||||||||||||||||||
| Hash: hash, | ||||||||||||||||||||||||||
| PaymentEntry: entry, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Sort target hashes lexicographically to ensure pagination is | ||||||||||||||||||||||||||
| // deterministic across all store backends. | ||||||||||||||||||||||||||
| sort.Slice(matchedPayments, func(i, j int) bool { | ||||||||||||||||||||||||||
| hashI := matchedPayments[i].Hash[:] | ||||||||||||||||||||||||||
| hashJ := matchedPayments[j].Hash[:] | ||||||||||||||||||||||||||
| return bytes.Compare(hashI, hashJ) < 0 | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Apply pagination limits and offsets. | ||||||||||||||||||||||||||
| total := int32(len(matchedPayments)) | ||||||||||||||||||||||||||
| if offset >= total { | ||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+476
to
+480
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defensive programming: Ensure that
Suggested change
References
bitromortac marked this conversation as resolved.
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| end := offset + limit | ||||||||||||||||||||||||||
| if limit < 0 || end > total { | ||||||||||||||||||||||||||
| end = total | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+482
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
Comment on lines
+483
to
+485
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation handles a negative While the RPC layer currently ensures the limit is non-negative, it would be best to make the store implementations consistent. I'd suggest either documenting this behavior in the |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return matchedPayments[offset:end], nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // CountAccountPayments returns the total number of payments associated with | ||||||||||||||||||||||||||
| // the given account. | ||||||||||||||||||||||||||
| func (s *BoltStore) CountAccountPayments(ctx context.Context, | ||||||||||||||||||||||||||
| id AccountID) (uint64, error) { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| account, err := s.Account(ctx, id) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return 0, err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return uint64(len(account.Payments)), nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func (s *BoltStore) updateAccount(id AccountID, | ||||||||||||||||||||||||||
| updateFn func(*OffChainBalanceAccount) error) error { | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.