Merge pull request #1316 from Cyberguru1/account-payments

accounts: add account payments subcommand
This commit is contained in:
Viktor Torstensson 2026-07-27 11:58:01 +02:00 committed by GitHub
commit e631ccf0e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2632 additions and 41 deletions

View file

@ -107,6 +107,12 @@ type AccountInvoices map[lntypes.Hash]struct{}
// AccountPayments is the set of payments that are associated with an account.
type AccountPayments map[lntypes.Hash]*PaymentEntry
// AccountPaymentEntry wraps a payment hash with its entry details.
type AccountPaymentEntry struct {
Hash 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.
@ -279,6 +285,16 @@ type Store interface {
DeleteAccountPayment(_ context.Context, id AccountID,
hash lntypes.Hash) error
// ListAccountPayments returns a paginated list of payments
// associated with the given account, sorted in ascending
// lexicographical order of their payment hash.
ListAccountPayments(ctx context.Context, id AccountID, offset,
limit int32) ([]*AccountPaymentEntry, error)
// CountAccountPayments returns the total number of payments associated
// with the given account.
CountAccountPayments(ctx context.Context, id AccountID) (uint64, error)
// RemoveAccount finds an account by its ID and removes it from the¨
// store.
RemoveAccount(ctx context.Context, id AccountID) error

View file

@ -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
// 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
}
// 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)
)
rawCtx, _, client := s.service.routerClient.RawClientWithMacAuth(ctx)
for _, entry := range paymentsFromStore {
entry := entry
wg.Add(1)
go func() {
defer wg.Done()
// Bounded concurrency using semaphore.
select {
case sem <- struct{}{}:
case <-rawCtx.Done():
return
}
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)
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()
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)
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
}
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],
)
}
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))
}
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,
}
}

View file

@ -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) {
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
}
end := offset + limit
if limit < 0 || end > total {
end = total
}
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 {

View file

@ -46,6 +46,8 @@ type SQLQueries interface {
ListAllAccountPayments(ctx context.Context) ([]sqlc.AccountPayment, error)
ListAccountInvoices(ctx context.Context, id int64) ([]sqlc.AccountInvoice, error)
ListAccountPayments(ctx context.Context, id int64) ([]sqlc.AccountPayment, error)
AccountPaymentsPaginated(ctx context.Context, arg sqlc.AccountPaymentsPaginatedParams) ([]sqlc.AccountPayment, error)
CountAccountPayments(ctx context.Context, accountID int64) (int64, error)
ListAllAccounts(ctx context.Context) ([]sqlc.Account, error)
SetAccountIndex(ctx context.Context, arg sqlc.SetAccountIndexParams) error
UpdateAccountBalance(ctx context.Context, arg sqlc.UpdateAccountBalanceParams) (int64, error)
@ -779,6 +781,80 @@ func (s *SQLStore) DeleteAccountPayment(ctx context.Context, alias AccountID,
}, sqldb.NoOpReset)
}
// ListAccountPayments returns a paginated list of payments
// associated with the given account, sorted in ascending lexicographical
// order of their payment hash.
func (s *SQLStore) ListAccountPayments(ctx context.Context, alias AccountID,
offset, limit int32) ([]*AccountPaymentEntry, error) {
var (
readTxOpts = db.NewQueryReadTx()
payments []*AccountPaymentEntry
)
err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error {
id, err := getAccountIDByAlias(ctx, db, alias)
if err != nil {
return err
}
var dbPayments []sqlc.AccountPayment
dbPayments, err = db.AccountPaymentsPaginated(
ctx, sqlc.AccountPaymentsPaginatedParams{
AccountID: id,
Limit: limit,
Offset: offset,
},
)
if err != nil {
return err
}
payments = make([]*AccountPaymentEntry, len(dbPayments))
for i, p := range dbPayments {
var hash lntypes.Hash
copy(hash[:], p.Hash)
payments[i] = &AccountPaymentEntry{
Hash: hash,
PaymentEntry: &PaymentEntry{
Status: lnrpc.Payment_PaymentStatus(
p.Status,
),
FullAmount: lnwire.MilliSatoshi(
p.FullAmountMsat,
),
},
}
}
return nil
}, sqldb.NoOpReset)
return payments, err
}
// CountAccountPayments returns the total number of payments associated with
// the given account.
func (s *SQLStore) CountAccountPayments(ctx context.Context,
alias AccountID) (uint64, error) {
var (
readTxOpts = db.NewQueryReadTx()
count int64
)
err := s.db.ExecTx(ctx, &readTxOpts, func(db SQLQueries) error {
id, err := getAccountIDByAlias(ctx, db, alias)
if err != nil {
return err
}
count, err = db.CountAccountPayments(ctx, id)
return err
}, sqldb.NoOpReset)
return uint64(count), err
}
// LastIndexes returns the last invoice add and settle index or
// ErrNoInvoiceIndexKnown if no indexes are known yet.
//

View file

@ -906,3 +906,77 @@ func TestCheckLabel(t *testing.T) {
})
}
}
// TestListAccountPayments tests listing and counting payment entries associated
// with a given account.
func TestListAccountPayments(t *testing.T) {
t.Parallel()
ctx := context.Background()
store := NewTestDB(t, clock.NewTestClock(time.Now()))
// Listing payments for non-existent account should fail.
_, err := store.ListAccountPayments(
ctx, AccountID{}, 0, 0,
)
require.ErrorIs(t, err, ErrAccNotFound)
acct, err := store.NewAccount(
ctx, 10000, time.Time{}, "payment-list",
)
require.NoError(t, err)
// Initially, there should be no payments.
payments, err := store.ListAccountPayments(
ctx, acct.ID, 0, 0,
)
require.NoError(t, err)
require.Empty(t, payments)
count, err := store.CountAccountPayments(ctx, acct.ID)
require.NoError(t, err)
require.Zero(t, count)
// Add 3 payments.
hash1 := lntypes.Hash{1}
hash2 := lntypes.Hash{2}
hash3 := lntypes.Hash{3}
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash1, 100, lnrpc.Payment_IN_FLIGHT,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash2, 200, lnrpc.Payment_SUCCEEDED,
)
require.NoError(t, err)
_, err = store.UpsertAccountPayment(
ctx, acct.ID, hash3, 300, lnrpc.Payment_FAILED,
)
require.NoError(t, err)
// Test counting all payments.
count, err = store.CountAccountPayments(ctx, acct.ID)
require.NoError(t, err)
require.EqualValues(t, 3, count)
// List all payments in default order (ascending by hash).
payments, err = store.ListAccountPayments(
ctx, acct.ID, 0, 3,
)
require.NoError(t, err)
require.Len(t, payments, 3)
require.Equal(t, hash1, payments[0].Hash)
require.Equal(t, hash2, payments[1].Hash)
require.Equal(t, hash3, payments[2].Hash)
// Test offset and limit.
payments, err = store.ListAccountPayments(
ctx, acct.ID, 1, 1,
)
require.NoError(t, err)
require.Len(t, payments, 1)
require.Equal(t, hash2, payments[0].Hash)
}

View file

@ -128,6 +128,10 @@ const sanitize = async () => {
'import "lnrpc/lightning.proto"',
'import "lnd.proto"',
);
content = content.replace(
'import "proto/lnd.proto";',
'import "lnd.proto";',
);
await fs.writeFile(path, content);
}
};

View file

@ -2,6 +2,7 @@
// file: lit-accounts.proto
import * as jspb from "google-protobuf";
import * as lnd_pb from "./lnd_pb";
export class CreateAccountRequest extends jspb.Message {
getAccountBalance(): string;
@ -432,3 +433,71 @@ export namespace AccountIdentifier {
}
}
export class AccountPaymentsRequest extends jspb.Message {
hasAccount(): boolean;
clearAccount(): void;
getAccount(): AccountIdentifier | undefined;
setAccount(value?: AccountIdentifier): void;
getMaxPayments(): string;
setMaxPayments(value: string): void;
getIndexOffset(): string;
setIndexOffset(value: string): void;
getCountTotalPayments(): boolean;
setCountTotalPayments(value: boolean): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): AccountPaymentsRequest.AsObject;
static toObject(includeInstance: boolean, msg: AccountPaymentsRequest): AccountPaymentsRequest.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: AccountPaymentsRequest, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): AccountPaymentsRequest;
static deserializeBinaryFromReader(message: AccountPaymentsRequest, reader: jspb.BinaryReader): AccountPaymentsRequest;
}
export namespace AccountPaymentsRequest {
export type AsObject = {
account?: AccountIdentifier.AsObject,
maxPayments: string,
indexOffset: string,
countTotalPayments: boolean,
}
}
export class AccountPaymentsResponse extends jspb.Message {
clearPaymentsList(): void;
getPaymentsList(): Array<lnd_pb.Payment>;
setPaymentsList(value: Array<lnd_pb.Payment>): void;
addPayments(value?: lnd_pb.Payment, index?: number): lnd_pb.Payment;
getFirstIndexOffset(): string;
setFirstIndexOffset(value: string): void;
getLastIndexOffset(): string;
setLastIndexOffset(value: string): void;
getTotalNumPayments(): string;
setTotalNumPayments(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): AccountPaymentsResponse.AsObject;
static toObject(includeInstance: boolean, msg: AccountPaymentsResponse): AccountPaymentsResponse.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: AccountPaymentsResponse, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): AccountPaymentsResponse;
static deserializeBinaryFromReader(message: AccountPaymentsResponse, reader: jspb.BinaryReader): AccountPaymentsResponse;
}
export namespace AccountPaymentsResponse {
export type AsObject = {
paymentsList: Array<lnd_pb.Payment.AsObject>,
firstIndexOffset: string,
lastIndexOffset: string,
totalNumPayments: string,
}
}

View file

