mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-04 12:09:27 +02:00
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:
parent
626a1b87fa
commit
26e759423b
@ -254,8 +254,8 @@ issues:
|
|||||||
- forbidigo
|
- forbidigo
|
||||||
- godot
|
- godot
|
||||||
|
|
||||||
# Allow fmt.Printf() in lncli.
|
# Allow fmt.Printf() in commands.
|
||||||
- path: cmd/lncli/*
|
- path: cmd/commands/*
|
||||||
linters:
|
linters:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build autopilotrpc
|
//go:build autopilotrpc
|
||||||
// +build autopilotrpc
|
// +build autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !autopilotrpc
|
//go:build !autopilotrpc
|
||||||
// +build !autopilotrpc
|
// +build !autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
//go:build chainrpc
|
//go:build chainrpc
|
||||||
// +build chainrpc
|
// +build chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !chainrpc
|
//go:build !chainrpc
|
||||||
// +build !chainrpc
|
// +build !chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var addInvoiceCommand = cli.Command{
|
var AddInvoiceCommand = cli.Command{
|
||||||
Name: "addinvoice",
|
Name: "addinvoice",
|
||||||
Category: "Invoices",
|
Category: "Invoices",
|
||||||
Usage: "Add a new invoice.",
|
Usage: "Add a new invoice.",
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
|
|||||||
Config: mcCfg.Config,
|
Config: mcCfg.Config,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req := &routerrpc.ResetMissionControlRequest{}
|
req := &routerrpc.ResetMissionControlRequest{}
|
||||||
_, err := client.ResetMissionControl(ctxc, req)
|
_, err := client.ResetMissionControl(ctxc, req)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -141,8 +141,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
// PaymentFlags returns common flags for sendpayment and payinvoice.
|
||||||
func paymentFlags() []cli.Flag {
|
func PaymentFlags() []cli.Flag {
|
||||||
return []cli.Flag{
|
return []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "pay_req",
|
Name: "pay_req",
|
||||||
@ -190,7 +190,7 @@ func paymentFlags() []cli.Flag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendPaymentCommand = cli.Command{
|
var SendPaymentCommand = cli.Command{
|
||||||
Name: "sendpayment",
|
Name: "sendpayment",
|
||||||
Category: "Payments",
|
Category: "Payments",
|
||||||
Usage: "Send a payment over lightning.",
|
Usage: "Send a payment over lightning.",
|
||||||
@ -214,7 +214,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
`,
|
`,
|
||||||
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
||||||
"--pay_req=R [--pay_addr=H]",
|
"--pay_req=R [--pay_addr=H]",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "dest, d",
|
Name: "dest, d",
|
||||||
Usage: "the compressed identity pubkey of the " +
|
Usage: "the compressed identity pubkey of the " +
|
||||||
@ -241,7 +241,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
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
|
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
||||||
@ -312,7 +312,7 @@ func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
|
|||||||
return payAddr, nil
|
return payAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPayment(ctx *cli.Context) error {
|
func SendPayment(ctx *cli.Context) error {
|
||||||
// Show command help if no arguments provided
|
// Show command help if no arguments provided
|
||||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||||
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
||||||
@ -343,7 +343,7 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -451,10 +451,10 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPaymentRequest(ctx *cli.Context,
|
func SendPaymentRequest(ctx *cli.Context,
|
||||||
req *routerrpc.SendPaymentRequest) error {
|
req *routerrpc.SendPaymentRequest) error {
|
||||||
|
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
@ -592,7 +592,7 @@ func sendPaymentRequest(ctx *cli.Context,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
finalState, err := printLivePayment(
|
finalState, err := PrintLivePayment(
|
||||||
ctxc, stream, client, printJSON,
|
ctxc, stream, client, printJSON,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -652,15 +652,15 @@ func trackPayment(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := lnrpc.NewLightningClient(conn)
|
client := lnrpc.NewLightningClient(conn)
|
||||||
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// printLivePayment receives payment updates from the given stream and either
|
// PrintLivePayment receives payment updates from the given stream and either
|
||||||
// outputs them as json or as a more user-friendly formatted table. The table
|
// 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
|
// option uses terminal control codes to rewrite the output. This call
|
||||||
// terminates when the payment reaches a final state.
|
// terminates when the payment reaches a final state.
|
||||||
func printLivePayment(ctxc context.Context,
|
func PrintLivePayment(ctxc context.Context,
|
||||||
stream routerrpc.Router_TrackPaymentV2Client,
|
stream routerrpc.Router_TrackPaymentV2Client,
|
||||||
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||||
|
|
||||||
@ -859,7 +859,7 @@ var payInvoiceCommand = cli.Command{
|
|||||||
This command is a shortcut for 'sendpayment --pay_req='.
|
This command is a shortcut for 'sendpayment --pay_req='.
|
||||||
`,
|
`,
|
||||||
ArgsUsage: "pay_req",
|
ArgsUsage: "pay_req",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "amt",
|
Name: "amt",
|
||||||
Usage: "(optional) number of satoshis to fulfill the " +
|
Usage: "(optional) number of satoshis to fulfill the " +
|
||||||
@ -888,7 +888,7 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendToRouteCommand = cli.Command{
|
var sendToRouteCommand = cli.Command{
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -42,8 +43,46 @@ const (
|
|||||||
defaultUtxoMinConf = 1
|
defaultUtxoMinConf = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
|
var (
|
||||||
"txid:index")
|
errBadChanPoint = errors.New(
|
||||||
|
"expecting chan_point to be in format of: txid:index",
|
||||||
|
)
|
||||||
|
|
||||||
|
customDataPattern = regexp.MustCompile(
|
||||||
|
`"custom_channel_data":\s*"([0-9a-z]+)"`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 customDataPattern.Match(jsonBytes) {
|
||||||
|
jsonBytes = 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, jsonBytes, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes = buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getContext() context.Context {
|
func getContext() context.Context {
|
||||||
shutdownInterceptor, err := signal.Intercept()
|
shutdownInterceptor, err := signal.Intercept()
|
||||||
@ -67,9 +106,9 @@ func printJSON(resp interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
json.Indent(&out, b, "", "\t")
|
_ = json.Indent(&out, b, "", " ")
|
||||||
out.WriteString("\n")
|
_, _ = out.WriteString("\n")
|
||||||
out.WriteTo(os.Stdout)
|
_, _ = out.WriteTo(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRespJSON(resp proto.Message) {
|
func printRespJSON(resp proto.Message) {
|
||||||
@ -79,7 +118,13 @@ func printRespJSON(resp proto.Message) {
|
|||||||
return
|
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
|
// actionDecorator is used to add additional information and error handling
|
||||||
@ -1420,15 +1465,15 @@ func walletBalance(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelBalanceCommand = cli.Command{
|
var ChannelBalanceCommand = cli.Command{
|
||||||
Name: "channelbalance",
|
Name: "channelbalance",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "Returns the sum of the total available channel balance across " +
|
Usage: "Returns the sum of the total available channel balance across " +
|
||||||
"all open channels.",
|
"all open channels.",
|
||||||
Action: actionDecorator(channelBalance),
|
Action: actionDecorator(ChannelBalance),
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelBalance(ctx *cli.Context) error {
|
func ChannelBalance(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@ -1553,7 +1598,7 @@ func pendingChannels(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var listChannelsCommand = cli.Command{
|
var ListChannelsCommand = cli.Command{
|
||||||
Name: "listchannels",
|
Name: "listchannels",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all open channels.",
|
Usage: "List all open channels.",
|
||||||
@ -1586,7 +1631,7 @@ var listChannelsCommand = cli.Command{
|
|||||||
"order to improve performance",
|
"order to improve performance",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: actionDecorator(listChannels),
|
Action: actionDecorator(ListChannels),
|
||||||
}
|
}
|
||||||
|
|
||||||
var listAliasesCommand = cli.Command{
|
var listAliasesCommand = cli.Command{
|
||||||
@ -1594,10 +1639,10 @@ var listAliasesCommand = cli.Command{
|
|||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all aliases.",
|
Usage: "List all aliases.",
|
||||||
Flags: []cli.Flag{},
|
Flags: []cli.Flag{},
|
||||||
Action: actionDecorator(listaliases),
|
Action: actionDecorator(listAliases),
|
||||||
}
|
}
|
||||||
|
|
||||||
func listaliases(ctx *cli.Context) error {
|
func listAliases(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@ -1614,7 +1659,7 @@ func listaliases(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listChannels(ctx *cli.Context) error {
|
func ListChannels(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -120,3 +120,57 @@ 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
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result, err := replaceCustomData([]byte(tc.data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expected, string(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
// +build dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
// +build !dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
//go:build invoicesrpc
|
//go:build invoicesrpc
|
||||||
// +build invoicesrpc
|
// +build invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !invoicesrpc
|
//go:build !invoicesrpc
|
||||||
// +build !invoicesrpc
|
// +build !invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
601
cmd/commands/main.go
Normal file
601
cmd/commands/main.go
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
// Copyright (c) 2013-2017 The btcsuite developers
|
||||||
|
// Copyright (c) 2015-2016 The Decred developers
|
||||||
|
// Copyright (C) 2015-2022 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
//go:build neutrinorpc
|
//go:build neutrinorpc
|
||||||
// +build neutrinorpc
|
// +build neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !neutrinorpc
|
//go:build !neutrinorpc
|
||||||
// +build !neutrinorpc
|
// +build !neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
//go:build peersrpc
|
//go:build peersrpc
|
||||||
// +build peersrpc
|
// +build peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !peersrpc
|
//go:build !peersrpc
|
||||||
// +build !peersrpc
|
// +build !peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build walletrpc
|
//go:build walletrpc
|
||||||
// +build walletrpc
|
// +build walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !walletrpc
|
//go:build !walletrpc
|
||||||
// +build !walletrpc
|
// +build !walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
//go:build watchtowerrpc
|
//go:build watchtowerrpc
|
||||||
// +build watchtowerrpc
|
// +build watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
@ -1,7 +1,7 @@
|
|||||||
//go:build !watchtowerrpc
|
//go:build !watchtowerrpc
|
||||||
// +build !watchtowerrpc
|
// +build !watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
@ -4,591 +4,8 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/lightningnetwork/lnd/cmd/commands"
|
||||||
"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() {
|
func main() {
|
||||||
app := cli.NewApp()
|
commands.Main()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user