lightning-terminal/cmd/litcli/accounts.go
ffranr a0e63124c0
multi: reformat long lines for readability
- Replace occurrences of `// nolint:lll` with `// nolint:ll` across
  files for consistency.
- Reformat multiline strings, comments, and function parameters to
  improve clarity and adhere to style guidelines.
- Add `// nolint:ll` comments where necessary to prevent linter
  warnings.
2025-12-09 16:12:03 +00:00

552 lines
12 KiB
Go

package main
import (
"encoding/hex"
"errors"
"fmt"
"os"
"strconv"
"github.com/lightninglabs/lightning-terminal/accounts"
"github.com/lightninglabs/lightning-terminal/litrpc"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/urfave/cli"
)
const (
idName = "id"
labelName = "label"
)
var accountsCommands = []cli.Command{
{
Name: "accounts",
ShortName: "a",
Usage: "Manage accounts",
Category: "Accounts",
Subcommands: []cli.Command{
createAccountCommand,
updateAccountCommand,
listAccountsCommand,
accountInfoCommand,
removeAccountCommand,
},
Description: "Manage accounts.",
},
}
var createAccountCommand = cli.Command{
Name: "create",
ShortName: "c",
Usage: "Create a new off-chain account with a balance.",
ArgsUsage: "balance [expiration_date] [--label=LABEL] [--save_to=FILE]",
Description: `Adds an entry to the account database.
This entry represents an amount of satoshis (account balance) that can be spent
using off-chain transactions (e.g. paying invoices).
Macaroons can be created to be locked to an account. This makes sure that the
bearer of the macaroon can only spend at most that amount of satoshis through
the daemon that has issued the macaroon.
Accounts only assert a maximum amount spendable. Having a certain account
balance does not guarantee that the node has the channel liquidity to actually
spend that amount.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "balance",
Usage: "The initial balance of the account.",
},
cli.Int64Flag{
Name: "expiration_date",
Usage: "The expiration date of the account expressed " +
"in seconds since the unix epoch. 0 means " +
"it does not expire.",
},
cli.StringFlag{
Name: "save_to",
Usage: "Store the account macaroon created for the " +
"account to the given file.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
},
Action: createAccount,
}
func createAccount(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
var (
initialBalance uint64
expirationDate int64
)
args := cli.Args()
switch {
case cli.IsSet("balance"):
initialBalance = cli.Uint64("balance")
case args.Present():
initialBalance, err = strconv.ParseUint(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode balance %v", err)
}
args = args.Tail()
}
switch {
case cli.IsSet("expiration_date"):
expirationDate = cli.Int64("expiration_date")
case args.Present():
expirationDate, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf(
"unable to decode expiration_date: %v", err,
)
}
args = args.Tail()
}
req := &litrpc.CreateAccountRequest{
AccountBalance: initialBalance,
ExpirationDate: expirationDate,
Label: cli.String(labelName),
}
resp, err := client.CreateAccount(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
// User requested to store the newly baked account macaroon to a file
// in addition to printing it to the console.
if cli.IsSet("save_to") {
fileName := lncfg.CleanAndExpandPath(cli.String("save_to"))
err := os.WriteFile(fileName, resp.Macaroon, 0644)
if err != nil {
return fmt.Errorf("error writing account macaroon "+
"to %s: %v", fileName, err)
}
fmt.Printf("Account macaroon saved to %s\n", fileName)
}
return nil
}
var updateAccountCommand = cli.Command{
Name: "update",
ShortName: "u",
Usage: "Update an existing off-chain account.",
ArgsUsage: "[id | label] new_balance [new_expiration_date] " +
"[--save_to=]",
Description: "Updates an existing off-chain account and sets " +
"either a new balance or new expiration date or both.",
Flags: []cli.Flag{
cli.StringFlag{
Name: idName,
Usage: "The ID of the account to update.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
cli.Int64Flag{
Name: "new_balance",
Usage: "(deprecated) The new balance of the account; " +
"-1 means do not update the balance.",
Value: -1,
Hidden: true,
},
cli.Int64Flag{
Name: "new_expiration_date",
Usage: "The new expiration date of the account " +
"expressed in seconds since the unix epoch; " +
"-1 means do not update the expiration date; " +
"0 means it does not expire.",
Value: -1,
},
},
Action: updateAccount,
Subcommands: []cli.Command{
creditCommand,
debitCommand,
},
}
func updateAccount(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
id, label, args, err := parseIDOrLabel(cli)
if err != nil {
return err
}
var (
newBalance int64
expirationDate int64
)
switch {
case cli.IsSet("new_balance"):
newBalance = cli.Int64("new_balance")
case args.Present():
newBalance, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode balance %v", err)
}
args = args.Tail()
}
switch {
case cli.IsSet("new_expiration_date"):
expirationDate = cli.Int64("new_expiration_date")
case args.Present():
expirationDate, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
return fmt.Errorf(
"unable to decode expiration_date: %v", err,
)
}
args = args.Tail()
}
req := &litrpc.UpdateAccountRequest{
Id: id,
Label: label,
AccountBalance: newBalance,
ExpirationDate: expirationDate,
}
resp, err := client.UpdateAccount(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var creditCommand = cli.Command{
Name: "credit",
ShortName: "c",
Usage: "Increase an account's balance by the given amount.",
ArgsUsage: "[id | label] amount",
Description: "Increases an existing off-chain account's balance by " +
"the given amount.",
Flags: []cli.Flag{
cli.StringFlag{
Name: idName,
Usage: "The ID of the account to credit.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
cli.Uint64Flag{
Name: "amount",
Usage: "The amount to credit the account.",
},
},
Action: creditBalance,
}
func creditBalance(cli *cli.Context) error {
return updateBalance(cli, true)
}
var debitCommand = cli.Command{
Name: "debit",
ShortName: "d",
Usage: "Decrease an account's balance by the given amount.",
ArgsUsage: "[id | label] amount",
Description: "Decreases an existing off-chain account's balance by " +
"the given amount.",
Flags: []cli.Flag{
cli.StringFlag{
Name: idName,
Usage: "The ID of the account to debit.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
cli.Uint64Flag{
Name: "amount",
Usage: "The amount to debit the account.",
},
},
Action: debitBalance,
}
func debitBalance(cli *cli.Context) error {
return updateBalance(cli, false)
}
func updateBalance(cli *cli.Context, add bool) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
account, args, err := parseAccountIdentifier(cli)
if err != nil {
return err
}
if (!cli.IsSet("amount") && len(args) != 1) ||
(cli.IsSet("amount") && len(args) != 0) {
return errors.New("invalid number of arguments")
}
var amount uint64
switch {
case cli.IsSet("amount"):
amount = cli.Uint64("amount")
case args.Present():
amount, err = strconv.ParseUint(args.First(), 10, 64)
if err != nil {
return fmt.Errorf("unable to decode amount %v", err)
}
args = args.Tail()
default:
return errors.New("must set a value for amount")
}
if add {
req := &litrpc.CreditAccountRequest{
Account: account,
Amount: amount,
}
resp, err := client.CreditAccount(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
} else {
req := &litrpc.DebitAccountRequest{
Account: account,
Amount: amount,
}
resp, err := client.DebitAccount(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
}
return nil
}
var listAccountsCommand = cli.Command{
Name: "list",
ShortName: "l",
Usage: "List all off-chain accounts.",
Description: "Returns all accounts that are currently stored in " +
"the account database.",
Action: listAccounts,
}
func listAccounts(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
req := &litrpc.ListAccountsRequest{}
resp, err := client.ListAccounts(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var accountInfoCommand = cli.Command{
Name: "info",
ShortName: "i",
Usage: "Show information about a single off-chain account.",
ArgsUsage: "[id | label]",
Description: "Returns a single account entry from the account " +
"database.",
Flags: []cli.Flag{
cli.StringFlag{
Name: idName,
Usage: "The ID of the account.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
},
Action: accountInfo,
}
func accountInfo(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
id, label, _, err := parseIDOrLabel(cli)
if err != nil {
return err
}
req := &litrpc.AccountInfoRequest{
Id: id,
Label: label,
}
resp, err := client.AccountInfo(ctx, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
var removeAccountCommand = cli.Command{
Name: "remove",
ShortName: "r",
Usage: "Remove an off-chain account from the database.",
ArgsUsage: "[id | label]",
Description: "Removes an account entry from the account database.",
Flags: []cli.Flag{
cli.StringFlag{
Name: idName,
Usage: "The ID of the account.",
},
cli.StringFlag{
Name: labelName,
Usage: "(optional) The unique label of the account.",
},
},
Action: removeAccount,
}
func removeAccount(cli *cli.Context) error {
ctx := getContext()
clientConn, cleanup, err := connectClient(cli, false)
if err != nil {
return err
}
defer cleanup()
client := litrpc.NewAccountsClient(clientConn)
id, label, _, err := parseIDOrLabel(cli)
if err != nil {
return err
}
req := &litrpc.RemoveAccountRequest{
Id: id,
Label: label,
}
_, err = client.RemoveAccount(ctx, req)
return err
}
// parseAccountIdentifier parses either the id or label from the command line,
// and returns the account identifier.
func parseAccountIdentifier(ctx *cli.Context) (
*litrpc.AccountIdentifier, cli.Args, error) {
id, label, args, err := parseIDOrLabel(ctx)
if err != nil {
return nil, nil, err
}
if id == "" && label == "" {
return nil, nil, fmt.Errorf("id argument missing")
}
if id != "" {
identifier := &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Id{
Id: id,
},
}
return identifier, args, nil
} else {
identifier := &litrpc.AccountIdentifier{
Identifier: &litrpc.AccountIdentifier_Label{
Label: label,
},
}
return identifier, args, nil
}
}
// parseIDOrLabel parses either the id or label from the command line.
func parseIDOrLabel(ctx *cli.Context) (string, string, cli.Args, error) {
var (
accountID string
label string
)
args := ctx.Args()
switch {
case ctx.IsSet(idName) && ctx.IsSet(labelName):
return "", "", nil, fmt.Errorf("either account ID or label " +
"must be specified, not both")
case ctx.IsSet(idName):
accountID = ctx.String(idName)
case ctx.IsSet(labelName):
label = ctx.String(labelName)
case args.Present():
accountID = args.First()
args = args.Tail()
// Since we have a positional argument, we cannot be sure it's
// an ID. So we check if it's an ID by trying to hex decode it
// and by checking the length. This will break if the user
// chooses labels that are also valid hex encoded IDs. But since
// the label is supposed to be human-readable, this should be
// unlikely.
_, err := hex.DecodeString(accountID)
if len(accountID) != hex.EncodedLen(accounts.AccountIDLen) ||
err != nil {
label = accountID
accountID = ""
}
default:
return "", "", nil, fmt.Errorf("id argument missing")
}
return accountID, label, args, nil
}