@ -24,12 +24,16 @@ var global =
(function () { return this; }).call(null) ||
Function('return this')();
var lnd_pb = require('./lnd_pb.js');
goog.object.extend(proto, lnd_pb);
goog.exportSymbol('proto.litrpc.Account', null, global);
goog.exportSymbol('proto.litrpc.AccountIdentifier', null, global);
goog.exportSymbol('proto.litrpc.AccountIdentifier.IdentifierCase', 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.AccountPaymentsRequest', null, global);
goog.exportSymbol('proto.litrpc.AccountPaymentsResponse', null, global);
goog.exportSymbol('proto.litrpc.CreateAccountRequest', null, global);
goog.exportSymbol('proto.litrpc.CreateAccountResponse', null, global);
goog.exportSymbol('proto.litrpc.CreditAccountRequest', null, global);
@ -377,6 +381,48 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.litrpc.AccountIdentifier.displayName = 'proto.litrpc.AccountIdentifier';
}
/**
* 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.AccountPaymentsRequest = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
};
goog.inherits(proto.litrpc.AccountPaymentsRequest, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.litrpc.AccountPaymentsRequest.displayName = 'proto.litrpc.AccountPaymentsRequest';
}
/**
* 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.AccountPaymentsResponse = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, proto.litrpc.AccountPaymentsResponse.repeatedFields_, null);
};
goog.inherits(proto.litrpc.AccountPaymentsResponse, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.litrpc.AccountPaymentsResponse.displayName = 'proto.litrpc.AccountPaymentsResponse';
}
@ -3351,4 +3397,495 @@ proto.litrpc.AccountIdentifier.prototype.hasLabel = function() {
};
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.litrpc.AccountPaymentsRequest.prototype.toObject = function(opt_includeInstance) {
return proto.litrpc.AccountPaymentsRequest.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.litrpc.AccountPaymentsRequest} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.litrpc.AccountPaymentsRequest.toObject = function(includeInstance, msg) {
var f, obj = {
account: (f = msg.getAccount()) && proto.litrpc.AccountIdentifier.toObject(includeInstance, f),
maxPayments: jspb.Message.getFieldWithDefault(msg, 2, "0"),
indexOffset: jspb.Message.getFieldWithDefault(msg, 3, "0"),
countTotalPayments: jspb.Message.getBooleanFieldWithDefault(msg, 4, false)
};
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.AccountPaymentsRequest}
*/
proto.litrpc.AccountPaymentsRequest.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.litrpc.AccountPaymentsRequest;
return proto.litrpc.AccountPaymentsRequest.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.litrpc.AccountPaymentsRequest} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.litrpc.AccountPaymentsRequest}
*/
proto.litrpc.AccountPaymentsRequest.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = new proto.litrpc.AccountIdentifier;
reader.readMessage(value,proto.litrpc.AccountIdentifier.deserializeBinaryFromReader);
msg.setAccount(value);
break;
case 2:
var value = /** @type {string} */ (reader.readUint64String());
msg.setMaxPayments(value);
break;
case 3:
var value = /** @type {string} */ (reader.readUint64String());
msg.setIndexOffset(value);
break;
case 4:
var value = /** @type {boolean} */ (reader.readBool());
msg.setCountTotalPayments(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.litrpc.AccountPaymentsRequest.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.litrpc.AccountPaymentsRequest.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.AccountPaymentsRequest} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.litrpc.AccountPaymentsRequest.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getAccount();
if (f != null) {
writer.writeMessage(
1,
f,
proto.litrpc.AccountIdentifier.serializeBinaryToWriter
);
}
f = message.getMaxPayments();
if (parseInt(f, 10) !== 0) {
writer.writeUint64String(
2,
f
);
}
f = message.getIndexOffset();
if (parseInt(f, 10) !== 0) {
writer.writeUint64String(
3,
f
);
}
f = message.getCountTotalPayments();
if (f) {
writer.writeBool(
4,
f
);
}
};
/**
* optional AccountIdentifier account = 1;
* @return {?proto.litrpc.AccountIdentifier}
*/
proto.litrpc.AccountPaymentsRequest.prototype.getAccount = function() {
return /** @type{?proto.litrpc.AccountIdentifier} */ (
jspb.Message.getWrapperField(this, proto.litrpc.AccountIdentifier, 1));
};
/**
* @param {?proto.litrpc.AccountIdentifier|undefined} value
* @return {!proto.litrpc.AccountPaymentsRequest} returns this
*/
proto.litrpc.AccountPaymentsRequest.prototype.setAccount = function(value) {
return jspb.Message.setWrapperField(this, 1, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.litrpc.AccountPaymentsRequest} returns this
*/
proto.litrpc.AccountPaymentsRequest.prototype.clearAccount = function() {
return this.setAccount(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.litrpc.AccountPaymentsRequest.prototype.hasAccount = function() {
return jspb.Message.getField(this, 1) != null;
};
/**
* optional uint64 max_payments = 2;
* @return {string}
*/
proto.litrpc.AccountPaymentsRequest.prototype.getMaxPayments = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0"));
};
/**
* @param {string} value
* @return {!proto.litrpc.AccountPaymentsRequest} returns this
*/
proto.litrpc.AccountPaymentsRequest.prototype.setMaxPayments = function(value) {
return jspb.Message.setProto3StringIntField(this, 2, value);
};
/**
* optional uint64 index_offset = 3;
* @return {string}
*/
proto.litrpc.AccountPaymentsRequest.prototype.getIndexOffset = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0"));
};
/**
* @param {string} value
* @return {!proto.litrpc.AccountPaymentsRequest} returns this
*/
proto.litrpc.AccountPaymentsRequest.prototype.setIndexOffset = function(value) {
return jspb.Message.setProto3StringIntField(this, 3, value);
};
/**
* optional bool count_total_payments = 4;
* @return {boolean}
*/
proto.litrpc.AccountPaymentsRequest.prototype.getCountTotalPayments = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false));
};
/**
* @param {boolean} value
* @return {!proto.litrpc.AccountPaymentsRequest} returns this
*/
proto.litrpc.AccountPaymentsRequest.prototype.setCountTotalPayments = function(value) {
return jspb.Message.setProto3BooleanField(this, 4, value);
};
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.litrpc.AccountPaymentsResponse.repeatedFields_ = [1];
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.litrpc.AccountPaymentsResponse.prototype.toObject = function(opt_includeInstance) {
return proto.litrpc.AccountPaymentsResponse.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.litrpc.AccountPaymentsResponse} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.litrpc.AccountPaymentsResponse.toObject = function(includeInstance, msg) {
var f, obj = {
paymentsList: jspb.Message.toObjectList(msg.getPaymentsList(),
lnd_pb.Payment.toObject, includeInstance),
firstIndexOffset: jspb.Message.getFieldWithDefault(msg, 2, "0"),
lastIndexOffset: jspb.Message.getFieldWithDefault(msg, 3, "0"),
totalNumPayments: jspb.Message.getFieldWithDefault(msg, 4, "0")
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.litrpc.AccountPaymentsResponse}
*/
proto.litrpc.AccountPaymentsResponse.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.litrpc.AccountPaymentsResponse;
return proto.litrpc.AccountPaymentsResponse.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.litrpc.AccountPaymentsResponse} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.litrpc.AccountPaymentsResponse}
*/
proto.litrpc.AccountPaymentsResponse.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = new lnd_pb.Payment;
reader.readMessage(value,lnd_pb.Payment.deserializeBinaryFromReader);
msg.addPayments(value);
break;
case 2:
var value = /** @type {string} */ (reader.readUint64String());
msg.setFirstIndexOffset(value);
break;
case 3:
var value = /** @type {string} */ (reader.readUint64String());
msg.setLastIndexOffset(value);
break;
case 4:
var value = /** @type {string} */ (reader.readUint64String());
msg.setTotalNumPayments(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.litrpc.AccountPaymentsResponse.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.litrpc.AccountPaymentsResponse.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.AccountPaymentsResponse} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.litrpc.AccountPaymentsResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getPaymentsList();
if (f.length > 0) {
writer.writeRepeatedMessage(
1,
f,
lnd_pb.Payment.serializeBinaryToWriter
);
}
f = message.getFirstIndexOffset();
if (parseInt(f, 10) !== 0) {
writer.writeUint64String(
2,
f
);
}
f = message.getLastIndexOffset();
if (parseInt(f, 10) !== 0) {
writer.writeUint64String(
3,
f
);
}
f = message.getTotalNumPayments();
if (parseInt(f, 10) !== 0) {
writer.writeUint64String(
4,
f
);
}
};
/**
* repeated lnrpc.Payment payments = 1;
* @return {!Array<!proto.lnrpc.Payment>}
*/
proto.litrpc.AccountPaymentsResponse.prototype.getPaymentsList = function() {
return /** @type{!Array<!proto.lnrpc.Payment>} */ (
jspb.Message.getRepeatedWrapperField(this, lnd_pb.Payment, 1));
};
/**
* @param {!Array<!proto.lnrpc.Payment>} value
* @return {!proto.litrpc.AccountPaymentsResponse} returns this
*/
proto.litrpc.AccountPaymentsResponse.prototype.setPaymentsList = function(value) {
return jspb.Message.setRepeatedWrapperField(this, 1, value);
};
/**
* @param {!proto.lnrpc.Payment=} opt_value
* @param {number=} opt_index
* @return {!proto.lnrpc.Payment}
*/
proto.litrpc.AccountPaymentsResponse.prototype.addPayments = function(opt_value, opt_index) {
return jspb.Message.addToRepeatedWrapperField(this, 1, opt_value, proto.lnrpc.Payment, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.litrpc.AccountPaymentsResponse} returns this
*/
proto.litrpc.AccountPaymentsResponse.prototype.clearPaymentsList = function() {
return this.setPaymentsList([]);
};
/**
* optional uint64 first_index_offset = 2;
* @return {string}
*/
proto.litrpc.AccountPaymentsResponse.prototype.getFirstIndexOffset = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "0"));
};
/**
* @param {string} value
* @return {!proto.litrpc.AccountPaymentsResponse} returns this
*/
proto.litrpc.AccountPaymentsResponse.prototype.setFirstIndexOffset = function(value) {
return jspb.Message.setProto3StringIntField(this, 2, value);
};
/**
* optional uint64 last_index_offset = 3;
* @return {string}
*/
proto.litrpc.AccountPaymentsResponse.prototype.getLastIndexOffset = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "0"));
};
/**
* @param {string} value
* @return {!proto.litrpc.AccountPaymentsResponse} returns this
*/
proto.litrpc.AccountPaymentsResponse.prototype.setLastIndexOffset = function(value) {
return jspb.Message.setProto3StringIntField(this, 3, value);
};
/**
* optional uint64 total_num_payments = 4;
* @return {string}
*/
proto.litrpc.AccountPaymentsResponse.prototype.getTotalNumPayments = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "0"));
};
/**
* @param {string} value
* @return {!proto.litrpc.AccountPaymentsResponse} returns this
*/
proto.litrpc.AccountPaymentsResponse.prototype.setTotalNumPayments = function(value) {
return jspb.Message.setProto3StringIntField(this, 4, value);
};
goog.object.extend(exports, proto.litrpc);

