cmd/lncli: move commands and export

We want to export some of our CLI code to re-use in other projects. But
in Golang you cannot import code from a `main` package.
So we need to move the actual code into its own package and only have
the `func main()` in the `main` package.
This commit is contained in:
Oliver Gugger 2024-08-23 10:57:59 +02:00
parent 75477c8896
commit 22a3988222
No known key found for this signature in database
GPG Key ID: 8E4256593F177720
43 changed files with 841 additions and 674 deletions

View File

@ -259,8 +259,8 @@ issues:
- forbidigo
- godot
# Allow fmt.Printf() in lncli.
- path: cmd/lncli/*
# Allow fmt.Printf() in commands.
- path: cmd/commands/*
linters:
- forbidigo

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"regexp"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"testing"

View File

@ -1,7 +1,7 @@
//go:build autopilotrpc
// +build autopilotrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"

View File

@ -1,7 +1,7 @@
//go:build !autopilotrpc
// +build !autopilotrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build chainrpc
// +build chainrpc
package main
package commands
import (
"bytes"

View File

@ -1,7 +1,7 @@
//go:build !chainrpc
// +build !chainrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"
@ -9,7 +9,7 @@ import (
"github.com/urfave/cli"
)
var addInvoiceCommand = cli.Command{
var AddInvoiceCommand = cli.Command{
Name: "addinvoice",
Category: "Invoices",
Usage: "Add a new invoice.",

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"
@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
Config: mcCfg.Config,
},
)
return err
}
@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
req := &routerrpc.ResetMissionControlRequest{}
_, err := client.ResetMissionControl(ctxc, req)
return err
}

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"
@ -25,6 +25,7 @@ import (
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
const (
@ -152,8 +153,8 @@ var (
}
)
// paymentFlags returns common flags for sendpayment and payinvoice.
func paymentFlags() []cli.Flag {
// PaymentFlags returns common flags for sendpayment and payinvoice.
func PaymentFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "pay_req",
@ -202,7 +203,7 @@ func paymentFlags() []cli.Flag {
}
}
var sendPaymentCommand = cli.Command{
var SendPaymentCommand = cli.Command{
Name: "sendpayment",
Category: "Payments",
Usage: "Send a payment over lightning.",
@ -226,7 +227,7 @@ var sendPaymentCommand = cli.Command{
`,
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
"--pay_req=R [--pay_addr=H]",
Flags: append(paymentFlags(),
Flags: append(PaymentFlags(),
cli.StringFlag{
Name: "dest, d",
Usage: "the compressed identity pubkey of the " +
@ -253,7 +254,7 @@ var sendPaymentCommand = cli.Command{
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
},
),
Action: sendPayment,
Action: SendPayment,
}
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
@ -324,13 +325,16 @@ func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
return payAddr, nil
}
func sendPayment(ctx *cli.Context) error {
func SendPayment(ctx *cli.Context) error {
// Show command help if no arguments provided
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, "sendpayment")
return nil
}
conn := getClientConn(ctx, false)
defer conn.Close()
args := ctx.Args()
// If a payment request was provided, we can exit early since all of the
@ -357,7 +361,9 @@ func sendPayment(ctx *cli.Context) error {
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
return SendPaymentRequest(
ctx, req, conn, conn, routerRPCSendPayment,
)
}
var (
@ -466,19 +472,29 @@ func sendPayment(ctx *cli.Context) error {
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
}
func sendPaymentRequest(ctx *cli.Context,
req *routerrpc.SendPaymentRequest) error {
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
// router client.
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
// that uses the lnd routerrpc.SendPaymentV2 call.
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
}
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
lnConn, paymentConn grpc.ClientConnInterface,
callSendPayment SendPaymentFn) error {
ctxc := getContext()
conn := getClientConn(ctx, false)
defer conn.Close()
client := lnrpc.NewLightningClient(conn)
routerClient := routerrpc.NewRouterClient(conn)
lnClient := lnrpc.NewLightningClient(lnConn)
outChan := ctx.Int64Slice("outgoing_chan_id")
if len(outChan) != 0 {
@ -558,7 +574,7 @@ func sendPaymentRequest(ctx *cli.Context,
if req.PaymentRequest != "" {
// Decode payment request to find out the amount.
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
if err != nil {
return err
}
@ -602,14 +618,12 @@ func sendPaymentRequest(ctx *cli.Context,
printJSON := ctx.Bool(jsonFlag.Name)
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
stream, err := routerClient.SendPaymentV2(ctxc, req)
stream, err := callSendPayment(ctxc, paymentConn, req)
if err != nil {
return err
}
finalState, err := printLivePayment(
ctxc, stream, client, printJSON,
)
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
if err != nil {
return err
}
@ -667,24 +681,29 @@ func trackPayment(ctx *cli.Context) error {
}
client := lnrpc.NewLightningClient(conn)
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
return err
}
// printLivePayment receives payment updates from the given stream and either
// PaymentResultStream is an interface that abstracts the Recv method of the
// SendPaymentV2 or TrackPaymentV2 client stream.
type PaymentResultStream interface {
Recv() (*lnrpc.Payment, error)
}
// PrintLivePayment receives payment updates from the given stream and either
// outputs them as json or as a more user-friendly formatted table. The table
// option uses terminal control codes to rewrite the output. This call
// terminates when the payment reaches a final state.
func printLivePayment(ctxc context.Context,
stream routerrpc.Router_TrackPaymentV2Client,
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
// Terminal escape codes aren't supported on Windows, fall back to json.
if !json && runtime.GOOS == "windows" {
json = true
}
aliases := newAliasCache(client)
aliases := newAliasCache(lnClient)
first := true
var lastLineCount int
@ -706,17 +725,17 @@ func printLivePayment(ctxc context.Context,
// Write raw json to stdout.
printRespJSON(payment)
} else {
table := formatPayment(ctxc, payment, aliases)
resultTable := formatPayment(ctxc, payment, aliases)
// Clear all previously written lines and print the
// updated table.
clearLines(lastLineCount)
fmt.Print(table)
fmt.Print(resultTable)
// Store the number of lines written for the next update
// pass.
lastLineCount = 0
for _, b := range table {
for _, b := range resultTable {
if b == '\n' {
lastLineCount++
}
@ -874,7 +893,7 @@ var payInvoiceCommand = cli.Command{
This command is a shortcut for 'sendpayment --pay_req='.
`,
ArgsUsage: "pay_req",
Flags: append(paymentFlags(),
Flags: append(PaymentFlags(),
cli.Int64Flag{
Name: "amt",
Usage: "(optional) number of satoshis to fulfill the " +
@ -885,6 +904,9 @@ var payInvoiceCommand = cli.Command{
}
func payInvoice(ctx *cli.Context) error {
conn := getClientConn(ctx, false)
defer conn.Close()
args := ctx.Args()
var payReq string
@ -905,7 +927,7 @@ func payInvoice(ctx *cli.Context) error {
Cancelable: ctx.Bool(cancelableFlag.Name),
}
return sendPaymentRequest(ctx, req)
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
}
var sendToRouteCommand = cli.Command{

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"errors"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bufio"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bufio"
@ -11,6 +11,7 @@ import (
"io"
"math"
"os"
"regexp"
"strconv"
"strings"
"sync"
@ -41,8 +42,47 @@ const (
defaultUtxoMinConf = 1
)
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
"txid:index")
var (
errBadChanPoint = errors.New(
"expecting chan_point to be in format of: txid:index",
)
customDataPattern = regexp.MustCompile(
`"custom_channel_data":\s*"([0-9a-f]+)"`,
)
)
// replaceCustomData replaces the custom channel data hex string with the
// decoded custom channel data in the JSON response.
func replaceCustomData(jsonBytes []byte) ([]byte, error) {
// If there's nothing to replace, return the original JSON.
if !customDataPattern.Match(jsonBytes) {
return jsonBytes, nil
}
replacedBytes := customDataPattern.ReplaceAllFunc(
jsonBytes, func(match []byte) []byte {
encoded := customDataPattern.FindStringSubmatch(
string(match),
)[1]
decoded, err := hex.DecodeString(encoded)
if err != nil {
return match
}
return []byte("\"custom_channel_data\":" +
string(decoded))
},
)
var buf bytes.Buffer
err := json.Indent(&buf, replacedBytes, "", " ")
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getContext() context.Context {
shutdownInterceptor, err := signal.Intercept()
@ -66,9 +106,9 @@ func printJSON(resp interface{}) {
}
var out bytes.Buffer
json.Indent(&out, b, "", "\t")
out.WriteString("\n")
out.WriteTo(os.Stdout)
_ = json.Indent(&out, b, "", " ")
_, _ = out.WriteString("\n")
_, _ = out.WriteTo(os.Stdout)
}
func printRespJSON(resp proto.Message) {
@ -78,7 +118,13 @@ func printRespJSON(resp proto.Message) {
return
}
fmt.Printf("%s\n", jsonBytes)
jsonBytesReplaced, err := replaceCustomData(jsonBytes)
if err != nil {
fmt.Println("unable to replace custom data: ", err)
jsonBytesReplaced = jsonBytes
}
fmt.Printf("%s\n", jsonBytesReplaced)
}
// actionDecorator is used to add additional information and error handling
@ -1442,15 +1488,15 @@ func walletBalance(ctx *cli.Context) error {
return nil
}
var channelBalanceCommand = cli.Command{
var ChannelBalanceCommand = cli.Command{
Name: "channelbalance",
Category: "Channels",
Usage: "Returns the sum of the total available channel balance across " +
"all open channels.",
Action: actionDecorator(channelBalance),
Action: actionDecorator(ChannelBalance),
}
func channelBalance(ctx *cli.Context) error {
func ChannelBalance(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
@ -1575,7 +1621,7 @@ func pendingChannels(ctx *cli.Context) error {
return nil
}
var listChannelsCommand = cli.Command{
var ListChannelsCommand = cli.Command{
Name: "listchannels",
Category: "Channels",
Usage: "List all open channels.",
@ -1608,7 +1654,7 @@ var listChannelsCommand = cli.Command{
"order to improve performance",
},
},
Action: actionDecorator(listChannels),
Action: actionDecorator(ListChannels),
}
var listAliasesCommand = cli.Command{
@ -1616,10 +1662,10 @@ var listAliasesCommand = cli.Command{
Category: "Channels",
Usage: "List all aliases.",
Flags: []cli.Flag{},
Action: actionDecorator(listaliases),
Action: actionDecorator(listAliases),
}
func listaliases(ctx *cli.Context) error {
func listAliases(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()
@ -1636,7 +1682,7 @@ func listaliases(ctx *cli.Context) error {
return nil
}
func listChannels(ctx *cli.Context) error {
func ListChannels(ctx *cli.Context) error {
ctxc := getContext()
client, cleanUp := getClient(ctx)
defer cleanUp()

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"
@ -120,3 +120,82 @@ func TestParseTimeLockDelta(t *testing.T) {
}
}
}
// TestReplaceCustomData tests that hex encoded custom data can be formatted as
// JSON in the console output.
func TestReplaceCustomData(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data string
replaceData string
expected string
expectedErr string
}{
{
name: "no replacement necessary",
data: "foo",
expected: "foo",
},
{
name: "valid json with replacement",
data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" +
hex.EncodeToString([]byte(
"{\"bar\":\"baz\"}",
)) + "\"}",
expected: `{
"foo": "bar",
"custom_channel_data": {
"bar": "baz"
}
}`,
},
{
name: "valid json with replacement and space",
data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" +
hex.EncodeToString([]byte(
"{\"bar\":\"baz\"}",
)) + "\"}",
expected: `{
"foo": "bar",
"custom_channel_data": {
"bar": "baz"
}
}`,
},
{
name: "doesn't match pattern, returned identical",
data: "this ain't even json, and no custom data " +
"either",
expected: "this ain't even json, and no custom data " +
"either",
},
{
name: "invalid json",
data: "this ain't json, " +
"\"custom_channel_data\":\"a\"",
expectedErr: "invalid character 'h' in literal true",
},
{
name: "valid json, invalid hex, just formatted",
data: "{\"custom_channel_data\":\"f\"}",
expected: "{\n \"custom_channel_data\": \"f\"\n}",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := replaceCustomData([]byte(tc.data))
if tc.expectedErr != "" {
require.ErrorContains(t, err, tc.expectedErr)
return
}
require.NoError(t, err)
require.Equal(t, tc.expected, string(result))
})
}
}

View File

@ -1,7 +1,7 @@
//go:build dev
// +build dev
package main
package commands
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !dev
// +build !dev
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build invoicesrpc
// +build invoicesrpc
package main
package commands
import (
"encoding/hex"

View File

@ -1,7 +1,7 @@
//go:build !invoicesrpc
// +build !invoicesrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/base64"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

601
cmd/commands/main.go Normal file
View File

@ -0,0 +1,601 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// Copyright (C) 2015-2024 The Lightning Network Developers
package commands
import (
"context"
"crypto/tls"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/tor"
"github.com/urfave/cli"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
const (
defaultDataDir = "data"
defaultChainSubDir = "chain"
defaultTLSCertFilename = "tls.cert"
defaultMacaroonFilename = "admin.macaroon"
defaultRPCPort = "10009"
defaultRPCHostPort = "localhost:" + defaultRPCPort
envVarRPCServer = "LNCLI_RPCSERVER"
envVarLNDDir = "LNCLI_LNDDIR"
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
envVarChain = "LNCLI_CHAIN"
envVarNetwork = "LNCLI_NETWORK"
envVarMacaroonPath = "LNCLI_MACAROONPATH"
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
envVarMacaroonIP = "LNCLI_MACAROONIP"
envVarProfile = "LNCLI_PROFILE"
envVarMacFromJar = "LNCLI_MACFROMJAR"
)
var (
DefaultLndDir = btcutil.AppDataDir("lnd", false)
defaultTLSCertPath = filepath.Join(
DefaultLndDir, defaultTLSCertFilename,
)
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
)
func fatal(err error) {
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
os.Exit(1)
}
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient,
func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
}
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewStateClient(conn), cleanUp
}
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
conn := getClientConn(ctx, false)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewLightningClient(conn), cleanUp
}
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
// First, we'll get the selected stored profile or an ephemeral one
// created from the global options in the CLI context.
profile, err := getGlobalOptions(ctx, skipMacaroons)
if err != nil {
fatal(fmt.Errorf("could not load global options: %w", err))
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithUnaryInterceptor(
addMetadataUnaryInterceptor(profile.Metadata),
),
grpc.WithStreamInterceptor(
addMetaDataStreamInterceptor(profile.Metadata),
),
}
if profile.Insecure {
opts = append(opts, grpc.WithInsecure())
} else {
// Load the specified TLS certificate.
certPool, err := profile.cert()
if err != nil {
fatal(fmt.Errorf("could not create cert pool: %w", err))
}
// Build transport credentials from the certificate pool. If
// there is no certificate pool, we expect the server to use a
// non-self-signed certificate such as a certificate obtained
// from Let's Encrypt.
var creds credentials.TransportCredentials
if certPool != nil {
creds = credentials.NewClientTLSFromCert(certPool, "")
} else {
// Fallback to the system pool. Using an empty tls
// config is an alternative to x509.SystemCertPool().
// That call is not supported on Windows.
creds = credentials.NewTLS(&tls.Config{})
}
opts = append(opts, grpc.WithTransportCredentials(creds))
}
// Only process macaroon credentials if --no-macaroons isn't set and
// if we're not skipping macaroon processing.
if !profile.NoMacaroons && !skipMacaroons {
// Find out which macaroon to load.
macName := profile.Macaroons.Default
if ctx.GlobalIsSet("macfromjar") {
macName = ctx.GlobalString("macfromjar")
}
var macEntry *macaroonEntry
for _, entry := range profile.Macaroons.Jar {
if entry.Name == macName {
macEntry = entry
break
}
}
if macEntry == nil {
fatal(fmt.Errorf("macaroon with name '%s' not found "+
"in profile", macName))
}
// Get and possibly decrypt the specified macaroon.
//
// TODO(guggero): Make it possible to cache the password so we
// don't need to ask for it every time.
mac, err := macEntry.loadMacaroon(readPassword)
if err != nil {
fatal(fmt.Errorf("could not load macaroon: %w", err))
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of
// the macaroon. It's good for 60 seconds by default to
// make up for any discrepancy between client and server
// clocks, but leaking the macaroon before it becomes
// invalid makes it possible for an attacker to reuse
// the macaroon. In addition, the validity time of the
// macaroon is extended by the time the server clock is
// behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more
// than 60 seconds).
// TODO(aakselrod): add better anti-replay protection.
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
// Lock macaroon down to a specific IP address.
macaroons.IPLockConstraint(profile.Macaroons.IP),
// ... Add more constraints if needed.
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(
mac, macConstraints...,
)
if err != nil {
fatal(err)
}
// Now we append the macaroon credentials to the dial options.
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
fatal(fmt.Errorf("error cloning mac: %w", err))
}
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
// If a socksproxy server is specified we use a tor dialer
// to connect to the grpc server.
if ctx.GlobalIsSet("socksproxy") {
socksProxy := ctx.GlobalString("socksproxy")
torDialer := func(_ context.Context, addr string) (net.Conn,
error) {
return tor.Dial(
addr, socksProxy, false, false,
tor.DefaultConnTimeout,
)
}
opts = append(opts, grpc.WithContextDialer(torDialer))
} else {
// We need to use a custom dialer so we can also connect to
// unix sockets and not just TCP addresses.
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
opts = append(opts, grpc.WithContextDialer(genericDialer))
}
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
conn, err := grpc.Dial(profile.RPCServer, opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
}
return conn
}
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
// appends any key-value metadata strings to the outgoing context of a grpc
// unary call.
func addMetadataUnaryInterceptor(
md map[string]string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
opts ...grpc.CallOption) error {
outCtx := contextWithMetadata(ctx, md)
return invoker(outCtx, method, req, reply, cc, opts...)
}
}
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
// appends any key-value metadata strings to the outgoing context of a grpc
// stream call.
func addMetaDataStreamInterceptor(
md map[string]string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
outCtx := contextWithMetadata(ctx, md)
return streamer(outCtx, desc, cc, method, opts...)
}
}
// contextWithMetaData appends the given metadata key-value pairs to the given
// context.
func contextWithMetadata(ctx context.Context,
md map[string]string) context.Context {
kvPairs := make([]string, 0, 2*len(md))
for k, v := range md {
kvPairs = append(kvPairs, k, v)
}
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
}
// extractPathArgs parses the TLS certificate and macaroon paths from the
// command.
func extractPathArgs(ctx *cli.Context) (string, string, error) {
network := strings.ToLower(ctx.GlobalString("network"))
switch network {
case "mainnet", "testnet", "regtest", "simnet", "signet":
default:
return "", "", fmt.Errorf("unknown network: %v", network)
}
// We'll now fetch the lnddir so we can make a decision on how to
// properly read the macaroons (if needed) and also the cert. This will
// either be the default, or will have been overwritten by the end
// user.
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
// If the macaroon path as been manually provided, then we'll only
// target the specified file.
var macPath string
if ctx.GlobalString("macaroonpath") != "" {
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString(
"macaroonpath",
))
} else {
// Otherwise, we'll go into the path:
// lnddir/data/chain/<chain>/<network> in order to fetch the
// macaroon that we need.
macPath = filepath.Join(
lndDir, defaultDataDir, defaultChainSubDir,
lnd.BitcoinChainName, network, defaultMacaroonFilename,
)
}
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
// If a custom lnd directory was set, we'll also check if custom paths
// for the TLS cert and macaroon file were set as well. If not, we'll
// override their paths so they can be found within the custom lnd
// directory set. This allows us to set a custom lnd directory, along
// with custom paths to the TLS cert and macaroon file.
if lndDir != DefaultLndDir {
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
}
return tlsCertPath, macPath, nil
}
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
// or flag b can be set, but not both. It returns the name of the flag or an
// error.
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
if ctx.IsSet(a) && ctx.IsSet(b) {
return "", fmt.Errorf(
"either %s or %s should be set, but not both", a, b,
)
}
if ctx.IsSet(a) {
return a, nil
}
return b, nil
}
func Main() {
app := cli.NewApp()
app.Name = "lncli"
app.Version = build.Version() + " commit=" + build.Commit
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "rpcserver",
Value: defaultRPCHostPort,
Usage: "The host:port of LN daemon.",
EnvVar: envVarRPCServer,
},
cli.StringFlag{
Name: "lnddir",
Value: DefaultLndDir,
Usage: "The path to lnd's base directory.",
TakesFile: true,
EnvVar: envVarLNDDir,
},
cli.StringFlag{
Name: "socksproxy",
Usage: "The host:port of a SOCKS proxy through " +
"which all connections to the LN " +
"daemon will be established over.",
EnvVar: envVarSOCKSProxy,
},
cli.StringFlag{
Name: "tlscertpath",
Value: defaultTLSCertPath,
Usage: "The path to lnd's TLS certificate.",
TakesFile: true,
EnvVar: envVarTLSCertPath,
},
cli.StringFlag{
Name: "chain, c",
Usage: "The chain lnd is running on, e.g. bitcoin.",
Value: "bitcoin",
EnvVar: envVarChain,
},
cli.StringFlag{
Name: "network, n",
Usage: "The network lnd is running on, e.g. mainnet, " +
"testnet, etc.",
Value: "mainnet",
EnvVar: envVarNetwork,
},
cli.BoolFlag{
Name: "no-macaroons",
Usage: "Disable macaroon authentication.",
},
cli.StringFlag{
Name: "macaroonpath",
Usage: "The path to macaroon file.",
TakesFile: true,
EnvVar: envVarMacaroonPath,
},
cli.Int64Flag{
Name: "macaroontimeout",
Value: 60,
Usage: "Anti-replay macaroon validity time in " +
"seconds.",
EnvVar: envVarMacaroonTimeout,
},
cli.StringFlag{
Name: "macaroonip",
Usage: "If set, lock macaroon to specific IP address.",
EnvVar: envVarMacaroonIP,
},
cli.StringFlag{
Name: "profile, p",
Usage: "Instead of reading settings from command " +
"line parameters or using the default " +
"profile, use a specific profile. If " +
"a default profile is set, this flag can be " +
"set to an empty string to disable reading " +
"values from the profiles file.",
EnvVar: envVarProfile,
},
cli.StringFlag{
Name: "macfromjar",
Usage: "Use this macaroon from the profile's " +
"macaroon jar instead of the default one. " +
"Can only be used if profiles are defined.",
EnvVar: envVarMacFromJar,
},
cli.StringSliceFlag{
Name: "metadata",
Usage: "This flag can be used to specify a key-value " +
"pair that should be appended to the " +
"outgoing context before the request is sent " +
"to lnd. This flag may be specified multiple " +
"times. The format is: \"key:value\".",
},
cli.BoolFlag{
Name: "insecure",
Usage: "Connect to the rpc server without TLS " +
"authentication",
Hidden: true,
},
}
app.Commands = []cli.Command{
createCommand,
createWatchOnlyCommand,
unlockCommand,
changePasswordCommand,
newAddressCommand,
estimateFeeCommand,
sendManyCommand,
sendCoinsCommand,
listUnspentCommand,
connectCommand,
disconnectCommand,
openChannelCommand,
batchOpenChannelCommand,
closeChannelCommand,
closeAllChannelsCommand,
abandonChannelCommand,
listPeersCommand,
walletBalanceCommand,
ChannelBalanceCommand,
getInfoCommand,
getDebugInfoCommand,
encryptDebugPackageCommand,
decryptDebugPackageCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
SendPaymentCommand,
payInvoiceCommand,
sendToRouteCommand,
AddInvoiceCommand,
lookupInvoiceCommand,
listInvoicesCommand,
ListChannelsCommand,
closedChannelsCommand,
listPaymentsCommand,
describeGraphCommand,
getNodeMetricsCommand,
getChanInfoCommand,
getNodeInfoCommand,
queryRoutesCommand,
getNetworkInfoCommand,
debugLevelCommand,
decodePayReqCommand,
listChainTxnsCommand,
stopCommand,
signMessageCommand,
verifyMessageCommand,
feeReportCommand,
updateChannelPolicyCommand,
forwardingHistoryCommand,
exportChanBackupCommand,
verifyChanBackupCommand,
restoreChanBackupCommand,
bakeMacaroonCommand,
listMacaroonIDsCommand,
deleteMacaroonIDCommand,
listPermissionsCommand,
printMacaroonCommand,
constrainMacaroonCommand,
trackPaymentCommand,
versionCommand,
profileSubCommand,
getStateCommand,
deletePaymentsCommand,
sendCustomCommand,
subscribeCustomCommand,
fishCompletionCommand,
listAliasesCommand,
estimateRouteFeeCommand,
generateManPageCommand,
}
// Add any extra commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...)
app.Commands = append(app.Commands, invoicesCommands()...)
app.Commands = append(app.Commands, neutrinoCommands()...)
app.Commands = append(app.Commands, routerCommands()...)
app.Commands = append(app.Commands, walletCommands()...)
app.Commands = append(app.Commands, watchtowerCommands()...)
app.Commands = append(app.Commands, wtclientCommands()...)
app.Commands = append(app.Commands, devCommands()...)
app.Commands = append(app.Commands, peersCommands()...)
app.Commands = append(app.Commands, chainCommands()...)
if err := app.Run(os.Args); err != nil {
fatal(err)
}
}
// readPassword reads a password from the terminal. This requires there to be an
// actual TTY so passing in a password from stdin won't work.
func readPassword(text string) ([]byte, error) {
fmt.Print(text)
// The variable syscall.Stdin is of a different type in the Windows API
// that's why we need the explicit cast. And of course the linter
// doesn't like it either.
pw, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
fmt.Println()
return pw, err
}
// networkParams parses the global network flag into a chaincfg.Params.
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
network := strings.ToLower(ctx.GlobalString("network"))
switch network {
case "mainnet":
return &chaincfg.MainNetParams, nil
case "testnet":
return &chaincfg.TestNet3Params, nil
case "regtest":
return &chaincfg.RegressionNetParams, nil
case "simnet":
return &chaincfg.SimNetParams, nil
case "signet":
return &chaincfg.SigNetParams, nil
default:
return nil, fmt.Errorf("unknown network: %v", network)
}
}
// parseCoinSelectionStrategy parses a coin selection strategy string
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
func parseCoinSelectionStrategy(ctx *cli.Context) (
lnrpc.CoinSelectionStrategy, error) {
strategy := ctx.String(coinSelectionStrategyFlag.Name)
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
}
switch strategy {
case "global-config":
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
case "largest":
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
case "random":
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
default:
return 0, fmt.Errorf("unknown coin selection strategy "+
"%v", strategy)
}
}

View File

@ -1,7 +1,7 @@
//go:build neutrinorpc
// +build neutrinorpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"

View File

@ -1,7 +1,7 @@
//go:build !neutrinorpc
// +build !neutrinorpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,7 +1,7 @@
//go:build peersrpc
// +build peersrpc
package main
package commands
import (
"fmt"

View File

@ -1,7 +1,7 @@
//go:build !peersrpc
// +build !peersrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View File

@ -1,4 +1,4 @@
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -1,7 +1,7 @@
//go:build walletrpc
// +build walletrpc
package main
package commands
import (
"bytes"

View File

@ -1,7 +1,7 @@
//go:build !walletrpc
// +build !walletrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"

View File

@ -1,7 +1,7 @@
//go:build watchtowerrpc
// +build watchtowerrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"

View File

@ -1,7 +1,7 @@
//go:build !watchtowerrpc
// +build !watchtowerrpc
package main
package commands
import "github.com/urfave/cli"

View File

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View File

@ -1,594 +1,11 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// Copyright (C) 2015-2022 The Lightning Network Developers
// Copyright (C) 2015-2024 The Lightning Network Developers
package main
import (
"context"
"crypto/tls"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/tor"
"github.com/urfave/cli"
"golang.org/x/term"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
const (
defaultDataDir = "data"
defaultChainSubDir = "chain"
defaultTLSCertFilename = "tls.cert"
defaultMacaroonFilename = "admin.macaroon"
defaultRPCPort = "10009"
defaultRPCHostPort = "localhost:" + defaultRPCPort
envVarRPCServer = "LNCLI_RPCSERVER"
envVarLNDDir = "LNCLI_LNDDIR"
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
envVarChain = "LNCLI_CHAIN"
envVarNetwork = "LNCLI_NETWORK"
envVarMacaroonPath = "LNCLI_MACAROONPATH"
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
envVarMacaroonIP = "LNCLI_MACAROONIP"
envVarProfile = "LNCLI_PROFILE"
envVarMacFromJar = "LNCLI_MACFROMJAR"
)
var (
defaultLndDir = btcutil.AppDataDir("lnd", false)
defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
)
func fatal(err error) {
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
os.Exit(1)
}
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
}
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
conn := getClientConn(ctx, true)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewStateClient(conn), cleanUp
}
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
conn := getClientConn(ctx, false)
cleanUp := func() {
conn.Close()
}
return lnrpc.NewLightningClient(conn), cleanUp
}
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
// First, we'll get the selected stored profile or an ephemeral one
// created from the global options in the CLI context.
profile, err := getGlobalOptions(ctx, skipMacaroons)
if err != nil {
fatal(fmt.Errorf("could not load global options: %w", err))
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithUnaryInterceptor(
addMetadataUnaryInterceptor(profile.Metadata),
),
grpc.WithStreamInterceptor(
addMetaDataStreamInterceptor(profile.Metadata),
),
}
if profile.Insecure {
opts = append(opts, grpc.WithInsecure())
} else {
// Load the specified TLS certificate.
certPool, err := profile.cert()
if err != nil {
fatal(fmt.Errorf("could not create cert pool: %w", err))
}
// Build transport credentials from the certificate pool. If
// there is no certificate pool, we expect the server to use a
// non-self-signed certificate such as a certificate obtained
// from Let's Encrypt.
var creds credentials.TransportCredentials
if certPool != nil {
creds = credentials.NewClientTLSFromCert(certPool, "")
} else {
// Fallback to the system pool. Using an empty tls
// config is an alternative to x509.SystemCertPool().
// That call is not supported on Windows.
creds = credentials.NewTLS(&tls.Config{})
}
opts = append(opts, grpc.WithTransportCredentials(creds))
}
// Only process macaroon credentials if --no-macaroons isn't set and
// if we're not skipping macaroon processing.
if !profile.NoMacaroons && !skipMacaroons {
// Find out which macaroon to load.
macName := profile.Macaroons.Default
if ctx.GlobalIsSet("macfromjar") {
macName = ctx.GlobalString("macfromjar")
}
var macEntry *macaroonEntry
for _, entry := range profile.Macaroons.Jar {
if entry.Name == macName {
macEntry = entry
break
}
}
if macEntry == nil {
fatal(fmt.Errorf("macaroon with name '%s' not found "+
"in profile", macName))
}
// Get and possibly decrypt the specified macaroon.
//
// TODO(guggero): Make it possible to cache the password so we
// don't need to ask for it every time.
mac, err := macEntry.loadMacaroon(readPassword)
if err != nil {
fatal(fmt.Errorf("could not load macaroon: %w", err))
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of
// the macaroon. It's good for 60 seconds by default to
// make up for any discrepancy between client and server
// clocks, but leaking the macaroon before it becomes
// invalid makes it possible for an attacker to reuse
// the macaroon. In addition, the validity time of the
// macaroon is extended by the time the server clock is
// behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more
// than 60 seconds).
// TODO(aakselrod): add better anti-replay protection.
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
// Lock macaroon down to a specific IP address.
macaroons.IPLockConstraint(profile.Macaroons.IP),
// ... Add more constraints if needed.
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(
mac, macConstraints...,
)
if err != nil {
fatal(err)
}
// Now we append the macaroon credentials to the dial options.
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
fatal(fmt.Errorf("error cloning mac: %w", err))
}
opts = append(opts, grpc.WithPerRPCCredentials(cred))
}
// If a socksproxy server is specified we use a tor dialer
// to connect to the grpc server.
if ctx.GlobalIsSet("socksproxy") {
socksProxy := ctx.GlobalString("socksproxy")
torDialer := func(_ context.Context, addr string) (net.Conn,
error) {
return tor.Dial(
addr, socksProxy, false, false,
tor.DefaultConnTimeout,
)
}
opts = append(opts, grpc.WithContextDialer(torDialer))
} else {
// We need to use a custom dialer so we can also connect to
// unix sockets and not just TCP addresses.
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
opts = append(opts, grpc.WithContextDialer(genericDialer))
}
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
conn, err := grpc.Dial(profile.RPCServer, opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
}
return conn
}
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
// appends any key-value metadata strings to the outgoing context of a grpc
// unary call.
func addMetadataUnaryInterceptor(
md map[string]string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
opts ...grpc.CallOption) error {
outCtx := contextWithMetadata(ctx, md)
return invoker(outCtx, method, req, reply, cc, opts...)
}
}
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
// appends any key-value metadata strings to the outgoing context of a grpc
// stream call.
func addMetaDataStreamInterceptor(
md map[string]string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc,
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
opts ...grpc.CallOption) (grpc.ClientStream, error) {
outCtx := contextWithMetadata(ctx, md)
return streamer(outCtx, desc, cc, method, opts...)
}
}
// contextWithMetaData appends the given metadata key-value pairs to the given
// context.
func contextWithMetadata(ctx context.Context,
md map[string]string) context.Context {
kvPairs := make([]string, 0, 2*len(md))
for k, v := range md {
kvPairs = append(kvPairs, k, v)
}
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
}
// extractPathArgs parses the TLS certificate and macaroon paths from the
// command.
func extractPathArgs(ctx *cli.Context) (string, string, error) {
network := strings.ToLower(ctx.GlobalString("network"))
switch network {
case "mainnet", "testnet", "regtest", "simnet", "signet":
default:
return "", "", fmt.Errorf("unknown network: %v", network)
}
// We'll now fetch the lnddir so we can make a decision on how to
// properly read the macaroons (if needed) and also the cert. This will
// either be the default, or will have been overwritten by the end
// user.
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
// If the macaroon path as been manually provided, then we'll only
// target the specified file.
var macPath string
if ctx.GlobalString("macaroonpath") != "" {
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
} else {
// Otherwise, we'll go into the path:
// lnddir/data/chain/<chain>/<network> in order to fetch the
// macaroon that we need.
macPath = filepath.Join(
lndDir, defaultDataDir, defaultChainSubDir,
lnd.BitcoinChainName, network, defaultMacaroonFilename,
)
}
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
// If a custom lnd directory was set, we'll also check if custom paths
// for the TLS cert and macaroon file were set as well. If not, we'll
// override their paths so they can be found within the custom lnd
// directory set. This allows us to set a custom lnd directory, along
// with custom paths to the TLS cert and macaroon file.
if lndDir != defaultLndDir {
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
}
return tlsCertPath, macPath, nil
}
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
// or flag b can be set, but not both. It returns the name of the flag or an
// error.
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
if ctx.IsSet(a) && ctx.IsSet(b) {
return "", fmt.Errorf(
"either %s or %s should be set, but not both", a, b,
)
}
if ctx.IsSet(a) {
return a, nil
}
return b, nil
}
import "github.com/lightningnetwork/lnd/cmd/commands"
func main() {
app := cli.NewApp()
app.Name = "lncli"
app.Version = build.Version() + " commit=" + build.Commit
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "rpcserver",
Value: defaultRPCHostPort,
Usage: "The host:port of LN daemon.",
EnvVar: envVarRPCServer,
},
cli.StringFlag{
Name: "lnddir",
Value: defaultLndDir,
Usage: "The path to lnd's base directory.",
TakesFile: true,
EnvVar: envVarLNDDir,
},
cli.StringFlag{
Name: "socksproxy",
Usage: "The host:port of a SOCKS proxy through " +
"which all connections to the LN " +
"daemon will be established over.",
EnvVar: envVarSOCKSProxy,
},
cli.StringFlag{
Name: "tlscertpath",
Value: defaultTLSCertPath,
Usage: "The path to lnd's TLS certificate.",
TakesFile: true,
EnvVar: envVarTLSCertPath,
},
cli.StringFlag{
Name: "chain, c",
Usage: "The chain lnd is running on, e.g. bitcoin.",
Value: "bitcoin",
EnvVar: envVarChain,
},
cli.StringFlag{
Name: "network, n",
Usage: "The network lnd is running on, e.g. mainnet, " +
"testnet, etc.",
Value: "mainnet",
EnvVar: envVarNetwork,
},
cli.BoolFlag{
Name: "no-macaroons",
Usage: "Disable macaroon authentication.",
},
cli.StringFlag{
Name: "macaroonpath",
Usage: "The path to macaroon file.",
TakesFile: true,
EnvVar: envVarMacaroonPath,
},
cli.Int64Flag{
Name: "macaroontimeout",
Value: 60,
Usage: "Anti-replay macaroon validity time in " +
"seconds.",
EnvVar: envVarMacaroonTimeout,
},
cli.StringFlag{
Name: "macaroonip",
Usage: "If set, lock macaroon to specific IP address.",
EnvVar: envVarMacaroonIP,
},
cli.StringFlag{
Name: "profile, p",
Usage: "Instead of reading settings from command " +
"line parameters or using the default " +
"profile, use a specific profile. If " +
"a default profile is set, this flag can be " +
"set to an empty string to disable reading " +
"values from the profiles file.",
EnvVar: envVarProfile,
},
cli.StringFlag{
Name: "macfromjar",
Usage: "Use this macaroon from the profile's " +
"macaroon jar instead of the default one. " +
"Can only be used if profiles are defined.",
EnvVar: envVarMacFromJar,
},
cli.StringSliceFlag{
Name: "metadata",
Usage: "This flag can be used to specify a key-value " +
"pair that should be appended to the " +
"outgoing context before the request is sent " +
"to lnd. This flag may be specified multiple " +
"times. The format is: \"key:value\".",
},
cli.BoolFlag{
Name: "insecure",
Usage: "Connect to the rpc server without TLS " +
"authentication",
Hidden: true,
},
}
app.Commands = []cli.Command{
createCommand,
createWatchOnlyCommand,
unlockCommand,
changePasswordCommand,
newAddressCommand,
estimateFeeCommand,
sendManyCommand,
sendCoinsCommand,
listUnspentCommand,
connectCommand,
disconnectCommand,
openChannelCommand,
batchOpenChannelCommand,
closeChannelCommand,
closeAllChannelsCommand,
abandonChannelCommand,
listPeersCommand,
walletBalanceCommand,
channelBalanceCommand,
getInfoCommand,
getDebugInfoCommand,
encryptDebugPackageCommand,
decryptDebugPackageCommand,
getRecoveryInfoCommand,
pendingChannelsCommand,
sendPaymentCommand,
payInvoiceCommand,
sendToRouteCommand,
addInvoiceCommand,
lookupInvoiceCommand,
listInvoicesCommand,
listChannelsCommand,
closedChannelsCommand,
listPaymentsCommand,
describeGraphCommand,
getNodeMetricsCommand,
getChanInfoCommand,
getNodeInfoCommand,
queryRoutesCommand,
getNetworkInfoCommand,
debugLevelCommand,
decodePayReqCommand,
listChainTxnsCommand,
stopCommand,
signMessageCommand,
verifyMessageCommand,
feeReportCommand,
updateChannelPolicyCommand,
forwardingHistoryCommand,
exportChanBackupCommand,
verifyChanBackupCommand,
restoreChanBackupCommand,
bakeMacaroonCommand,
listMacaroonIDsCommand,
deleteMacaroonIDCommand,
listPermissionsCommand,
printMacaroonCommand,
constrainMacaroonCommand,
trackPaymentCommand,
versionCommand,
profileSubCommand,
getStateCommand,
deletePaymentsCommand,
sendCustomCommand,
subscribeCustomCommand,
fishCompletionCommand,
listAliasesCommand,
estimateRouteFeeCommand,
generateManPageCommand,
}
// Add any extra commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...)
app.Commands = append(app.Commands, invoicesCommands()...)
app.Commands = append(app.Commands, neutrinoCommands()...)
app.Commands = append(app.Commands, routerCommands()...)
app.Commands = append(app.Commands, walletCommands()...)
app.Commands = append(app.Commands, watchtowerCommands()...)
app.Commands = append(app.Commands, wtclientCommands()...)
app.Commands = append(app.Commands, devCommands()...)
app.Commands = append(app.Commands, peersCommands()...)
app.Commands = append(app.Commands, chainCommands()...)
if err := app.Run(os.Args); err != nil {
fatal(err)
}
}
// readPassword reads a password from the terminal. This requires there to be an
// actual TTY so passing in a password from stdin won't work.
func readPassword(text string) ([]byte, error) {
fmt.Print(text)
// The variable syscall.Stdin is of a different type in the Windows API
// that's why we need the explicit cast. And of course the linter
// doesn't like it either.
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
fmt.Println()
return pw, err
}
// networkParams parses the global network flag into a chaincfg.Params.
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
network := strings.ToLower(ctx.GlobalString("network"))
switch network {
case "mainnet":
return &chaincfg.MainNetParams, nil
case "testnet":
return &chaincfg.TestNet3Params, nil
case "regtest":
return &chaincfg.RegressionNetParams, nil
case "simnet":
return &chaincfg.SimNetParams, nil
case "signet":
return &chaincfg.SigNetParams, nil
default:
return nil, fmt.Errorf("unknown network: %v", network)
}
}
// parseCoinSelectionStrategy parses a coin selection strategy string
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
func parseCoinSelectionStrategy(ctx *cli.Context) (
lnrpc.CoinSelectionStrategy, error) {
strategy := ctx.String(coinSelectionStrategyFlag.Name)
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
}
switch strategy {
case "global-config":
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
nil
case "largest":
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
case "random":
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
default:
return 0, fmt.Errorf("unknown coin selection strategy "+
"%v", strategy)
}
commands.Main()
}