account: expose historical account modification fees via RPC

This commit adds a new RPC endpoint which returns a per-account list of account modification fees.
This commit is contained in:
Robyn 2022-10-17 11:38:46 +01:00 committed by ffranr
parent 87f76c7d80
commit cd632058b0
No known key found for this signature in database
GPG key ID: B1F8848557AA29D2
10 changed files with 1439 additions and 712 deletions

View file

@ -30,6 +30,7 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
@ -52,6 +53,8 @@ const (
// both extremes for valid account expirations.
minAccountExpiry = 144 // One day worth of blocks.
maxAccountExpiry = 144 * 365 // A year worth of blocks.
txLabelPrefixTag = "poold -- "
)
var (
@ -148,27 +151,46 @@ type AccountTxLabel struct {
func actionTxLabel(account *Account, action Action, isExpirySpend bool,
txFee *btcutil.Amount, balanceDiff btcutil.Amount) string {
prefixTag := "poold -- %s"
acctKey := account.TraderKey.PubKey.SerializeCompressed()
key := fmt.Sprintf("%x", acctKey)
label := AccountTxLabel{
Key: key,
Action: action,
ExpiryHeight: account.Expiry,
OutputIndex: account.OutPoint.Index,
IsExpirySpend: isExpirySpend,
TxFee: txFee,
BalanceDiff: balanceDiff,
label := TxLabel{
Account: AccountTxLabel{
Key: key,
Action: action,
ExpiryHeight: account.Expiry,
OutputIndex: account.OutPoint.Index,
IsExpirySpend: isExpirySpend,
TxFee: txFee,
BalanceDiff: balanceDiff,
},
}
labelJson, err := json.Marshal(label)
if err != nil {
log.Errorf("Internal error: failed to serialize json "+
"from %v: %v", label, err)
return fmt.Sprintf(prefixTag, action)
return fmt.Sprintf("%s%s", txLabelPrefixTag, action)
}
return fmt.Sprintf(prefixTag, labelJson)
return fmt.Sprintf("%s%s", txLabelPrefixTag, labelJson)
}
// IsPoolTx returns true if the given transaction is related to pool.
func IsPoolTx(tx *lnrpc.Transaction) bool {
return strings.HasPrefix(tx.Label, txLabelPrefixTag)
}
// ParseTxLabel parses and returns data fields stored in a given transaction
// label.
func ParseTxLabel(label string) (*TxLabel, error) {
label = strings.TrimPrefix(label, txLabelPrefixTag)
var data TxLabel
err := json.Unmarshal([]byte(label), &data)
if err != nil {
return nil, err
}
return &data, nil
}
// ManagerConfig contains all of the required dependencies for the Manager to

View file

@ -49,12 +49,14 @@ var (
)
type testCase struct {
name string
feeExpr FeeExpr
fee btcutil.Amount
version Version
newVersion Version
expectedErr string
name string
feeExpr FeeExpr
fee btcutil.Amount
version Version
newVersion Version
expectedErr string
action Action
isExpirySpend bool
// The following fields are only used by deposit tests.
fundedOutputAmount btcutil.Amount
@ -1430,3 +1432,55 @@ func TestMakeTxnLabel(t *testing.T) {
require.Equal(t, genLabel, testCase.label)
}
}
// TestParseTxLabel tests whether an account transaction labels can be
// parsed correctly.
func TestParseTxLabel(t *testing.T) {
t.Parallel()
cases := []*testCase{
{
fee: btcutil.Amount(1027),
action: WITHDRAW,
isExpirySpend: false,
},
{
fee: btcutil.Amount(42),
action: DEPOSIT,
isExpirySpend: false,
},
{
fee: btcutil.Amount(42),
action: RENEW,
isExpirySpend: true,
},
}
runSubTests(t, cases, func(t *testing.T, h *testHarness, tc *testCase) {
expiryHeight := uint32(bestHeight + maxAccountExpiry)
account := h.openAccount(
maxAccountValue, expiryHeight, bestHeight, tc.version,
)
acctKey := account.TraderKey.PubKey.SerializeCompressed()
key := fmt.Sprintf("%x", acctKey)
label := actionTxLabel(
account, tc.action, tc.isExpirySpend, &tc.fee,
btcutil.Amount(10000),
)
actual, err := ParseTxLabel(label)
expected := &TxLabel{
AccountTxLabel{
Key: key,
Action: tc.action,
ExpiryHeight: expiryHeight,
OutputIndex: 0,
IsExpirySpend: tc.isExpirySpend,
TxFee: &tc.fee,
BalanceDiff: btcutil.Amount(10000),
},
}
require.Nil(t, err)
require.Equal(t, expected, actual)
})
}

View file

@ -26,6 +26,7 @@ var accountsCommands = []cli.Command{
withdrawAccountCommand,
renewAccountCommand,
closeAccountCommand,
listAccountFeesCommand,
bumpAccountFeeCommand,
recoverAccountsCommand,
},
@ -819,6 +820,35 @@ func bumpAccountFee(ctx *cli.Context) error {
return nil
}
var listAccountFeesCommand = cli.Command{
Name: "listfees",
ShortName: "f",
Usage: "list the account modification transaction fees",
Description: `
This command prints a map from account key to an ordered list of account
modification transaction fees.
`,
Action: listAccountFees,
}
func listAccountFees(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.AccountModificationFees(
context.Background(), &poolrpc.AccountModificationFeesRequest{},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var recoverAccountsCommand = cli.Command{
Name: "recover",
Usage: "recover accounts after data loss with the help of the " +

View file

@ -58,6 +58,10 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "account",
Action: "write",
}},
"/poolrpc.Trader/AccountModificationFees": {{
Entity: "account",
Action: "read",
}},
"/poolrpc.Trader/SubmitOrder": {{
Entity: "order",
Action: "write",

File diff suppressed because it is too large Load diff

View file

@ -297,6 +297,31 @@ func RegisterTraderJSONCallbacks(registry map[string]func(ctx context.Context,
callback(string(respBytes), nil)
}
registry["poolrpc.Trader.AccountModificationFees"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &AccountModificationFeesRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewTraderClient(conn)
resp, err := client.AccountModificationFees(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["poolrpc.Trader.SubmitOrder"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View file

@ -78,6 +78,13 @@ service Trader {
rpc RecoverAccounts (RecoverAccountsRequest)
returns (RecoverAccountsResponse);
/* pool: `accounts listfees`
AccountModificationFees returns a map from account key to an ordered list of
account action modification fees.
*/
rpc AccountModificationFees (AccountModificationFeesRequest)
returns (AccountModificationFeesResponse);
/* pool: `orders submit`
SubmitOrder creates a new ask or bid order and submits for the given account
and submits it to the auction server for matching.
@ -1011,6 +1018,45 @@ message RecoverAccountsResponse {
uint32 num_recovered_accounts = 1;
}
message AccountModificationFeesRequest {
}
message AccountModificationFee {
// Modification action type.
string action = 1;
// Transaction ID.
string txid = 2;
// Action transaction block height.
int32 block_height = 3;
// Action transaction timestamp.
int64 timestamp = 4;
// Action transaction output amount.
int64 output_amount = 5;
// Action transaction fee.
oneof fee {
// A flag which is true if fee value has not been set, and is otherwise
// false.
bool fee_null = 6;
// Action transaction fee value.
int64 fee_value = 7;
}
}
message ListOfAccountModificationFees {
repeated AccountModificationFee modification_fees = 1;
}
message AccountModificationFeesResponse {
// A map from account key to an ordered list of account modification fees.
map<string, ListOfAccountModificationFees> accounts = 1;
}
message AuctionFeeRequest {
}

View file

@ -1018,6 +1018,55 @@
}
}
},
"poolrpcAccountModificationFee": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Modification action type."
},
"txid": {
"type": "string",
"description": "Transaction ID."
},
"block_height": {
"type": "integer",
"format": "int32",
"description": "Action transaction block height."
},
"timestamp": {
"type": "string",
"format": "int64",
"description": "Action transaction timestamp."
},
"output_amount": {
"type": "string",
"format": "int64",
"description": "Action transaction output amount."
},
"fee_null": {
"type": "boolean",
"description": "A flag which is true if fee value has not been set, and is otherwise\nfalse."
},
"fee_value": {
"type": "string",
"format": "int64",
"description": "Action transaction fee value."
}
}
},
"poolrpcAccountModificationFeesResponse": {
"type": "object",
"properties": {
"accounts": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/poolrpcListOfAccountModificationFees"
},
"description": "A map from account key to an ordered list of account modification fees."
}
}
},
"poolrpcAccountState": {
"type": "string",
"enum": [
@ -1737,6 +1786,17 @@
}
}
},
"poolrpcListOfAccountModificationFees": {
"type": "object",
"properties": {
"modification_fees": {
"type": "array",
"items": {
"$ref": "#/definitions/poolrpcAccountModificationFee"
}
}
}
},
"poolrpcListOrdersResponse": {
"type": "object",
"properties": {

View file

@ -66,6 +66,10 @@ type TraderClient interface {
//RecoverAccounts queries the auction server for this trader daemon's accounts
//in case we lost our local account database.
RecoverAccounts(ctx context.Context, in *RecoverAccountsRequest, opts ...grpc.CallOption) (*RecoverAccountsResponse, error)
// pool: `accounts listfees`
//AccountModificationFees returns a map from account key to an ordered list of
//account action modification fees.
AccountModificationFees(ctx context.Context, in *AccountModificationFeesRequest, opts ...grpc.CallOption) (*AccountModificationFeesResponse, error)
// pool: `orders submit`
//SubmitOrder creates a new ask or bid order and submits for the given account
//and submits it to the auction server for matching.
@ -266,6 +270,15 @@ func (c *traderClient) RecoverAccounts(ctx context.Context, in *RecoverAccountsR
return out, nil
}
func (c *traderClient) AccountModificationFees(ctx context.Context, in *AccountModificationFeesRequest, opts ...grpc.CallOption) (*AccountModificationFeesResponse, error) {
out := new(AccountModificationFeesResponse)
err := c.cc.Invoke(ctx, "/poolrpc.Trader/AccountModificationFees", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *traderClient) SubmitOrder(ctx context.Context, in *SubmitOrderRequest, opts ...grpc.CallOption) (*SubmitOrderResponse, error) {
out := new(SubmitOrderResponse)
err := c.cc.Invoke(ctx, "/poolrpc.Trader/SubmitOrder", in, out, opts...)
@ -479,6 +492,10 @@ type TraderServer interface {
//RecoverAccounts queries the auction server for this trader daemon's accounts
//in case we lost our local account database.
RecoverAccounts(context.Context, *RecoverAccountsRequest) (*RecoverAccountsResponse, error)
// pool: `accounts listfees`
//AccountModificationFees returns a map from account key to an ordered list of
//account action modification fees.
AccountModificationFees(context.Context, *AccountModificationFeesRequest) (*AccountModificationFeesResponse, error)
// pool: `orders submit`
//SubmitOrder creates a new ask or bid order and submits for the given account
//and submits it to the auction server for matching.
@ -610,6 +627,9 @@ func (UnimplementedTraderServer) BumpAccountFee(context.Context, *BumpAccountFee
func (UnimplementedTraderServer) RecoverAccounts(context.Context, *RecoverAccountsRequest) (*RecoverAccountsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RecoverAccounts not implemented")
}
func (UnimplementedTraderServer) AccountModificationFees(context.Context, *AccountModificationFeesRequest) (*AccountModificationFeesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AccountModificationFees not implemented")
}
func (UnimplementedTraderServer) SubmitOrder(context.Context, *SubmitOrderRequest) (*SubmitOrderResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SubmitOrder not implemented")
}
@ -875,6 +895,24 @@ func _Trader_RecoverAccounts_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _Trader_AccountModificationFees_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AccountModificationFeesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TraderServer).AccountModificationFees(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/poolrpc.Trader/AccountModificationFees",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TraderServer).AccountModificationFees(ctx, req.(*AccountModificationFeesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Trader_SubmitOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitOrderRequest)
if err := dec(in); err != nil {
@ -1250,6 +1288,10 @@ var Trader_ServiceDesc = grpc.ServiceDesc{
MethodName: "RecoverAccounts",
Handler: _Trader_RecoverAccounts_Handler,
},
{
MethodName: "AccountModificationFees",
Handler: _Trader_AccountModificationFees_Handler,
},
{
MethodName: "SubmitOrder",
Handler: _Trader_SubmitOrder_Handler,

View file

@ -3030,6 +3030,80 @@ func (s *rpcServer) determineAccountVersion(
return account.VersionTaprootEnabled, nil
}
// AccountModificationFees returns a map from account key to an ordered list of
// account modifying action fees.
func (s *rpcServer) AccountModificationFees(ctx context.Context,
req *poolrpc.AccountModificationFeesRequest) (
*poolrpc.AccountModificationFeesResponse, error) {
// Retrieve all lightning wallet transactions.
transactionDetails, err := s.lndClient.GetTransactions(
ctx, &lnrpc.GetTransactionsRequest{},
)
if err != nil {
return nil, fmt.Errorf("error retrieving transactions: %v", err)
}
// A map from account key to an ordered list of account action fees.
accountActions := make(map[string][]*poolrpc.AccountModificationFee)
for _, tx := range transactionDetails.Transactions {
// Skip transactions which are not related to pool.
if !account.IsPoolTx(tx) {
continue
}
// Parse account data from transaction label.
labelData, err := account.ParseTxLabel(tx.Label)
if err != nil {
return nil, fmt.Errorf("failed to parse transaction "+
"label: %v (error: %v)", tx.Label, err)
}
// Ignore transaction if account modification not found.
if labelData == nil {
continue
}
// Select the account specific transaction output. Each account
// action transaction should include an account specific output.
output := tx.OutputDetails[labelData.Account.OutputIndex]
// Construct account modification fee structure.
acctModFee := &poolrpc.AccountModificationFee{
Action: string(labelData.Account.Action),
Txid: tx.TxHash,
BlockHeight: tx.BlockHeight,
Timestamp: tx.TimeStamp,
OutputAmount: output.Amount,
}
acctModFee.Fee = &poolrpc.AccountModificationFee_FeeNull{
FeeNull: true,
}
if labelData.Account.TxFee != nil {
acctModFee.Fee = &poolrpc.AccountModificationFee_FeeValue{
FeeValue: int64(*labelData.Account.TxFee),
}
}
// Handle all non-create account actions.
accountActions[labelData.Account.Key] = append(
accountActions[labelData.Account.Key], acctModFee,
)
}
result := make(map[string]*poolrpc.ListOfAccountModificationFees)
for traderKey, modificationFees := range accountActions {
result[traderKey] = &poolrpc.ListOfAccountModificationFees{
ModificationFees: modificationFees,
}
}
return &poolrpc.AccountModificationFeesResponse{
Accounts: result,
}, nil
}
// rpcOrderStateToDBState maps the order state as received over the RPC
// protocol to the local state that we use in the database.
func rpcOrderStateToDBState(state auctioneerrpc.OrderState) (order.State,