View file

@ -67,6 +67,15 @@ type AccountsRemoveAccount = {
readonly responseType: typeof lit_accounts_pb.RemoveAccountResponse;
};
type AccountsAccountPayments = {
readonly methodName: string;
readonly service: typeof Accounts;
readonly requestStream: false;
readonly responseStream: false;
readonly requestType: typeof lit_accounts_pb.AccountPaymentsRequest;
readonly responseType: typeof lit_accounts_pb.AccountPaymentsResponse;
};
export class Accounts {
static readonly serviceName: string;
static readonly CreateAccount: AccountsCreateAccount;
@ -76,6 +85,7 @@ export class Accounts {
static readonly ListAccounts: AccountsListAccounts;
static readonly AccountInfo: AccountsAccountInfo;
static readonly RemoveAccount: AccountsRemoveAccount;
static readonly AccountPayments: AccountsAccountPayments;
}
export type ServiceError = { message: string, code: number; metadata: grpc.Metadata }
@ -173,5 +183,14 @@ export class AccountsClient {
requestMessage: lit_accounts_pb.RemoveAccountRequest,
callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.RemoveAccountResponse|null) => void
): UnaryResponse;
accountPayments(
requestMessage: lit_accounts_pb.AccountPaymentsRequest,
metadata: grpc.Metadata,
callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.AccountPaymentsResponse|null) => void
): UnaryResponse;
accountPayments(
requestMessage: lit_accounts_pb.AccountPaymentsRequest,
callback: (error: ServiceError|null, responseMessage: lit_accounts_pb.AccountPaymentsResponse|null) => void
): UnaryResponse;
}

View file

@ -73,6 +73,15 @@ Accounts.RemoveAccount = {
responseType: lit_accounts_pb.RemoveAccountResponse
};
Accounts.AccountPayments = {
methodName: "AccountPayments",
service: Accounts,
requestStream: false,
responseStream: false,
requestType: lit_accounts_pb.AccountPaymentsRequest,
responseType: lit_accounts_pb.AccountPaymentsResponse
};
exports.Accounts = Accounts;
function AccountsClient(serviceHost, options) {
@ -297,5 +306,36 @@ AccountsClient.prototype.removeAccount = function removeAccount(requestMessage,
};
};
AccountsClient.prototype.accountPayments = function accountPayments(requestMessage, metadata, callback) {
if (arguments.length === 2) {
callback = arguments[1];
}
var client = grpc.unary(Accounts.AccountPayments, {
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();
}
};
};
exports.AccountsClient = AccountsClient;

View file

@ -30,6 +30,7 @@ var accountsCommands = []cli.Command{
listAccountsCommand,
accountInfoCommand,
removeAccountCommand,
accountPaymentsCommand,
},
Description: "Manage accounts.",
},
@ -541,3 +542,84 @@ func parseIDOrLabel(ctx *cli.Context) (string, string, cli.Args, error) {
return accountID, label, args, nil
}
var accountPaymentsCommand = cli.Command{
Name: "payments",
ShortName: "p",
Usage: "Show detailed payment history for a single " +
"off-chain account.",
ArgsUsage: "[id | label]",
Description: "Returns the detailed payment history for an " +
"account by fetching their stored hashes and querying " +
"LND. The results are returned paginated and " +
"sorted in ascending lexicographical order of their " +
"payment hash.",
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.",
},
cli.Uint64Flag{
Name: "max_payments",
Usage: fmt.Sprintf("The maximum number of payments to "+
"return. The default value is %d and "+
"the maximum is %d.",
accounts.DefaultMaxPayments,
accounts.MaxPaymentsLimit),
Value: accounts.DefaultMaxPayments,
},
cli.Uint64Flag{
Name: "index_offset",
Usage: "The row offset into the list of payments " +
"that will be used as the start of the " +
"query.",
},
cli.BoolFlag{
Name: "count_total_payments",
Usage: "If true, the total number of payments " +
"matching the query will be returned.",
},
},
Action: accountPayments,
}
func accountPayments(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
account, _, err := parseAccountIdentifier(cli)
if err != nil {
return err
}
maxPayments := cli.Uint64("max_payments")
if maxPayments > accounts.MaxPaymentsLimit {
return fmt.Errorf(
"max_payments cannot exceed %d",
accounts.MaxPaymentsLimit,
)
}
req := &litrpc.AccountPaymentsRequest{
Account: account,
MaxPayments: maxPayments,
IndexOffset: cli.Uint64("index_offset"),
CountTotalPayments: cli.Bool("count_total_payments"),
}
resp, err := client.AccountPayments(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}

View file

@ -11,6 +11,48 @@ import (
"time"
)
const accountPaymentsPaginated = `-- name: AccountPaymentsPaginated :many
SELECT account_id, hash, status, full_amount_msat
FROM account_payments
WHERE account_id = $1
ORDER BY hash ASC
LIMIT $2 OFFSET $3
`
type AccountPaymentsPaginatedParams struct {
AccountID int64
Limit int32
Offset int32
}
func (q *Queries) AccountPaymentsPaginated(ctx context.Context, arg AccountPaymentsPaginatedParams) ([]AccountPayment, error) {
rows, err := q.db.QueryContext(ctx, accountPaymentsPaginated, arg.AccountID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AccountPayment
for rows.Next() {
var i AccountPayment
if err := rows.Scan(
&i.AccountID,
&i.Hash,
&i.Status,
&i.FullAmountMsat,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const addAccountInvoice = `-- name: AddAccountInvoice :exec
INSERT INTO account_invoices (account_id, hash)
VALUES ($1, $2)
@ -26,6 +68,19 @@ func (q *Queries) AddAccountInvoice(ctx context.Context, arg AddAccountInvoicePa
return err
}
const countAccountPayments = `-- name: CountAccountPayments :one
SELECT COUNT(*)
FROM account_payments
WHERE account_id = $1
`
func (q *Queries) CountAccountPayments(ctx context.Context, accountID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countAccountPayments, accountID)
var count int64
err := row.Scan(&count)
return count, err
}
const deleteAccount = `-- name: DeleteAccount :exec
DELETE FROM accounts
WHERE id = $1

View file

@ -10,7 +10,9 @@ import (
)
type Querier interface {
AccountPaymentsPaginated(ctx context.Context, arg AccountPaymentsPaginatedParams) ([]AccountPayment, error)
AddAccountInvoice(ctx context.Context, arg AddAccountInvoiceParams) error
CountAccountPayments(ctx context.Context, accountID int64) (int64, error)
DeleteAccount(ctx context.Context, id int64) error
DeleteAccountPayment(ctx context.Context, arg DeleteAccountPaymentParams) error
DeleteAllTempKVStores(ctx context.Context) error

View file

@ -80,6 +80,18 @@ WHERE account_id = $1;
SELECT *
FROM account_payments;
-- name: AccountPaymentsPaginated :many
SELECT *
FROM account_payments
WHERE account_id = $1
ORDER BY hash ASC
LIMIT $2 OFFSET $3;
-- name: CountAccountPayments :one
SELECT COUNT(*)
FROM account_payments
WHERE account_id = $1;
-- name: ListAccountInvoices :many
SELECT *
FROM account_invoices

View file

@ -33,6 +33,12 @@
### Functional Changes/Additions
* [Add accounts payments history subcommand](https://github.com/lightninglabs/lightning-terminal/pull/1316):
Added the `litcli accounts payments` subcommand and corresponding gRPC
endpoint `AccountPayments` to retrieve the off-chain payment history of
an account, supporting pagination (sorted in ascending lexicographical
order of their payment hash) and counting of total payments.
### Technical and Architectural Updates
* [Report litd's own version for `litd
@ -57,4 +63,5 @@
* 0xfandom
* bitromortac
* Cyberguru1
* Vandit Singh

View file

@ -2,8 +2,11 @@ package itest
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"sort"
"strings"
"testing"
"time"
@ -14,8 +17,10 @@ import (
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
@ -164,6 +169,225 @@ func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort,
ctxa, t, rawConn, charlie, acctBalance,
)
// Initiate a HODL payment (which remains IN_FLIGHT) and a FAILED
// payment to verify that the AccountPayments RPC correctly retrieves
// and reports payments across all potential lifecycles (SUCCEEDED,
// IN_FLIGHT, FAILED).
testCtx, testCancel := context.WithTimeout(ctxb, defaultTimeout)
defer testCancel()
routerClient := routerrpc.NewRouterClient(rawConn)
// 1. Initiate HODL payment (IN_FLIGHT)
var preimage lntypes.Preimage
_, err = rand.Read(preimage[:])
require.NoError(t.t, err)
holdHash := preimage.Hash()
holdInv, err := charlie.AddHoldInvoice(
testCtx, &invoicesrpc.AddHoldInvoiceRequest{
Hash: holdHash[:],
Value: 2222,
},
)
require.NoError(t.t, err)
sendReqHold := &routerrpc.SendPaymentRequest{
PaymentRequest: holdInv.PaymentRequest,
TimeoutSeconds: 2,
FeeLimitMsat: 1000,
}
holdStream, err := routerClient.SendPaymentV2(ctxa, sendReqHold)
require.NoError(t.t, err)
holdPayment, err := getPaymentResult(holdStream, true)
require.NoError(t.t, err)
require.Equal(t.t, lnrpc.Payment_IN_FLIGHT, holdPayment.Status)
// Wait for the hold invoice to be accepted by Charlie.
require.Eventually(t.t, func() bool {
inv, err := charlie.LookupInvoice(
testCtx, &lnrpc.PaymentHash{
RHash: holdHash[:],
},
)
return err == nil &&
inv.State == lnrpc.Invoice_ACCEPTED
}, defaultTimeout, 100*time.Millisecond)
// 2. Initiate a FAILED payment
var failedHash lntypes.Hash
_, err = rand.Read(failedHash[:])
require.NoError(t.t, err)
var fakePubKey [33]byte
fakePubKey[0] = 0x02
sendReqFailed := &routerrpc.SendPaymentRequest{
Dest: fakePubKey[:],
Amt: 1111,
PaymentHash: failedHash[:],
TimeoutSeconds: 2,
FeeLimitMsat: 1000,
}
failedStream, err := routerClient.SendPaymentV2(ctxa, sendReqFailed)
require.NoError(t.t, err)
failedPayment, err := getPaymentResult(failedStream, false)
require.NoError(t.t, err)
require.Equal(t.t, lnrpc.Payment_FAILED, failedPayment.Status)
// Test AccountPayments RPC with all 3 payments (succeeded,
// in-flight, failed).
paymentsResp, err := acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
CountTotalPayments: true,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Sort the payments by value descending to ensure index-based
// assertions are deterministic regardless of database sorting by hash.
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
valI := paymentsResp.Payments[i].ValueSat
valJ := paymentsResp.Payments[j].ValueSat
return valI > valJ
})
// Check the succeeded payment. This corresponds to the 4444 sat
// payment initiated earlier in testAccountRestrictions using this
// account.
require.Equal(
t.t, uint64(4444),
uint64(paymentsResp.Payments[0].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[0].Status,
)
// Check In-flight payment
require.Equal(
t.t, uint64(2222),
uint64(paymentsResp.Payments[1].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_IN_FLIGHT,
paymentsResp.Payments[1].Status,
)
// Check Failed payment
require.Equal(
t.t, uint64(1111),
uint64(paymentsResp.Payments[2].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_FAILED,
paymentsResp.Payments[2].Status,
)
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Query by label.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Label{
Label: acctLabel,
},
},
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Query with pagination limit.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
MaxPayments: 2,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 2)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 2, paymentsResp.LastIndexOffset)
// Query with pagination offset out of bounds.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 3,
},
)
require.NoError(t.t, err)
require.Empty(t.t, paymentsResp.Payments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 0, paymentsResp.LastIndexOffset)
// Query with pagination offset inside bounds, where the number of
// payments returned is fewer than MaxPayments.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 2,
MaxPayments: 2,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 1)
require.EqualValues(t.t, 2, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Query with invalid pagination max_payments (exceeding 50).
_, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
MaxPayments: 51,
},
)
require.Error(t.t, err)
require.Contains(t.t, err.Error(), "max_payments cannot exceed 50")
// Query with invalid pagination index_offset (exceeding 31-bit int).
_, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
IndexOffset: 0x80000000,
},
)
require.Error(t.t, err)
require.Contains(t.t, err.Error(), "index_offset out of range")
// Test the same account restrictions with an LNC session that is bound
// to the account.
testAccountRestrictionsLNC(
@ -176,6 +400,84 @@ func runAccountSystemTest(t *harnessTest, node *HarnessNode, hostPort,
ctxa, t, rawConn, charlie, newAcctBalance,
)
// Settle the HODL invoice to clean up node state.
_, err = charlie.SettleInvoice(testCtx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
require.NoError(t.t, err)
// Delete a single failed payment from LND to simulate a desync case
// where this payment exists in LiT's account database but is
// deleted/absent in LND.
_, err = node.LightningClient.DeletePayment(
testCtx, &lnrpc.DeletePaymentRequest{
PaymentHash: failedHash[:],
},
)
require.NoError(t.t, err)
// Now call AccountPayments. It should find all 3 payments in the local
// store, returning a placeholder entry with status Payment_UNKNOWN for
// the deleted payment that was not found in LND.
paymentsResp, err = acctClient.AccountPayments(
ctxm, &litrpc.AccountPaymentsRequest{
Account: &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: acctResp.Account.Id,
},
},
CountTotalPayments: true,
},
)
require.NoError(t.t, err)
require.Len(t.t, paymentsResp.Payments, 3)
// Sort the payments by value descending to ensure index-based
// assertions are deterministic regardless of database sorting by hash.
sort.Slice(paymentsResp.Payments, func(i, j int) bool {
valI := paymentsResp.Payments[i].ValueSat
valJ := paymentsResp.Payments[j].ValueSat
return valI > valJ
})
// Succeeded payment
require.Equal(
t.t, uint64(4444),
uint64(paymentsResp.Payments[0].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[0].Status,
)
// Settled hold payment
require.Equal(
t.t, uint64(2222),
uint64(paymentsResp.Payments[1].ValueSat),
)
require.Equal(
t.t, lnrpc.Payment_SUCCEEDED,
paymentsResp.Payments[1].Status,
)
// Placeholder payment for the desynced/deleted payment
require.Equal(
t.t, hex.EncodeToString(failedHash[:]),
paymentsResp.Payments[2].PaymentHash,
)
require.Equal(
t.t, lnrpc.Payment_UNKNOWN,
paymentsResp.Payments[2].Status,
)
require.Equal(
t.t, lnrpc.PaymentFailureReason_FAILURE_REASON_NONE,
paymentsResp.Payments[2].FailureReason,
)
require.EqualValues(t.t, 3, paymentsResp.TotalNumPayments)
require.EqualValues(t.t, 0, paymentsResp.FirstIndexOffset)
require.EqualValues(t.t, 3, paymentsResp.LastIndexOffset)
// Clean up our channel and payments, so we can start the next test
// iteration with a clean slate.
closeChannelAndAssert(t, net, node, channelOp, false)
@ -246,7 +548,7 @@ func testAccountRestrictionsLNC(ctxm context.Context, t *harnessTest,
// There should be invoices and payments from the previous test over RPC
// directly.
assertNumInvoices(ctxt, t.t, lightningClient, 1)
assertNumPayments(ctxt, t.t, lightningClient, 1)
assertNumPayments(ctxt, t.t, lightningClient, 3)
}
// testAccountRestrictions tests the different scenarios in which the account

View file

@ -195,4 +195,29 @@ func RegisterAccountsJSONCallbacks(registry map[string]func(ctx context.Context,
}
callback(string(respBytes), nil)
}
registry["litrpc.Accounts.AccountPayments"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &AccountPaymentsRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewAccountsClient(conn)
resp, err := client.AccountPayments(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
}

View file

@ -7,6 +7,7 @@
package litrpc
import (
lnrpc "github.com/lightningnetwork/lnd/lnrpc"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
@ -960,11 +961,162 @@ func (*AccountIdentifier_Id) isAccountIdentifier_Identifier() {}
func (*AccountIdentifier_Label) isAccountIdentifier_Identifier() {}
type AccountPaymentsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The identifier of the account to query payments for.
Account *AccountIdentifier `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"`
// The maximum number of payments to return. If set to 0, it will default
// to 20. Capped at 50.
MaxPayments uint64 `protobuf:"varint,2,opt,name=max_payments,json=maxPayments,proto3" json:"max_payments,omitempty"`
// The row offset into the list of payments that will be used as the start of
// the query. The payments are returned in ascending lexicographical order of
// their payment hash.
IndexOffset uint64 `protobuf:"varint,3,opt,name=index_offset,json=indexOffset,proto3" json:"index_offset,omitempty"`
// If set, the total number of payments matching the query will be returned
// in the response.
CountTotalPayments bool `protobuf:"varint,4,opt,name=count_total_payments,json=countTotalPayments,proto3" json:"count_total_payments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AccountPaymentsRequest) Reset() {
*x = AccountPaymentsRequest{}
mi := &file_lit_accounts_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AccountPaymentsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AccountPaymentsRequest) ProtoMessage() {}
func (x *AccountPaymentsRequest) ProtoReflect() protoreflect.Message {
mi := &file_lit_accounts_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AccountPaymentsRequest.ProtoReflect.Descriptor instead.
func (*AccountPaymentsRequest) Descriptor() ([]byte, []int) {
return file_lit_accounts_proto_rawDescGZIP(), []int{16}
}
func (x *AccountPaymentsRequest) GetAccount() *AccountIdentifier {
if x != nil {
return x.Account
}
return nil
}
func (x *AccountPaymentsRequest) GetMaxPayments() uint64 {
if x != nil {
return x.MaxPayments
}
return 0
}
func (x *AccountPaymentsRequest) GetIndexOffset() uint64 {
if x != nil {
return x.IndexOffset
}
return 0
}
func (x *AccountPaymentsRequest) GetCountTotalPayments() bool {
if x != nil {
return x.CountTotalPayments
}
return false
}
type AccountPaymentsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The detailed payments associated with the account, sorted in ascending
// lexicographical order of their payment hash.
Payments []*lnrpc.Payment `protobuf:"bytes,1,rep,name=payments,proto3" json:"payments,omitempty"`
// The row offset of the first payment returned.
FirstIndexOffset uint64 `protobuf:"varint,2,opt,name=first_index_offset,json=firstIndexOffset,proto3" json:"first_index_offset,omitempty"`
// The row offset of the last payment returned. This can be used as the
// index_offset in a subsequent query to paginate forwards.
LastIndexOffset uint64 `protobuf:"varint,3,opt,name=last_index_offset,json=lastIndexOffset,proto3" json:"last_index_offset,omitempty"`
// The total number of payments matching the query (only set if
// count_total_payments was true in the request).
TotalNumPayments uint64 `protobuf:"varint,4,opt,name=total_num_payments,json=totalNumPayments,proto3" json:"total_num_payments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AccountPaymentsResponse) Reset() {
*x = AccountPaymentsResponse{}
mi := &file_lit_accounts_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AccountPaymentsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AccountPaymentsResponse) ProtoMessage() {}
func (x *AccountPaymentsResponse) ProtoReflect() protoreflect.Message {
mi := &file_lit_accounts_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AccountPaymentsResponse.ProtoReflect.Descriptor instead.
func (*AccountPaymentsResponse) Descriptor() ([]byte, []int) {
return file_lit_accounts_proto_rawDescGZIP(), []int{17}
}
func (x *AccountPaymentsResponse) GetPayments() []*lnrpc.Payment {
if x != nil {
return x.Payments
}
return nil
}
func (x *AccountPaymentsResponse) GetFirstIndexOffset() uint64 {
if x != nil {
return x.FirstIndexOffset
}
return 0
}
func (x *AccountPaymentsResponse) GetLastIndexOffset() uint64 {
if x != nil {
return x.LastIndexOffset
}
return 0
}
func (x *AccountPaymentsResponse) GetTotalNumPayments() uint64 {
if x != nil {
return x.TotalNumPayments
}
return 0
}
var File_lit_accounts_proto protoreflect.FileDescriptor
const file_lit_accounts_proto_rawDesc = "" +
"\n" +
"\x12lit-accounts.proto\x12\x06litrpc\"~\n" +
"\x12lit-accounts.proto\x12\x06litrpc\x1a\x0fproto/lnd.proto\"~\n" +
"\x14CreateAccountRequest\x12'\n" +
"\x0faccount_balance\x18\x01 \x01(\x04R\x0eaccountBalance\x12'\n" +
"\x0fexpiration_date\x18\x02 \x01(\x03R\x0eexpirationDate\x12\x14\n" +
@ -1019,7 +1171,17 @@ const file_lit_accounts_proto_rawDesc = "" +
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x12\x16\n" +
"\x05label\x18\x02 \x01(\tH\x00R\x05labelB\f\n" +
"\n" +
"identifier2\x86\x04\n" +
"identifier\"\xc5\x01\n" +
"\x16AccountPaymentsRequest\x123\n" +
"\aaccount\x18\x01 \x01(\v2\x19.litrpc.AccountIdentifierR\aaccount\x12!\n" +
"\fmax_payments\x18\x02 \x01(\x04R\vmaxPayments\x12!\n" +
"\findex_offset\x18\x03 \x01(\x04R\vindexOffset\x120\n" +
"\x14count_total_payments\x18\x04 \x01(\bR\x12countTotalPayments\"\xcd\x01\n" +
"\x17AccountPaymentsResponse\x12*\n" +
"\bpayments\x18\x01 \x03(\v2\x0e.lnrpc.PaymentR\bpayments\x12,\n" +
"\x12first_index_offset\x18\x02 \x01(\x04R\x10firstIndexOffset\x12*\n" +
"\x11last_index_offset\x18\x03 \x01(\x04R\x0flastIndexOffset\x12,\n" +
"\x12total_num_payments\x18\x04 \x01(\x04R\x10totalNumPayments2\xda\x04\n" +
"\bAccounts\x12L\n" +
"\rCreateAccount\x12\x1c.litrpc.CreateAccountRequest\x1a\x1d.litrpc.CreateAccountResponse\x12>\n" +
"\rUpdateAccount\x12\x1c.litrpc.UpdateAccountRequest\x1a\x0f.litrpc.Account\x12L\n" +
@ -1027,7 +1189,8 @@ const file_lit_accounts_proto_rawDesc = "" +
"\fDebitAccount\x12\x1b.litrpc.DebitAccountRequest\x1a\x1c.litrpc.DebitAccountResponse\x12I\n" +
"\fListAccounts\x12\x1b.litrpc.ListAccountsRequest\x1a\x1c.litrpc.ListAccountsResponse\x12:\n" +
"\vAccountInfo\x12\x1a.litrpc.AccountInfoRequest\x1a\x0f.litrpc.Account\x12L\n" +
"\rRemoveAccount\x12\x1c.litrpc.RemoveAccountRequest\x1a\x1d.litrpc.RemoveAccountResponseB4Z2github.com/lightninglabs/lightning-terminal/litrpcb\x06proto3"
"\rRemoveAccount\x12\x1c.litrpc.RemoveAccountRequest\x1a\x1d.litrpc.RemoveAccountResponse\x12R\n" +
"\x0fAccountPayments\x12\x1e.litrpc.AccountPaymentsRequest\x1a\x1f.litrpc.AccountPaymentsResponseB4Z2github.com/lightninglabs/lightning-terminal/litrpcb\x06proto3"
var (
file_lit_accounts_proto_rawDescOnce sync.Once
@ -1041,24 +1204,27 @@ func file_lit_accounts_proto_rawDescGZIP() []byte {
return file_lit_accounts_proto_rawDescData
}
var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_lit_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
var file_lit_accounts_proto_goTypes = []any{
(*CreateAccountRequest)(nil), // 0: litrpc.CreateAccountRequest
(*CreateAccountResponse)(nil), // 1: litrpc.CreateAccountResponse
(*Account)(nil), // 2: litrpc.Account
(*AccountInvoice)(nil), // 3: litrpc.AccountInvoice
(*AccountPayment)(nil), // 4: litrpc.AccountPayment
(*UpdateAccountRequest)(nil), // 5: litrpc.UpdateAccountRequest
(*CreditAccountRequest)(nil), // 6: litrpc.CreditAccountRequest
(*CreditAccountResponse)(nil), // 7: litrpc.CreditAccountResponse
(*DebitAccountRequest)(nil), // 8: litrpc.DebitAccountRequest
(*DebitAccountResponse)(nil), // 9: litrpc.DebitAccountResponse
(*ListAccountsRequest)(nil), // 10: litrpc.ListAccountsRequest
(*ListAccountsResponse)(nil), // 11: litrpc.ListAccountsResponse
(*AccountInfoRequest)(nil), // 12: litrpc.AccountInfoRequest
(*RemoveAccountRequest)(nil), // 13: litrpc.RemoveAccountRequest
(*RemoveAccountResponse)(nil), // 14: litrpc.RemoveAccountResponse
(*AccountIdentifier)(nil), // 15: litrpc.AccountIdentifier
(*CreateAccountRequest)(nil), // 0: litrpc.CreateAccountRequest
(*CreateAccountResponse)(nil), // 1: litrpc.CreateAccountResponse
(*Account)(nil), // 2: litrpc.Account
(*AccountInvoice)(nil), // 3: litrpc.AccountInvoice
(*AccountPayment)(nil), // 4: litrpc.AccountPayment
(*UpdateAccountRequest)(nil), // 5: litrpc.UpdateAccountRequest
(*CreditAccountRequest)(nil), // 6: litrpc.CreditAccountRequest
(*CreditAccountResponse)(nil), // 7: litrpc.CreditAccountResponse
(*DebitAccountRequest)(nil), // 8: litrpc.DebitAccountRequest
(*DebitAccountResponse)(nil), // 9: litrpc.DebitAccountResponse
(*ListAccountsRequest)(nil), // 10: litrpc.ListAccountsRequest
(*ListAccountsResponse)(nil), // 11: litrpc.ListAccountsResponse
(*AccountInfoRequest)(nil), // 12: litrpc.AccountInfoRequest
(*RemoveAccountRequest)(nil), // 13: litrpc.RemoveAccountRequest
(*RemoveAccountResponse)(nil), // 14: litrpc.RemoveAccountResponse
(*AccountIdentifier)(nil), // 15: litrpc.AccountIdentifier
(*AccountPaymentsRequest)(nil), // 16: litrpc.AccountPaymentsRequest
(*AccountPaymentsResponse)(nil), // 17: litrpc.AccountPaymentsResponse
(*lnrpc.Payment)(nil), // 18: lnrpc.Payment
}
var file_lit_accounts_proto_depIdxs = []int32{
2, // 0: litrpc.CreateAccountResponse.account:type_name -> litrpc.Account
@ -1069,25 +1235,29 @@ var file_lit_accounts_proto_depIdxs = []int32{
15, // 5: litrpc.DebitAccountRequest.account:type_name -> litrpc.AccountIdentifier
2, // 6: litrpc.DebitAccountResponse.account:type_name -> litrpc.Account
2, // 7: litrpc.ListAccountsResponse.accounts:type_name -> litrpc.Account
0, // 8: litrpc.Accounts.CreateAccount:input_type -> litrpc.CreateAccountRequest
5, // 9: litrpc.Accounts.UpdateAccount:input_type -> litrpc.UpdateAccountRequest
6, // 10: litrpc.Accounts.CreditAccount:input_type -> litrpc.CreditAccountRequest
8, // 11: litrpc.Accounts.DebitAccount:input_type -> litrpc.DebitAccountRequest
10, // 12: litrpc.Accounts.ListAccounts:input_type -> litrpc.ListAccountsRequest
12, // 13: litrpc.Accounts.AccountInfo:input_type -> litrpc.AccountInfoRequest
13, // 14: litrpc.Accounts.RemoveAccount:input_type -> litrpc.RemoveAccountRequest
1, // 15: litrpc.Accounts.CreateAccount:output_type -> litrpc.CreateAccountResponse
2, // 16: litrpc.Accounts.UpdateAccount:output_type -> litrpc.Account
7, // 17: litrpc.Accounts.CreditAccount:output_type -> litrpc.CreditAccountResponse
9, // 18: litrpc.Accounts.DebitAccount:output_type -> litrpc.DebitAccountResponse
11, // 19: litrpc.Accounts.ListAccounts:output_type -> litrpc.ListAccountsResponse
2, // 20: litrpc.Accounts.AccountInfo:output_type -> litrpc.Account
14, // 21: litrpc.Accounts.RemoveAccount:output_type -> litrpc.RemoveAccountResponse
15, // [15:22] is the sub-list for method output_type
8, // [8:15] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
15, // 8: litrpc.AccountPaymentsRequest.account:type_name -> litrpc.AccountIdentifier
18, // 9: litrpc.AccountPaymentsResponse.payments:type_name -> lnrpc.Payment
0, // 10: litrpc.Accounts.CreateAccount:input_type -> litrpc.CreateAccountRequest
5, // 11: litrpc.Accounts.UpdateAccount:input_type -> litrpc.UpdateAccountRequest
6, // 12: litrpc.Accounts.CreditAccount:input_type -> litrpc.CreditAccountRequest
8, // 13: litrpc.Accounts.DebitAccount:input_type -> litrpc.DebitAccountRequest
10, // 14: litrpc.Accounts.ListAccounts:input_type -> litrpc.ListAccountsRequest
12, // 15: litrpc.Accounts.AccountInfo:input_type -> litrpc.AccountInfoRequest
13, // 16: litrpc.Accounts.RemoveAccount:input_type -> litrpc.RemoveAccountRequest
16, // 17: litrpc.Accounts.AccountPayments:input_type -> litrpc.AccountPaymentsRequest
1, // 18: litrpc.Accounts.CreateAccount:output_type -> litrpc.CreateAccountResponse
2, // 19: litrpc.Accounts.UpdateAccount:output_type -> litrpc.Account
7, // 20: litrpc.Accounts.CreditAccount:output_type -> litrpc.CreditAccountResponse
9, // 21: litrpc.Accounts.DebitAccount:output_type -> litrpc.DebitAccountResponse
11, // 22: litrpc.Accounts.ListAccounts:output_type -> litrpc.ListAccountsResponse
2, // 23: litrpc.Accounts.AccountInfo:output_type -> litrpc.Account
14, // 24: litrpc.Accounts.RemoveAccount:output_type -> litrpc.RemoveAccountResponse
17, // 25: litrpc.Accounts.AccountPayments:output_type -> litrpc.AccountPaymentsResponse
18, // [18:26] is the sub-list for method output_type
10, // [10:18] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_lit_accounts_proto_init() }
@ -1105,7 +1275,7 @@ func file_lit_accounts_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_lit_accounts_proto_rawDesc), len(file_lit_accounts_proto_rawDesc)),
NumEnums: 0,
NumMessages: 16,
NumMessages: 18,
NumExtensions: 0,
NumServices: 1,
},

View file

@ -325,6 +325,42 @@ func local_request_Accounts_RemoveAccount_0(ctx context.Context, marshaler runti
}
var (
filter_Accounts_AccountPayments_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Accounts_AccountPayments_0(ctx context.Context, marshaler runtime.Marshaler, client AccountsClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq AccountPaymentsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Accounts_AccountPayments_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.AccountPayments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Accounts_AccountPayments_0(ctx context.Context, marshaler runtime.Marshaler, server AccountsServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq AccountPaymentsRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Accounts_AccountPayments_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.AccountPayments(ctx, &protoReq)
return msg, metadata, err
}
// RegisterAccountsHandlerServer registers the http handlers for service Accounts to "mux".
// UnaryRPC :call AccountsServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@ -482,6 +518,31 @@ func RegisterAccountsHandlerServer(ctx context.Context, mux *runtime.ServeMux, s
})
mux.Handle("GET", pattern_Accounts_AccountPayments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/litrpc.Accounts/AccountPayments", runtime.WithHTTPPathPattern("/v1/accounts/payments"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Accounts_AccountPayments_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Accounts_AccountPayments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -655,6 +716,28 @@ func RegisterAccountsHandlerClient(ctx context.Context, mux *runtime.ServeMux, c
})
mux.Handle("GET", pattern_Accounts_AccountPayments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/litrpc.Accounts/AccountPayments", runtime.WithHTTPPathPattern("/v1/accounts/payments"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Accounts_AccountPayments_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_Accounts_AccountPayments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
@ -670,6 +753,8 @@ var (
pattern_Accounts_ListAccounts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "accounts"}, ""))
pattern_Accounts_RemoveAccount_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "accounts", "id"}, ""))
pattern_Accounts_AccountPayments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "accounts", "payments"}, ""))
)
var (
@ -684,4 +769,6 @@ var (
forward_Accounts_ListAccounts_0 = runtime.ForwardResponseMessage
forward_Accounts_RemoveAccount_0 = runtime.ForwardResponseMessage
forward_Accounts_AccountPayments_0 = runtime.ForwardResponseMessage
)

View file

@ -2,6 +2,8 @@ syntax = "proto3";
package litrpc;
import "proto/lnd.proto";
option go_package = "github.com/lightninglabs/lightning-terminal/litrpc";
service Accounts {
@ -52,6 +54,12 @@ service Accounts {
RemoveAccount removes the given account from the account database.
*/
rpc RemoveAccount (RemoveAccountRequest) returns (RemoveAccountResponse);
/* litcli: `accounts payments`
AccountPayments returns the detailed payment history for the given account.
*/
rpc AccountPayments (AccountPaymentsRequest)
returns (AccountPaymentsResponse);
}
message CreateAccountRequest {
@ -244,4 +252,55 @@ message AccountIdentifier {
// The label of the account.
string label = 2;
}
}
message AccountPaymentsRequest {
/*
The identifier of the account to query payments for.
*/
AccountIdentifier account = 1;
/*
The maximum number of payments to return. If set to 0, it will default
to 20. Capped at 50.
*/
uint64 max_payments = 2;
/*
The row offset into the list of payments that will be used as the start of
the query. The payments are returned in ascending lexicographical order of
their payment hash.
*/
uint64 index_offset = 3;
/*
If set, the total number of payments matching the query will be returned
in the response.
*/
bool count_total_payments = 4;
}
message AccountPaymentsResponse {
/*
The detailed payments associated with the account, sorted in ascending
lexicographical order of their payment hash.
*/
repeated lnrpc.Payment payments = 1;
/*
The row offset of the first payment returned.
*/
uint64 first_index_offset = 2;
/*
The row offset of the last payment returned. This can be used as the
index_offset in a subsequent query to paginate forwards.
*/
uint64 last_index_offset = 3;
/*
The total number of payments matching the query (only set if
count_total_payments was true in the request).
*/
uint64 total_num_payments = 4;
}

View file

@ -151,6 +151,68 @@
]
}
},
"/v1/accounts/payments": {
"get": {
"summary": "litcli: `accounts payments`\nAccountPayments returns the detailed payment history for the given account.",
"operationId": "Accounts_AccountPayments",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/litrpcAccountPaymentsResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "account.id",
"description": "The ID of the account.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "account.label",
"description": "The label of the account.",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "max_payments",
"description": "The maximum number of payments to return. If set to 0, it will default\nto 20. Capped at 50.",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "index_offset",
"description": "The row offset into the list of payments that will be used as the start of\nthe query. The payments are returned in ascending lexicographical order of\ntheir payment hash.",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "count_total_payments",
"description": "If set, the total number of payments matching the query will be returned\nin the response.",
"in": "query",
"required": false,
"type": "boolean"
}
],
"tags": [
"Accounts"
]
}
},
"/v1/accounts/{id}": {
"delete": {
"summary": "litcli: `accounts remove`\nRemoveAccount removes the given account from the account database.",
@ -295,6 +357,63 @@
}
}
},
"FailureFailureCode": {
"type": "string",
"enum": [
"RESERVED",
"INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS",
"INCORRECT_PAYMENT_AMOUNT",
"FINAL_INCORRECT_CLTV_EXPIRY",
"FINAL_INCORRECT_HTLC_AMOUNT",
"FINAL_EXPIRY_TOO_SOON",
"INVALID_REALM",
"EXPIRY_TOO_SOON",
"INVALID_ONION_VERSION",
"INVALID_ONION_HMAC",
"INVALID_ONION_KEY",
"AMOUNT_BELOW_MINIMUM",
"FEE_INSUFFICIENT",
"INCORRECT_CLTV_EXPIRY",
"CHANNEL_DISABLED",
"TEMPORARY_CHANNEL_FAILURE",
"REQUIRED_NODE_FEATURE_MISSING",
"REQUIRED_CHANNEL_FEATURE_MISSING",
"UNKNOWN_NEXT_PEER",
"TEMPORARY_NODE_FAILURE",
"PERMANENT_NODE_FAILURE",
"PERMANENT_CHANNEL_FAILURE",
"EXPIRY_TOO_FAR",
"MPP_TIMEOUT",
"INVALID_ONION_PAYLOAD",
"INVALID_ONION_BLINDING",
"INTERNAL_FAILURE",
"UNKNOWN_FAILURE",
"UNREADABLE_FAILURE"
],
"default": "RESERVED",
"description": " - RESERVED: The numbers assigned in this enumeration match the failure codes as\ndefined in BOLT #4. Because protobuf 3 requires enums to start with 0,\na RESERVED value is added.\n - INTERNAL_FAILURE: An internal error occurred.\n - UNKNOWN_FAILURE: The error source is known, but the failure itself couldn't be decoded.\n - UNREADABLE_FAILURE: An unreadable failure result is returned if the received failure message\ncannot be decrypted. In that case the error source is unknown."
},
"HTLCAttemptHTLCStatus": {
"type": "string",
"enum": [
"IN_FLIGHT",
"SUCCEEDED",
"FAILED"
],
"default": "IN_FLIGHT"
},
"PaymentPaymentStatus": {
"type": "string",
"enum": [
"UNKNOWN",
"IN_FLIGHT",
"SUCCEEDED",
"FAILED",
"INITIATED"
],
"default": "UNKNOWN",
"description": " - UNKNOWN: Deprecated. This status will never be returned.\n - IN_FLIGHT: Payment has inflight HTLCs.\n - SUCCEEDED: Payment is settled.\n - FAILED: Payment is failed.\n - INITIATED: Payment is created and has not attempted any HTLCs."
},
"litrpcAccount": {
"type": "object",
"properties": {
@ -386,6 +505,34 @@
}
}
},
"litrpcAccountPaymentsResponse": {
"type": "object",
"properties": {
"payments": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/lnrpcPayment"
},
"description": "The detailed payments associated with the account, sorted in ascending\nlexicographical order of their payment hash."
},
"first_index_offset": {
"type": "string",
"format": "uint64",
"description": "The row offset of the first payment returned."
},
"last_index_offset": {
"type": "string",
"format": "uint64",
"description": "The row offset of the last payment returned. This can be used as the\nindex_offset in a subsequent query to paginate forwards."
},
"total_num_payments": {
"type": "string",
"format": "uint64",
"description": "The total number of payments matching the query (only set if\ncount_total_payments was true in the request)."
}
}
},
"litrpcCreateAccountRequest": {
"type": "object",
"properties": {
@ -453,6 +600,409 @@
"litrpcRemoveAccountResponse": {
"type": "object"
},
"lnrpcAMPRecord": {
"type": "object",
"properties": {
"root_share": {
"type": "string",
"format": "byte"
},
"set_id": {
"type": "string",
"format": "byte"
},
"child_index": {
"type": "integer",
"format": "int64"
}
}
},
"lnrpcChannelUpdate": {
"type": "object",
"properties": {
"signature": {
"type": "string",
"format": "byte",
"description": "The signature that validates the announced data and proves the ownership\nof node id."
},
"chain_hash": {
"type": "string",
"format": "byte",
"description": "The target chain that this channel was opened within. This value\nshould be the genesis hash of the target chain. Along with the short\nchannel ID, this uniquely identifies the channel globally in a\nblockchain."
},
"chan_id": {
"type": "string",
"format": "uint64",
"description": "The unique description of the funding transaction."
},
"timestamp": {
"type": "integer",
"format": "int64",
"description": "A timestamp that allows ordering in the case of multiple announcements.\nWe should ignore the message if timestamp is not greater than the\nlast-received."
},
"message_flags": {
"type": "integer",
"format": "int64",
"description": "The bitfield that describes whether optional fields are present in this\nupdate. Currently, the least-significant bit must be set to 1 if the\noptional field MaxHtlc is present."
},
"channel_flags": {
"type": "integer",
"format": "int64",
"description": "The bitfield that describes additional meta-data concerning how the\nupdate is to be interpreted. Currently, the least-significant bit must be\nset to 0 if the creating node corresponds to the first node in the\npreviously sent channel announcement and 1 otherwise. If the second bit\nis set, then the channel is set to be disabled."
},
"time_lock_delta": {
"type": "integer",
"format": "int64",
"description": "The minimum number of blocks this node requires to be added to the expiry\nof HTLCs. This is a security parameter determined by the node operator.\nThis value represents the required gap between the time locks of the\nincoming and outgoing HTLC's set to this node."
},
"htlc_minimum_msat": {
"type": "string",
"format": "uint64",
"description": "The minimum HTLC value which will be accepted."
},
"base_fee": {
"type": "integer",
"format": "int64",
"description": "The base fee that must be used for incoming HTLC's to this particular\nchannel. This value will be tacked onto the required for a payment\nindependent of the size of the payment."
},
"fee_rate": {
"type": "integer",
"format": "int64",
"description": "The fee rate that will be charged per millionth of a satoshi."
},
"htlc_maximum_msat": {
"type": "string",
"format": "uint64",
"description": "The maximum HTLC value which will be accepted."
},
"extra_opaque_data": {
"type": "string",
"format": "byte",
"description": "The set of data that was appended to this message, some of which we may\nnot actually know how to iterate or parse. By holding onto this data, we\nensure that we're able to properly validate the set of signatures that\ncover these new fields, and ensure we're able to make upgrades to the\nnetwork in a forwards compatible manner."
}
}
},
"lnrpcFailure": {
"type": "object",
"properties": {
"code": {
"$ref": "#/definitions/FailureFailureCode",
"title": "Failure code as defined in the Lightning spec"
},
"channel_update": {
"$ref": "#/definitions/lnrpcChannelUpdate",
"description": "An optional channel update message."
},
"htlc_msat": {
"type": "string",
"format": "uint64",
"description": "A failure type-dependent htlc value."
},
"onion_sha_256": {
"type": "string",
"format": "byte",
"description": "The sha256 sum of the onion payload."
},
"cltv_expiry": {
"type": "integer",
"format": "int64",
"description": "A failure type-dependent cltv expiry value."
},
"flags": {
"type": "integer",
"format": "int64",
"description": "A failure type-dependent flags value."
},
"failure_source_index": {
"type": "integer",
"format": "int64",
"description": "The position in the path of the intermediate or final node that generated\nthe failure message. Position zero is the sender node."
},
"height": {
"type": "integer",
"format": "int64",
"description": "A failure type-dependent block height."
}
}
},
"lnrpcHTLCAttempt": {
"type": "object",
"properties": {
"attempt_id": {
"type": "string",
"format": "uint64",
"description": "The unique ID that is used for this attempt."
},
"status": {
"$ref": "#/definitions/HTLCAttemptHTLCStatus",
"description": "The status of the HTLC."
},
"route": {
"$ref": "#/definitions/lnrpcRoute",
"description": "The route taken by this HTLC."
},
"attempt_time_ns": {
"type": "string",
"format": "int64",
"description": "The time in UNIX nanoseconds at which this HTLC was sent."
},
"resolve_time_ns": {
"type": "string",
"format": "int64",
"description": "The time in UNIX nanoseconds at which this HTLC was settled or failed.\nThis value will not be set if the HTLC is still IN_FLIGHT."
},
"failure": {
"$ref": "#/definitions/lnrpcFailure",
"description": "Detailed htlc failure info."
},
"preimage": {
"type": "string",
"format": "byte",
"description": "The preimage that was used to settle the HTLC."
}
}
},
"lnrpcHop": {
"type": "object",
"properties": {
"chan_id": {
"type": "string",
"format": "uint64",
"description": "The unique channel ID for the channel. The first 3 bytes are the block\nheight, the next 3 the index within the block, and the last 2 bytes are the\noutput index for the channel."
},
"chan_capacity": {
"type": "string",
"format": "int64"
},
"amt_to_forward": {
"type": "string",
"format": "int64"
},
"fee": {
"type": "string",
"format": "int64"
},
"expiry": {
"type": "integer",
"format": "int64"
},
"amt_to_forward_msat": {
"type": "string",
"format": "int64"
},
"fee_msat": {
"type": "string",
"format": "int64"
},
"pub_key": {
"type": "string",
"description": "An optional public key of the hop. If the public key is given, the payment\ncan be executed without relying on a copy of the channel graph."
},
"tlv_payload": {
"type": "boolean",
"description": "If set to true, then this hop will be encoded using the new variable length\nTLV format. Note that if any custom tlv_records below are specified, then\nthis field MUST be set to true for them to be encoded properly."
},
"mpp_record": {
"$ref": "#/definitions/lnrpcMPPRecord",
"description": "An optional TLV record that signals the use of an MPP payment. If present,\nthe receiver will enforce that the same mpp_record is included in the final\nhop payload of all non-zero payments in the HTLC set. If empty, a regular\nsingle-shot payment is or was attempted."
},
"amp_record": {
"$ref": "#/definitions/lnrpcAMPRecord",
"description": "An optional TLV record that signals the use of an AMP payment. If present,\nthe receiver will treat all received payments including the same\n(payment_addr, set_id) pair as being part of one logical payment. The\npayment will be settled by XORing the root_share's together and deriving the\nchild hashes and preimages according to BOLT XX. Must be used in conjunction\nwith mpp_record."
},
"custom_records": {
"type": "object",
"additionalProperties": {
"type": "string",
"format": "byte"
},
"description": "An optional set of key-value TLV records. This is useful within the context\nof the SendToRoute call as it allows callers to specify arbitrary K-V pairs\nto drop off at each hop within the onion."
},
"metadata": {
"type": "string",
"format": "byte",
"description": "The payment metadata to send along with the payment to the payee."
},
"blinding_point": {
"type": "string",
"format": "byte",
"description": "Blinding point is an optional blinding point included for introduction\nnodes in blinded paths. This field is mandatory for hops that represents\nthe introduction point in a blinded path."
},
"encrypted_data": {
"type": "string",
"format": "byte",
"description": "Encrypted data is a receiver-produced blob of data that provides hops\nin a blinded route with forwarding data. As this data is encrypted by\nthe recipient, we will not be able to parse it - it is essentially an\narbitrary blob of data from our node's perspective. This field is\nmandatory for all hops in a blinded path, including the introduction\nnode."
},
"total_amt_msat": {
"type": "string",
"format": "uint64",
"description": "The total amount that is sent to the recipient (possibly across multiple\nHTLCs), as specified by the sender when making a payment to a blinded path.\nThis value is only set in the final hop payload of a blinded payment. This\nvalue is analogous to the MPPRecord that is used for regular (non-blinded)\nMPP payments."
}
}
},
"lnrpcMPPRecord": {
"type": "object",
"properties": {
"payment_addr": {
"type": "string",
"format": "byte",
"description": "A unique, random identifier used to authenticate the sender as the intended\npayer of a multi-path payment. The payment_addr must be the same for all\nsubpayments, and match the payment_addr provided in the receiver's invoice.\nThe same payment_addr must be used on all subpayments. This is also called\npayment secret in specifications (e.g. BOLT 11)."
},
"total_amt_msat": {
"type": "string",
"format": "int64",
"description": "The total amount in milli-satoshis being sent as part of a larger multi-path\npayment. The caller is responsible for ensuring subpayments to the same node\nand payment_hash sum exactly to total_amt_msat. The same\ntotal_amt_msat must be used on all subpayments."
}
}
},
"lnrpcPayment": {
"type": "object",
"properties": {
"payment_hash": {
"type": "string",
"title": "The payment hash"
},
"value": {
"type": "string",
"format": "int64",
"description": "Deprecated, use value_sat or value_msat."
},
"creation_date": {
"type": "string",
"format": "int64",
"title": "Deprecated, use creation_time_ns"
},
"fee": {
"type": "string",
"format": "int64",
"description": "Deprecated, use fee_sat or fee_msat."
},
"payment_preimage": {
"type": "string",
"title": "The payment preimage"
},
"value_sat": {
"type": "string",
"format": "int64",
"title": "The value of the payment in satoshis"
},
"value_msat": {
"type": "string",
"format": "int64",
"title": "The value of the payment in milli-satoshis"
},
"payment_request": {
"type": "string",
"description": "The optional payment request being fulfilled."
},
"status": {
"$ref": "#/definitions/PaymentPaymentStatus",
"description": "The status of the payment."
},
"fee_sat": {
"type": "string",
"format": "int64",
"title": "The fee paid for this payment in satoshis"
},
"fee_msat": {
"type": "string",
"format": "int64",
"title": "The fee paid for this payment in milli-satoshis"
},
"creation_time_ns": {
"type": "string",
"format": "int64",
"description": "The time in UNIX nanoseconds at which the payment was created."
},
"htlcs": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/lnrpcHTLCAttempt"
},
"description": "The HTLCs made in attempt to settle the payment."
},
"payment_index": {
"type": "string",
"format": "uint64",
"description": "The creation index of this payment. Each payment can be uniquely identified\nby this index, which may not strictly increment by 1 for payments made in\nolder versions of lnd."
},
"failure_reason": {
"$ref": "#/definitions/lnrpcPaymentFailureReason"
},
"first_hop_custom_records": {
"type": "object",
"additionalProperties": {
"type": "string",
"format": "byte"
},
"description": "The custom TLV records that were sent to the first hop as part of the HTLC\nwire message for this payment."
}
}
},
"lnrpcPaymentFailureReason": {
"type": "string",
"enum": [
"FAILURE_REASON_NONE",
"FAILURE_REASON_TIMEOUT",
"FAILURE_REASON_NO_ROUTE",
"FAILURE_REASON_ERROR",
"FAILURE_REASON_INCORRECT_PAYMENT_DETAILS",
"FAILURE_REASON_INSUFFICIENT_BALANCE",
"FAILURE_REASON_CANCELED"
],
"default": "FAILURE_REASON_NONE",
"description": " - FAILURE_REASON_NONE: Payment isn't failed (yet).\n - FAILURE_REASON_TIMEOUT: There are more routes to try, but the payment timeout was exceeded.\n - FAILURE_REASON_NO_ROUTE: All possible routes were tried and failed permanently. Or were no\nroutes to the destination at all.\n - FAILURE_REASON_ERROR: A non-recoverable error has occured.\n - FAILURE_REASON_INCORRECT_PAYMENT_DETAILS: Payment details incorrect (unknown hash, invalid amt or\ninvalid final cltv delta)\n - FAILURE_REASON_INSUFFICIENT_BALANCE: Insufficient local balance.\n - FAILURE_REASON_CANCELED: The payment was canceled."
},
"lnrpcRoute": {
"type": "object",
"properties": {
"total_time_lock": {
"type": "integer",
"format": "int64",
"description": "The cumulative (final) time lock across the entire route. This is the CLTV\nvalue that should be extended to the first hop in the route. All other hops\nwill decrement the time-lock as advertised, leaving enough time for all\nhops to wait for or present the payment preimage to complete the payment."
},
"total_fees": {
"type": "string",
"format": "int64",
"description": "The sum of the fees paid at each hop within the final route. In the case\nof a one-hop payment, this value will be zero as we don't need to pay a fee\nto ourselves."
},
"total_amt": {
"type": "string",
"format": "int64",
"description": "The total amount of funds required to complete a payment over this route.\nThis value includes the cumulative fees at each hop. As a result, the HTLC\nextended to the first-hop in the route will need to have at least this many\nsatoshis, otherwise the route will fail at an intermediate node due to an\ninsufficient amount of fees."
},
"hops": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/lnrpcHop"
},
"description": "Contains details concerning the specific forwarding details at each hop."
},
"total_fees_msat": {
"type": "string",
"format": "int64",
"description": "The total fees in millisatoshis."
},
"total_amt_msat": {
"type": "string",
"format": "int64",
"description": "The total amount in millisatoshis."
},
"first_hop_amount_msat": {
"type": "string",
"format": "int64",
"description": "The actual on-chain amount that was sent out to the first hop. This value is\nonly different from the total_amt_msat field if this is a custom channel\npayment and the value transported in the HTLC is different from the BTC\namount in the HTLC. If this value is zero, then this is an old payment that\ndidn't have this value yet and can be ignored."
},
"custom_channel_data": {
"type": "string",
"format": "byte",
"description": "Custom channel data that might be populated in custom channels."
}
},
"description": "A path through the channel graph which runs over one or more channels in\nsuccession. This struct carries all the information required to craft the\nSphinx onion packet, and send the payment along the first hop in the path. A\nroute is only selected as valid if all the channels have sufficient capacity to\ncarry the initial payment amount after fees are accounted for."
},
"protobufAny": {
"type": "object",
"properties": {

View file

@ -21,3 +21,5 @@ http:
- selector: litrpc.Accounts.DebitAccount
post: "/v1/accounts/debit/{account.id}"
body: "*"
- selector: litrpc.Accounts.AccountPayments
get: "/v1/accounts/payments"

View file

@ -52,6 +52,9 @@ type AccountsClient interface {
// litcli: `accounts remove`
// RemoveAccount removes the given account from the account database.
RemoveAccount(ctx context.Context, in *RemoveAccountRequest, opts ...grpc.CallOption) (*RemoveAccountResponse, error)
// litcli: `accounts payments`
// AccountPayments returns the detailed payment history for the given account.
AccountPayments(ctx context.Context, in *AccountPaymentsRequest, opts ...grpc.CallOption) (*AccountPaymentsResponse, error)
}
type accountsClient struct {
@ -125,6 +128,15 @@ func (c *accountsClient) RemoveAccount(ctx context.Context, in *RemoveAccountReq
return out, nil
}
func (c *accountsClient) AccountPayments(ctx context.Context, in *AccountPaymentsRequest, opts ...grpc.CallOption) (*AccountPaymentsResponse, error) {
out := new(AccountPaymentsResponse)
err := c.cc.Invoke(ctx, "/litrpc.Accounts/AccountPayments", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AccountsServer is the server API for Accounts service.
// All implementations must embed UnimplementedAccountsServer
// for forward compatibility
@ -163,6 +175,9 @@ type AccountsServer interface {
// litcli: `accounts remove`
// RemoveAccount removes the given account from the account database.
RemoveAccount(context.Context, *RemoveAccountRequest) (*RemoveAccountResponse, error)
// litcli: `accounts payments`
// AccountPayments returns the detailed payment history for the given account.
AccountPayments(context.Context, *AccountPaymentsRequest) (*AccountPaymentsResponse, error)
mustEmbedUnimplementedAccountsServer()
}
@ -191,6 +206,9 @@ func (UnimplementedAccountsServer) AccountInfo(context.Context, *AccountInfoRequ
func (UnimplementedAccountsServer) RemoveAccount(context.Context, *RemoveAccountRequest) (*RemoveAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveAccount not implemented")
}
func (UnimplementedAccountsServer) AccountPayments(context.Context, *AccountPaymentsRequest) (*AccountPaymentsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AccountPayments not implemented")
}
func (UnimplementedAccountsServer) mustEmbedUnimplementedAccountsServer() {}
// UnsafeAccountsServer may be embedded to opt out of forward compatibility for this service.
@ -330,6 +348,24 @@ func _Accounts_RemoveAccount_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _Accounts_AccountPayments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AccountPaymentsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountsServer).AccountPayments(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/litrpc.Accounts/AccountPayments",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountsServer).AccountPayments(ctx, req.(*AccountPaymentsRequest))
}
return interceptor(ctx, in, info, handler)
}
// Accounts_ServiceDesc is the grpc.ServiceDesc for Accounts service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -365,6 +401,10 @@ var Accounts_ServiceDesc = grpc.ServiceDesc{
MethodName: "RemoveAccount",
Handler: _Accounts_RemoveAccount_Handler,
},
{
MethodName: "AccountPayments",
Handler: _Accounts_AccountPayments_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "lit-accounts.proto",

View file

@ -44,6 +44,10 @@ var (
Entity: "account",
Action: "read",
}},
"/litrpc.Accounts/AccountPayments": {{
Entity: "account",
Action: "read",
}},
"/litrpc.Accounts/RemoveAccount": {{
Entity: "account",
Action: "write",

View file

@ -2,6 +2,8 @@ syntax = "proto3";
package litrpc;
import "lnd.proto";
option go_package = "github.com/lightninglabs/lightning-terminal/litrpc";
service Accounts {
@ -52,6 +54,12 @@ service Accounts {
RemoveAccount removes the given account from the account database.
*/
rpc RemoveAccount (RemoveAccountRequest) returns (RemoveAccountResponse);
/* litcli: `accounts payments`
AccountPayments returns the detailed payment history for the given account.
*/
rpc AccountPayments (AccountPaymentsRequest)
returns (AccountPaymentsResponse);
}
message CreateAccountRequest {
@ -244,4 +252,55 @@ message AccountIdentifier {
// The label of the account.
string label = 2;
}
}
message AccountPaymentsRequest {
/*
The identifier of the account to query payments for.
*/
AccountIdentifier account = 1;
/*
The maximum number of payments to return. If set to 0, it will default
to 20. Capped at 50.
*/
uint64 max_payments = 2 [jstype = JS_STRING];
/*
The row offset into the list of payments that will be used as the start of
the query. The payments are returned in ascending lexicographical order of
their payment hash.
*/
uint64 index_offset = 3 [jstype = JS_STRING];
/*
If set, the total number of payments matching the query will be returned
in the response.
*/
bool count_total_payments = 4;
}
message AccountPaymentsResponse {
/*
The detailed payments associated with the account, sorted in ascending
lexicographical order of their payment hash.
*/
repeated lnrpc.Payment payments = 1;
/*
The row offset of the first payment returned.
*/
uint64 first_index_offset = 2 [jstype = JS_STRING];
/*
The row offset of the last payment returned. This can be used as the
index_offset in a subsequent query to paginate forwards.
*/
uint64 last_index_offset = 3 [jstype = JS_STRING];
/*
The total number of payments matching the query (only set if
count_total_payments was true in the request).
*/
uint64 total_num_payments = 4 [jstype = JS_STRING];
}