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:
43 files changed
+841
-674
No files matched your search
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// reTimeRange matches systemd.time-like short negative timeranges, e.g. "-200s".
|
||||
var reTimeRange = regexp.MustCompile(`^-\d{1,18}[s|m|h|d|w|M|y]$`)
|
||||
|
||||
// secondsPer allows translating s(seconds), m(minutes), h(ours), d(ays),
|
||||
// w(eeks), M(onths) and y(ears) into corresponding seconds.
|
||||
var secondsPer = map[string]int64{
|
||||
"s": 1,
|
||||
"m": 60,
|
||||
"h": 3600,
|
||||
"d": 86400,
|
||||
"w": 604800,
|
||||
"M": 2630016, // 30.44 days
|
||||
"y": 31557600, // 365.25 days
|
||||
}
|
||||
|
||||
// parseTime parses UNIX timestamps or short timeranges inspired by systemd
|
||||
// (when starting with "-"), e.g. "-1M" for one month (30.44 days) ago.
|
||||
func parseTime(s string, base time.Time) (uint64, error) {
|
||||
if reTimeRange.MatchString(s) {
|
||||
last := len(s) - 1
|
||||
|
||||
d, err := strconv.ParseInt(s[1:last], 10, 64)
|
||||
if err != nil {
|
||||
return uint64(0), err
|
||||
}
|
||||
|
||||
mul := secondsPer[string(s[last])]
|
||||
return uint64(base.Unix() - d*mul), nil
|
||||
}
|
||||
|
||||
return strconv.ParseUint(s, 10, 64)
|
||||
}
|
||||
|
||||
var lightningPrefix = "lightning:"
|
||||
|
||||
// stripPrefix removes accidentally copied 'lightning:' prefix.
|
||||
func stripPrefix(s string) string {
|
||||
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var now = time.Date(2017, 11, 10, 7, 8, 9, 1234, time.UTC)
|
||||
|
||||
var partTimeTests = []struct {
|
||||
in string
|
||||
expected uint64
|
||||
errExpected bool
|
||||
}{
|
||||
{
|
||||
"12345",
|
||||
uint64(12345),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-0s",
|
||||
uint64(now.Unix()),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-1s",
|
||||
uint64(time.Date(2017, 11, 10, 7, 8, 8, 1234, time.UTC).Unix()),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-2h",
|
||||
uint64(time.Date(2017, 11, 10, 5, 8, 9, 1234, time.UTC).Unix()),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-3d",
|
||||
uint64(time.Date(2017, 11, 7, 7, 8, 9, 1234, time.UTC).Unix()),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-4w",
|
||||
uint64(time.Date(2017, 10, 13, 7, 8, 9, 1234, time.UTC).Unix()),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-5M",
|
||||
uint64(now.Unix() - 30.44*5*24*60*60),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-6y",
|
||||
uint64(now.Unix() - 365.25*6*24*60*60),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-999999999999999999s",
|
||||
uint64(now.Unix() - 999999999999999999),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"-9999999999999999991s",
|
||||
0,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"-7z",
|
||||
0,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
// Test that parsing absolute and relative times works.
|
||||
func TestParseTime(t *testing.T) {
|
||||
for _, test := range partTimeTests {
|
||||
actual, err := parseTime(test.in, now)
|
||||
if test.errExpected == (err == nil) {
|
||||
t.Fatalf("unexpected error for %s:\n%v\n", test.in, err)
|
||||
}
|
||||
if actual != test.expected {
|
||||
t.Fatalf(
|
||||
"for %s actual and expected do not match:\n%d\n%d\n",
|
||||
test.in,
|
||||
actual,
|
||||
test.expected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stripPrefixTests = []struct {
|
||||
in string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"lightning:ln123",
|
||||
"ln123",
|
||||
},
|
||||
{
|
||||
"lightning: ln123",
|
||||
"ln123",
|
||||
},
|
||||
{
|
||||
"ln123",
|
||||
"ln123",
|
||||
},
|
||||
}
|
||||
|
||||
func TestStripPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range stripPrefixTests {
|
||||
actual := stripPrefix(test.in)
|
||||
require.Equal(t, test.expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
//go:build autopilotrpc
|
||||
// +build autopilotrpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func getAutopilotClient(ctx *cli.Context) (autopilotrpc.AutopilotClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return autopilotrpc.NewAutopilotClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var getStatusCommand = cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Get the active status of autopilot.",
|
||||
Description: "",
|
||||
Action: actionDecorator(getStatus),
|
||||
}
|
||||
|
||||
func getStatus(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getAutopilotClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &autopilotrpc.StatusRequest{}
|
||||
|
||||
resp, err := client.Status(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var enableCommand = cli.Command{
|
||||
Name: "enable",
|
||||
Usage: "Enable the autopilot.",
|
||||
Description: "",
|
||||
Action: actionDecorator(enable),
|
||||
}
|
||||
|
||||
var disableCommand = cli.Command{
|
||||
Name: "disable",
|
||||
Usage: "Disable the active autopilot.",
|
||||
Description: "",
|
||||
Action: actionDecorator(disable),
|
||||
}
|
||||
|
||||
func enable(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getAutopilotClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
// We will enable the autopilot.
|
||||
req := &autopilotrpc.ModifyStatusRequest{
|
||||
Enable: true,
|
||||
}
|
||||
|
||||
resp, err := client.ModifyStatus(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func disable(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getAutopilotClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
// We will disable the autopilot.
|
||||
req := &autopilotrpc.ModifyStatusRequest{
|
||||
Enable: false,
|
||||
}
|
||||
|
||||
resp, err := client.ModifyStatus(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var queryScoresCommand = cli.Command{
|
||||
Name: "query",
|
||||
Usage: "Query the autopilot heuristics for nodes' scores.",
|
||||
ArgsUsage: "[flags] <pubkey> <pubkey> <pubkey> ...",
|
||||
Description: "",
|
||||
Action: actionDecorator(queryScores),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ignorelocalstate, i",
|
||||
Usage: "Ignore local channel state when calculating " +
|
||||
"scores.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func queryScores(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getAutopilotClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
var pubs []string
|
||||
|
||||
// Keep reading pubkeys as long as there are arguments.
|
||||
loop:
|
||||
for {
|
||||
switch {
|
||||
case args.Present():
|
||||
pubs = append(pubs, args.First())
|
||||
args = args.Tail()
|
||||
default:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
req := &autopilotrpc.QueryScoresRequest{
|
||||
Pubkeys: pubs,
|
||||
IgnoreLocalState: ctx.Bool("ignorelocalstate"),
|
||||
}
|
||||
|
||||
resp, err := client.QueryScores(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// autopilotCommands will return the set of commands to enable for autopilotrpc
|
||||
// builds.
|
||||
func autopilotCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "autopilot",
|
||||
Category: "Autopilot",
|
||||
Usage: "Interact with a running autopilot.",
|
||||
Description: "",
|
||||
Subcommands: []cli.Command{
|
||||
getStatusCommand,
|
||||
enableCommand,
|
||||
disableCommand,
|
||||
queryScoresCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !autopilotrpc
|
||||
// +build !autopilotrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// autopilotCommands will return nil for non-autopilotrpc builds.
|
||||
func autopilotCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
//go:build chainrpc
|
||||
// +build chainrpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// chainCommands will return the set of commands to enable for chainrpc builds.
|
||||
func chainCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "chain",
|
||||
Category: "On-chain",
|
||||
Usage: "Interact with the bitcoin blockchain.",
|
||||
Subcommands: []cli.Command{
|
||||
getBlockCommand,
|
||||
getBestBlockCommand,
|
||||
getBlockHashCommand,
|
||||
getBlockHeaderCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getChainClient(ctx *cli.Context) (chainrpc.ChainKitClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return chainrpc.NewChainKitClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var getBlockCommand = cli.Command{
|
||||
Name: "getblock",
|
||||
Category: "On-chain",
|
||||
Usage: "Get block by block hash.",
|
||||
Description: "Returns a block given the corresponding block hash.",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "hash",
|
||||
Usage: "the target block hash",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "print entire block as JSON",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(getBlock),
|
||||
}
|
||||
|
||||
func getBlock(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
blockHashString string
|
||||
)
|
||||
|
||||
verbose := false
|
||||
if ctx.IsSet("verbose") {
|
||||
verbose = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("hash"):
|
||||
blockHashString = ctx.String("hash")
|
||||
|
||||
case args.Present():
|
||||
blockHashString = args.First()
|
||||
|
||||
default:
|
||||
return fmt.Errorf("hash argument missing")
|
||||
}
|
||||
|
||||
blockHash, err := chainhash.NewHashFromStr(blockHashString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, cleanUp := getChainClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &chainrpc.GetBlockRequest{BlockHash: blockHash.CloneBytes()}
|
||||
resp, err := client.GetBlock(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert raw block bytes into wire.MsgBlock.
|
||||
msgBlock := &wire.MsgBlock{}
|
||||
blockReader := bytes.NewReader(resp.RawBlock)
|
||||
err = msgBlock.Deserialize(blockReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
printJSON(msgBlock)
|
||||
} else {
|
||||
printJSON(msgBlock.Header)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getBlockHeaderCommand = cli.Command{
|
||||
Name: "getblockheader",
|
||||
Usage: "Get a block header.",
|
||||
Category: "On-chain",
|
||||
Description: "Returns a block header with a particular block hash.",
|
||||
ArgsUsage: "hash",
|
||||
Action: actionDecorator(getBlockHeader),
|
||||
}
|
||||
|
||||
func getBlockHeader(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
args := ctx.Args()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if !args.Present() {
|
||||
return cli.ShowCommandHelp(ctx, "getblockheader")
|
||||
}
|
||||
|
||||
blockHash, err := chainhash.NewHashFromStr(args.First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &chainrpc.GetBlockHeaderRequest{BlockHash: blockHash[:]}
|
||||
|
||||
client, cleanUp := getChainClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
resp, err := client.GetBlockHeader(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getBestBlockCommand = cli.Command{
|
||||
Name: "getbestblock",
|
||||
Category: "On-chain",
|
||||
Usage: "Get best block.",
|
||||
Description: "Returns the latest block hash and height from the " +
|
||||
"valid most-work chain.",
|
||||
Action: actionDecorator(getBestBlock),
|
||||
}
|
||||
|
||||
func getBestBlock(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
client, cleanUp := getChainClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
resp, err := client.GetBestBlock(ctxc, &chainrpc.GetBestBlockRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cast gRPC block hash bytes as chain hash type.
|
||||
var blockHash chainhash.Hash
|
||||
copy(blockHash[:], resp.BlockHash)
|
||||
|
||||
printJSON(struct {
|
||||
BlockHash chainhash.Hash `json:"block_hash"`
|
||||
BlockHeight int32 `json:"block_height"`
|
||||
}{
|
||||
BlockHash: blockHash,
|
||||
BlockHeight: resp.BlockHeight,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getBlockHashCommand = cli.Command{
|
||||
Name: "getblockhash",
|
||||
Category: "On-chain",
|
||||
Usage: "Get block hash by block height.",
|
||||
Description: "Returns the block hash from the best chain at a given " +
|
||||
"height.",
|
||||
Flags: []cli.Flag{
|
||||
cli.Int64Flag{
|
||||
Name: "height",
|
||||
Usage: "target block height",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(getBlockHash),
|
||||
}
|
||||
|
||||
func getBlockHash(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if ctx.NArg()+ctx.NumFlags() != 1 {
|
||||
return cli.ShowCommandHelp(ctx, "getblockhash")
|
||||
}
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
blockHeight int64
|
||||
)
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("height"):
|
||||
blockHeight = ctx.Int64("height")
|
||||
|
||||
case args.Present():
|
||||
blockHeightString := args.First()
|
||||
|
||||
// Convert block height positional argument from string to
|
||||
// int64.
|
||||
var err error
|
||||
blockHeight, err = strconv.ParseInt(blockHeightString, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("block height argument missing")
|
||||
}
|
||||
|
||||
client, cleanUp := getChainClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &chainrpc.GetBlockHashRequest{BlockHeight: blockHeight}
|
||||
resp, err := client.GetBlockHash(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cast gRPC block hash bytes as chain hash type.
|
||||
var blockHash chainhash.Hash
|
||||
copy(blockHash[:], resp.BlockHash)
|
||||
|
||||
printJSON(struct {
|
||||
BlockHash chainhash.Hash `json:"block_hash"`
|
||||
}{
|
||||
BlockHash: blockHash,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !chainrpc
|
||||
// +build !chainrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// chainCommands will return nil for non-chainrpc builds.
|
||||
func chainCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var sendCustomCommand = cli.Command{
|
||||
Name: "sendcustom",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "peer",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "type",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "data",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(sendCustom),
|
||||
}
|
||||
|
||||
func sendCustom(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
peer, err := hex.DecodeString(ctx.String("peer"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgType := ctx.Uint64("type")
|
||||
|
||||
data, err := hex.DecodeString(ctx.String("data"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.SendCustomMessage(
|
||||
ctxc,
|
||||
&lnrpc.SendCustomMessageRequest{
|
||||
Peer: peer,
|
||||
Type: uint32(msgType),
|
||||
Data: data,
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var subscribeCustomCommand = cli.Command{
|
||||
Name: "subscribecustom",
|
||||
Action: actionDecorator(subscribeCustom),
|
||||
}
|
||||
|
||||
func subscribeCustom(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
stream, err := client.SubscribeCustomMessages(
|
||||
ctxc,
|
||||
&lnrpc.SubscribeCustomMessagesRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Received from peer %x: type=%d, data=%x\n",
|
||||
msg.Peer, msg.Type, msg.Data)
|
||||
}
|
||||
}
|
||||
@@ -1,450 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/lightningnetwork/lnd"
|
||||
"github.com/lightningnetwork/lnd/lnencrypt"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var getDebugInfoCommand = cli.Command{
|
||||
Name: "getdebuginfo",
|
||||
Category: "Debug",
|
||||
Usage: "Returns debug information related to the active daemon.",
|
||||
Action: actionDecorator(getDebugInfo),
|
||||
}
|
||||
|
||||
func getDebugInfo(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.GetDebugInfoRequest{}
|
||||
resp, err := client.GetDebugInfo(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DebugPackage struct {
|
||||
EphemeralPubKey string `json:"ephemeral_public_key"`
|
||||
EncryptedPayload string `json:"encrypted_payload"`
|
||||
}
|
||||
|
||||
var encryptDebugPackageCommand = cli.Command{
|
||||
Name: "encryptdebugpackage",
|
||||
Category: "Debug",
|
||||
Usage: "Collects a package of debug information and encrypts it.",
|
||||
Description: `
|
||||
When requesting support with lnd, it's often required to submit a lot of
|
||||
debug information to the developer in order to track down a problem.
|
||||
This command will collect all the relevant information and encrypt it
|
||||
using the provided public key. The resulting file can then be sent to
|
||||
the developer for further analysis.
|
||||
Because the file is encrypted, it is safe to send it over insecure
|
||||
channels or upload it to a GitHub issue.
|
||||
|
||||
The file by default contains the output of the following commands:
|
||||
- lncli getinfo
|
||||
- lncli getdebuginfo
|
||||
- lncli getnetworkinfo
|
||||
|
||||
By specifying the following flags, additional information can be added
|
||||
to the file (usually this will be requested by the developer depending
|
||||
on the issue at hand):
|
||||
--peers:
|
||||
- lncli listpeers
|
||||
--onchain:
|
||||
- lncli listunspent
|
||||
- lncli listchaintxns
|
||||
--channels:
|
||||
- lncli listchannels
|
||||
- lncli pendingchannels
|
||||
- lncli closedchannels
|
||||
|
||||
Use 'lncli encryptdebugpackage 0xxxxxx... > package.txt' to write the
|
||||
encrypted package to a file called package.txt.
|
||||
`,
|
||||
ArgsUsage: "pubkey [--output_file F]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "pubkey",
|
||||
Usage: "the public key to encrypt the information " +
|
||||
"for (hex-encoded, e.g. 02aabb..), this " +
|
||||
"should be provided to you by the issue " +
|
||||
"tracker or developer you're requesting " +
|
||||
"support from",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "output_file",
|
||||
Usage: "(optional) the file to write the encrypted " +
|
||||
"package to; if not specified, the debug " +
|
||||
"package is printed to stdout",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "peers",
|
||||
Usage: "include information about connected peers " +
|
||||
"(lncli listpeers)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "onchain",
|
||||
Usage: "include information about on-chain " +
|
||||
"transactions (lncli listunspent, " +
|
||||
"lncli listchaintxns)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "channels",
|
||||
Usage: "include information about channels " +
|
||||
"(lncli listchannels, lncli pendingchannels, " +
|
||||
"lncli closedchannels)",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(encryptDebugPackage),
|
||||
}
|
||||
|
||||
func encryptDebugPackage(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "encryptdebugpackage")
|
||||
}
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
pubKeyBytes []byte
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("pubkey"):
|
||||
pubKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
||||
case args.Present():
|
||||
pubKeyBytes, err = hex.DecodeString(args.First())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode pubkey argument: %w", err)
|
||||
}
|
||||
|
||||
pubKey, err := btcec.ParsePubKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse pubkey: %w", err)
|
||||
}
|
||||
|
||||
// Collect the information we want to send from the daemon.
|
||||
payload, err := collectDebugPackageInfo(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to collect debug package "+
|
||||
"information: %w", err)
|
||||
}
|
||||
|
||||
// We've collected the information we want to send, but before
|
||||
// encrypting it, we want to compress it as much as possible to reduce
|
||||
// the size of the final payload.
|
||||
var (
|
||||
compressBuf bytes.Buffer
|
||||
options = brotli.WriterOptions{
|
||||
Quality: brotli.BestCompression,
|
||||
}
|
||||
writer = brotli.NewWriterOptions(&compressBuf, options)
|
||||
)
|
||||
_, err = writer.Write(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compress payload: %w", err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return fmt.Errorf("unable to compress payload: %w", err)
|
||||
}
|
||||
|
||||
// Now we have the full payload that we want to encrypt, so we'll create
|
||||
// an ephemeral keypair to encrypt the payload with.
|
||||
localKey, err := btcec.NewPrivateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate local key: %w", err)
|
||||
}
|
||||
|
||||
enc, err := lnencrypt.ECDHEncrypter(localKey, pubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create encrypter: %w", err)
|
||||
}
|
||||
|
||||
var cipherBuf bytes.Buffer
|
||||
err = enc.EncryptPayloadToWriter(compressBuf.Bytes(), &cipherBuf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encrypt payload: %w", err)
|
||||
}
|
||||
|
||||
response := DebugPackage{
|
||||
EphemeralPubKey: hex.EncodeToString(
|
||||
localKey.PubKey().SerializeCompressed(),
|
||||
),
|
||||
EncryptedPayload: hex.EncodeToString(
|
||||
cipherBuf.Bytes(),
|
||||
),
|
||||
}
|
||||
|
||||
// If the user specified an output file, we'll write the encrypted
|
||||
// payload to that file.
|
||||
if ctx.IsSet("output_file") {
|
||||
fileName := lnd.CleanAndExpandPath(ctx.String("output_file"))
|
||||
jsonBytes, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encode JSON: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(fileName, jsonBytes, 0644)
|
||||
}
|
||||
|
||||
// Finally, we'll print out the final payload as a JSON if no output
|
||||
// file was specified.
|
||||
printJSON(response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectDebugPackageInfo collects the information we want to send to the
|
||||
// developer(s) from the daemon.
|
||||
func collectDebugPackageInfo(ctx *cli.Context) ([]byte, error) {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
info, err := client.GetInfo(ctxc, &lnrpc.GetInfoRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting info: %w", err)
|
||||
}
|
||||
|
||||
debugInfo, err := client.GetDebugInfo(
|
||||
ctxc, &lnrpc.GetDebugInfoRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting debug info: %w", err)
|
||||
}
|
||||
|
||||
networkInfo, err := client.GetNetworkInfo(
|
||||
ctxc, &lnrpc.NetworkInfoRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting network info: %w", err)
|
||||
}
|
||||
|
||||
var payloadBuf bytes.Buffer
|
||||
addToBuf := func(msgs ...proto.Message) error {
|
||||
for _, msg := range msgs {
|
||||
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(
|
||||
msg,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error encoding response: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
payloadBuf.Write(jsonBytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := addToBuf(info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := addToBuf(debugInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := addToBuf(info, debugInfo, networkInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add optional information to the payload.
|
||||
if ctx.Bool("peers") {
|
||||
peers, err := client.ListPeers(ctxc, &lnrpc.ListPeersRequest{
|
||||
LatestError: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting peers: %w", err)
|
||||
}
|
||||
if err := addToBuf(peers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Bool("onchain") {
|
||||
unspent, err := client.ListUnspent(
|
||||
ctxc, &lnrpc.ListUnspentRequest{
|
||||
MaxConfs: math.MaxInt32,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting unspent: %w", err)
|
||||
}
|
||||
chainTxns, err := client.GetTransactions(
|
||||
ctxc, &lnrpc.GetTransactionsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting chain txns: %w",
|
||||
err)
|
||||
}
|
||||
if err := addToBuf(unspent, chainTxns); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Bool("channels") {
|
||||
channels, err := client.ListChannels(
|
||||
ctxc, &lnrpc.ListChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting channels: %w",
|
||||
err)
|
||||
}
|
||||
pendingChannels, err := client.PendingChannels(
|
||||
ctxc, &lnrpc.PendingChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting pending "+
|
||||
"channels: %w", err)
|
||||
}
|
||||
closedChannels, err := client.ClosedChannels(
|
||||
ctxc, &lnrpc.ClosedChannelsRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting closed "+
|
||||
"channels: %w", err)
|
||||
}
|
||||
if err := addToBuf(
|
||||
channels, pendingChannels, closedChannels,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return payloadBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
var decryptDebugPackageCommand = cli.Command{
|
||||
Name: "decryptdebugpackage",
|
||||
Category: "Debug",
|
||||
Usage: "Decrypts a package of debug information.",
|
||||
Description: `
|
||||
Decrypt a debug package that was created with the encryptdebugpackage
|
||||
command. Decryption requires the private key that corresponds to the
|
||||
public key the package was encrypted to.
|
||||
The command expects the encrypted package JSON to be provided on stdin.
|
||||
If decryption is successful, the information will be printed to stdout.
|
||||
|
||||
Use 'lncli decryptdebugpackage 0xxxxxx... < package.txt > decrypted.txt'
|
||||
to read the encrypted package from a file called package.txt and to
|
||||
write the decrypted content to a file called decrypted.txt.
|
||||
`,
|
||||
ArgsUsage: "privkey [--input_file F]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "privkey",
|
||||
Usage: "the hex encoded private key to decrypt the " +
|
||||
"debug package",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "input_file",
|
||||
Usage: "(optional) the file to read the encrypted " +
|
||||
"package from; if not specified, the debug " +
|
||||
"package is read from stdin",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(decryptDebugPackage),
|
||||
}
|
||||
|
||||
func decryptDebugPackage(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "decryptdebugpackage")
|
||||
}
|
||||
|
||||
var (
|
||||
args = ctx.Args()
|
||||
privKeyBytes []byte
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("pubkey"):
|
||||
privKeyBytes, err = hex.DecodeString(ctx.String("pubkey"))
|
||||
case args.Present():
|
||||
privKeyBytes, err = hex.DecodeString(args.First())
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode privkey argument: %w", err)
|
||||
}
|
||||
|
||||
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
||||
|
||||
// Read the file from stdin and decode the JSON into a DebugPackage.
|
||||
var pkg DebugPackage
|
||||
if ctx.IsSet("input_file") {
|
||||
fileName := lnd.CleanAndExpandPath(ctx.String("input_file"))
|
||||
jsonBytes, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read file '%s': %w",
|
||||
fileName, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(jsonBytes, &pkg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode JSON: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(os.Stdin).Decode(&pkg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode JSON: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the ephemeral public key and encrypted payload.
|
||||
ephemeralPubKeyBytes, err := hex.DecodeString(pkg.EphemeralPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode ephemeral public key: %w",
|
||||
err)
|
||||
}
|
||||
encryptedPayloadBytes, err := hex.DecodeString(pkg.EncryptedPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode encrypted payload: %w", err)
|
||||
}
|
||||
|
||||
// Parse the ephemeral public key and create an encrypter.
|
||||
ephemeralPubKey, err := btcec.ParsePubKey(ephemeralPubKeyBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse ephemeral public key: %w",
|
||||
err)
|
||||
}
|
||||
enc, err := lnencrypt.ECDHEncrypter(privKey, ephemeralPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create encrypter: %w", err)
|
||||
}
|
||||
|
||||
// Decrypt the payload.
|
||||
decryptedPayload, err := enc.DecryptPayloadFromReader(
|
||||
bytes.NewReader(encryptedPayloadBytes),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decrypt payload: %w", err)
|
||||
}
|
||||
|
||||
// Decompress the payload.
|
||||
reader := brotli.NewReader(bytes.NewBuffer(decryptedPayload))
|
||||
decompressedPayload, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decompress payload: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(decompressedPayload))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const argsStr = "[source node] [dest node] [unix ts seconds] [amount in msat]"
|
||||
|
||||
var importMissionControlCommand = cli.Command{
|
||||
Name: "importmc",
|
||||
Category: "Payments",
|
||||
Usage: "Import a result to the internal mission control state.",
|
||||
ArgsUsage: fmt.Sprintf("importmc %v", argsStr),
|
||||
Action: actionDecorator(importMissionControl),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "failure",
|
||||
Usage: "whether the routing history entry was a failure",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "whether to force the history entry import",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func importMissionControl(ctx *cli.Context) error {
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
if ctx.NArg() != 4 {
|
||||
return fmt.Errorf("please provide args: %v", argsStr)
|
||||
}
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
sourceNode, err := route.NewVertexFromStr(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("please provide valid source node: %w", err)
|
||||
}
|
||||
|
||||
destNode, err := route.NewVertexFromStr(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("please provide valid dest node: %w", err)
|
||||
}
|
||||
|
||||
ts, err := strconv.ParseInt(args[2], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("please provide unix timestamp "+
|
||||
"in seconds: %v", err)
|
||||
}
|
||||
|
||||
if ts <= 0 {
|
||||
return errors.New("please provide positive timestamp")
|
||||
}
|
||||
|
||||
amt, err := strconv.ParseInt(args[3], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("please provide amount in msat: %w", err)
|
||||
}
|
||||
|
||||
// Allow 0 value as failure amount.
|
||||
if !ctx.IsSet("failure") && amt <= 0 {
|
||||
return errors.New("success amount must be >0")
|
||||
}
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
importResult := &routerrpc.PairHistory{
|
||||
NodeFrom: sourceNode[:],
|
||||
NodeTo: destNode[:],
|
||||
History: &routerrpc.PairData{},
|
||||
}
|
||||
|
||||
if ctx.IsSet("failure") {
|
||||
importResult.History.FailAmtMsat = amt
|
||||
importResult.History.FailTime = ts
|
||||
} else {
|
||||
importResult.History.SuccessAmtMsat = amt
|
||||
importResult.History.SuccessTime = ts
|
||||
}
|
||||
|
||||
req := &routerrpc.XImportMissionControlRequest{
|
||||
Pairs: []*routerrpc.PairHistory{
|
||||
importResult,
|
||||
},
|
||||
Force: ctx.IsSet("force"),
|
||||
}
|
||||
|
||||
rpcCtx := context.Background()
|
||||
_, err = client.XImportMissionControl(rpcCtx, req)
|
||||
return err
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var addInvoiceCommand = cli.Command{
|
||||
Name: "addinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Add a new invoice.",
|
||||
Description: `
|
||||
Add a new invoice, expressing intent for a future payment.
|
||||
|
||||
Invoices without an amount can be created by not supplying any
|
||||
parameters or providing an amount of 0. These invoices allow the payer
|
||||
to specify the amount of satoshis they wish to send.`,
|
||||
ArgsUsage: "value preimage",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "memo",
|
||||
Usage: "a description of the payment to attach along " +
|
||||
"with the invoice (default=\"\")",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "preimage",
|
||||
Usage: "the hex-encoded preimage (32 byte) which will " +
|
||||
"allow settling an incoming HTLC payable to this " +
|
||||
"preimage. If not set, a random preimage will be " +
|
||||
"created.",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "amt",
|
||||
Usage: "the amt of satoshis in this invoice",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "amt_msat",
|
||||
Usage: "the amt of millisatoshis in this invoice",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "description_hash",
|
||||
Usage: "SHA-256 hash of the description of the payment. " +
|
||||
"Used if the purpose of payment cannot naturally " +
|
||||
"fit within the memo. If provided this will be " +
|
||||
"used instead of the description(memo) field in " +
|
||||
"the encoded invoice.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "fallback_addr",
|
||||
Usage: "fallback on-chain address that can be used in " +
|
||||
"case the lightning payment fails",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "expiry",
|
||||
Usage: "the invoice's expiry time in seconds. If not " +
|
||||
"specified, an expiry of " +
|
||||
"86400 seconds (24 hours) is implied.",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "cltv_expiry_delta",
|
||||
Usage: "The minimum CLTV delta to use for the final " +
|
||||
"hop. If this is set to 0, the default value " +
|
||||
"is used. The default value for " +
|
||||
"cltv_expiry_delta is configured by the " +
|
||||
"'bitcoin.timelockdelta' option.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "private",
|
||||
Usage: "encode routing hints in the invoice with " +
|
||||
"private channels in order to assist the " +
|
||||
"payer in reaching you. If amt and amt_msat " +
|
||||
"are zero, a large number of hints with " +
|
||||
"these channels can be included, which " +
|
||||
"might not be desirable.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "amp",
|
||||
Usage: "creates an AMP invoice. If true, preimage " +
|
||||
"should not be set.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "blind",
|
||||
Usage: "creates an invoice that contains blinded " +
|
||||
"paths. Note that invoices with blinded " +
|
||||
"paths will be signed using a random " +
|
||||
"ephemeral key so as not to reveal the real " +
|
||||
"node ID of this node.",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "min_real_blinded_hops",
|
||||
Usage: "The minimum number of real hops to use in a " +
|
||||
"blinded path. This option will only be used " +
|
||||
"if `--blind` has also been set.",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "num_blinded_hops",
|
||||
Usage: "The number of hops to use for each " +
|
||||
"blinded path included in the invoice. This " +
|
||||
"option will only be used if `--blind` has " +
|
||||
"also been set. Dummy hops will be used to " +
|
||||
"pad paths shorter than this.",
|
||||
},
|
||||
cli.UintFlag{
|
||||
Name: "max_blinded_paths",
|
||||
Usage: "The maximum number of blinded paths to add " +
|
||||
"to an invoice. This option will only be " +
|
||||
"used if `--blind` has also been set.",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "blinded_path_omit_node",
|
||||
Usage: "The pub key (in hex) of a node not to " +
|
||||
"use on a blinded path. The flag may be " +
|
||||
"specified multiple times.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(addInvoice),
|
||||
}
|
||||
|
||||
func addInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
preimage []byte
|
||||
descHash []byte
|
||||
amt int64
|
||||
amtMsat int64
|
||||
err error
|
||||
)
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
amt = ctx.Int64("amt")
|
||||
amtMsat = ctx.Int64("amt_msat")
|
||||
if !ctx.IsSet("amt") && !ctx.IsSet("amt_msat") && args.Present() {
|
||||
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
||||
args = args.Tail()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode amt argument: %w",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("preimage"):
|
||||
preimage, err = hex.DecodeString(ctx.String("preimage"))
|
||||
case args.Present():
|
||||
preimage, err = hex.DecodeString(args.First())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse preimage: %w", err)
|
||||
}
|
||||
|
||||
descHash, err = hex.DecodeString(ctx.String("description_hash"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse description_hash: %w", err)
|
||||
}
|
||||
|
||||
if ctx.IsSet("private") && ctx.IsSet("blind") {
|
||||
return fmt.Errorf("cannot include both route hints and " +
|
||||
"blinded paths in the same invoice")
|
||||
}
|
||||
|
||||
blindedPathCfg, err := parseBlindedPathCfg(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse blinded path config: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
invoice := &lnrpc.Invoice{
|
||||
Memo: ctx.String("memo"),
|
||||
RPreimage: preimage,
|
||||
Value: amt,
|
||||
ValueMsat: amtMsat,
|
||||
DescriptionHash: descHash,
|
||||
FallbackAddr: ctx.String("fallback_addr"),
|
||||
Expiry: ctx.Int64("expiry"),
|
||||
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
|
||||
Private: ctx.Bool("private"),
|
||||
IsAmp: ctx.Bool("amp"),
|
||||
IsBlinded: ctx.Bool("blind"),
|
||||
BlindedPathConfig: blindedPathCfg,
|
||||
}
|
||||
|
||||
resp, err := client.AddInvoice(ctxc, invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBlindedPathCfg(ctx *cli.Context) (*lnrpc.BlindedPathConfig, error) {
|
||||
if !ctx.Bool("blind") {
|
||||
if ctx.IsSet("min_real_blinded_hops") ||
|
||||
ctx.IsSet("num_blinded_hops") ||
|
||||
ctx.IsSet("max_blinded_paths") ||
|
||||
ctx.IsSet("blinded_path_omit_node") {
|
||||
|
||||
return nil, fmt.Errorf("blinded path options are " +
|
||||
"only used if the `--blind` options is set")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var blindCfg lnrpc.BlindedPathConfig
|
||||
|
||||
if ctx.IsSet("min_real_blinded_hops") {
|
||||
minNumRealHops := uint32(ctx.Uint("min_real_blinded_hops"))
|
||||
blindCfg.MinNumRealHops = &minNumRealHops
|
||||
}
|
||||
|
||||
if ctx.IsSet("num_blinded_hops") {
|
||||
numHops := uint32(ctx.Uint("num_blinded_hops"))
|
||||
blindCfg.NumHops = &numHops
|
||||
}
|
||||
|
||||
if ctx.IsSet("max_blinded_paths") {
|
||||
maxPaths := uint32(ctx.Uint("max_blinded_paths"))
|
||||
blindCfg.MaxNumPaths = &maxPaths
|
||||
}
|
||||
|
||||
for _, pubKey := range ctx.StringSlice("blinded_path_omit_node") {
|
||||
pubKeyBytes, err := hex.DecodeString(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blindCfg.NodeOmissionList = append(
|
||||
blindCfg.NodeOmissionList, pubKeyBytes,
|
||||
)
|
||||
}
|
||||
|
||||
return &blindCfg, nil
|
||||
}
|
||||
|
||||
var lookupInvoiceCommand = cli.Command{
|
||||
Name: "lookupinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Lookup an existing invoice by its payment hash.",
|
||||
ArgsUsage: "rhash",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "rhash",
|
||||
Usage: "the 32 byte payment hash of the invoice to query for, the hash " +
|
||||
"should be a hex-encoded string",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(lookupInvoice),
|
||||
}
|
||||
|
||||
func lookupInvoice(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
var (
|
||||
rHash []byte
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("rhash"):
|
||||
rHash, err = hex.DecodeString(ctx.String("rhash"))
|
||||
case ctx.Args().Present():
|
||||
rHash, err = hex.DecodeString(ctx.Args().First())
|
||||
default:
|
||||
return fmt.Errorf("rhash argument missing")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode rhash argument: %w", err)
|
||||
}
|
||||
|
||||
req := &lnrpc.PaymentHash{
|
||||
RHash: rHash,
|
||||
}
|
||||
|
||||
invoice, err := client.LookupInvoice(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(invoice)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var listInvoicesCommand = cli.Command{
|
||||
Name: "listinvoices",
|
||||
Category: "Invoices",
|
||||
Usage: "List all invoices currently stored within the database. Any " +
|
||||
"active debug invoices are ignored.",
|
||||
Description: `
|
||||
This command enables the retrieval of all invoices currently stored
|
||||
within the database. It has full support for paginationed responses,
|
||||
allowing users to query for specific invoices through their add_index.
|
||||
This can be done by using either the first_index_offset or
|
||||
last_index_offset fields included in the response as the index_offset of
|
||||
the next request. Backward pagination is enabled by default to receive
|
||||
current invoices first. If you wish to paginate forwards, set the
|
||||
paginate-forwards flag. If none of the parameters are specified, then
|
||||
the last 100 invoices will be returned.
|
||||
|
||||
For example: if you have 200 invoices, "lncli listinvoices" will return
|
||||
the last 100 created. If you wish to retrieve the previous 100, the
|
||||
first_offset_index of the response can be used as the index_offset of
|
||||
the next listinvoices request.`,
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "pending_only",
|
||||
Usage: "toggles if all invoices should be returned, " +
|
||||
"or only those that are currently unsettled",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "index_offset",
|
||||
Usage: "the index of an invoice that will be used as " +
|
||||
"either the start or end of a query to " +
|
||||
"determine which invoices should be returned " +
|
||||
"in the response",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "max_invoices",
|
||||
Usage: "the max number of invoices to return",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "paginate-forwards",
|
||||
Usage: "if set, invoices succeeding the " +
|
||||
"index_offset will be returned",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "creation_date_start",
|
||||
Usage: "timestamp in seconds, if set, filter " +
|
||||
"invoices with creation date greater than or " +
|
||||
"equal to it",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "creation_date_end",
|
||||
Usage: "timestamp in seconds, if set, filter " +
|
||||
"invoices with creation date less than or " +
|
||||
"equal to it",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(listInvoices),
|
||||
}
|
||||
|
||||
func listInvoices(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.ListInvoiceRequest{
|
||||
PendingOnly: ctx.Bool("pending_only"),
|
||||
IndexOffset: ctx.Uint64("index_offset"),
|
||||
NumMaxInvoices: ctx.Uint64("max_invoices"),
|
||||
Reversed: !ctx.Bool("paginate-forwards"),
|
||||
CreationDateStart: ctx.Uint64("creation_date_start"),
|
||||
CreationDateEnd: ctx.Uint64("creation_date_end"),
|
||||
}
|
||||
|
||||
invoices, err := client.ListInvoices(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(invoices)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var decodePayReqCommand = cli.Command{
|
||||
Name: "decodepayreq",
|
||||
Category: "Invoices",
|
||||
Usage: "Decode a payment request.",
|
||||
Description: "Decode the passed payment request revealing the destination, payment hash and value of the payment request",
|
||||
ArgsUsage: "pay_req",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "pay_req",
|
||||
Usage: "the bech32 encoded payment request",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(decodePayReq),
|
||||
}
|
||||
|
||||
func decodePayReq(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
var payreq string
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("pay_req"):
|
||||
payreq = ctx.String("pay_req")
|
||||
case ctx.Args().Present():
|
||||
payreq = ctx.Args().First()
|
||||
default:
|
||||
return fmt.Errorf("pay_req argument missing")
|
||||
}
|
||||
|
||||
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
||||
PayReq: stripPrefix(payreq),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
macTimeoutFlag = cli.Uint64Flag{
|
||||
Name: "timeout",
|
||||
Usage: "the number of seconds the macaroon will be " +
|
||||
"valid before it times out",
|
||||
}
|
||||
macIPAddressFlag = cli.StringFlag{
|
||||
Name: "ip_address",
|
||||
Usage: "the IP address the macaroon will be bound to",
|
||||
}
|
||||
macCustomCaveatNameFlag = cli.StringFlag{
|
||||
Name: "custom_caveat_name",
|
||||
Usage: "the name of the custom caveat to add",
|
||||
}
|
||||
macCustomCaveatConditionFlag = cli.StringFlag{
|
||||
Name: "custom_caveat_condition",
|
||||
Usage: "the condition of the custom caveat to add, can be " +
|
||||
"empty if custom caveat doesn't need a value",
|
||||
}
|
||||
)
|
||||
|
||||
var bakeMacaroonCommand = cli.Command{
|
||||
Name: "bakemacaroon",
|
||||
Category: "Macaroons",
|
||||
Usage: "Bakes a new macaroon with the provided list of permissions " +
|
||||
"and restrictions.",
|
||||
ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] " +
|
||||
"[--custom_caveat_name= [--custom_caveat_condition=]] " +
|
||||
"[--root_key_id=] [--allow_external_permissions] " +
|
||||
"permissions...",
|
||||
Description: `
|
||||
Bake a new macaroon that grants the provided permissions and
|
||||
optionally adds restrictions (timeout, IP address) to it.
|
||||
|
||||
The new macaroon can either be shown on command line in hex serialized
|
||||
format or it can be saved directly to a file using the --save_to
|
||||
argument.
|
||||
|
||||
A permission is a tuple of an entity and an action, separated by a
|
||||
colon. Multiple operations can be added as arguments, for example:
|
||||
|
||||
lncli bakemacaroon info:read invoices:write foo:bar
|
||||
|
||||
For even more fine-grained permission control, it is also possible to
|
||||
specify single RPC method URIs that are allowed to be accessed by a
|
||||
macaroon. This can be achieved by specifying "uri:<methodURI>" pairs,
|
||||
for example:
|
||||
|
||||
lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion
|
||||
|
||||
The macaroon created by this command would only be allowed to use the
|
||||
"lncli getinfo" and "lncli version" commands.
|
||||
|
||||
To get a list of all available URIs and permissions, use the
|
||||
"lncli listpermissions" command.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "save_to",
|
||||
Usage: "save the created macaroon to this file " +
|
||||
"using the default binary format",
|
||||
},
|
||||
macTimeoutFlag,
|
||||
macIPAddressFlag,
|
||||
macCustomCaveatNameFlag,
|
||||
macCustomCaveatConditionFlag,
|
||||
cli.Uint64Flag{
|
||||
Name: "root_key_id",
|
||||
Usage: "the numerical root key ID used to create the " +
|
||||
"macaroon",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow_external_permissions",
|
||||
Usage: "whether permissions lnd is not familiar with " +
|
||||
"are allowed",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(bakeMacaroon),
|
||||
}
|
||||
|
||||
func bakeMacaroon(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
// Show command help if no arguments.
|
||||
if ctx.NArg() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "bakemacaroon")
|
||||
}
|
||||
args := ctx.Args()
|
||||
|
||||
var (
|
||||
savePath string
|
||||
rootKeyID uint64
|
||||
parsedPermissions []*lnrpc.MacaroonPermission
|
||||
err error
|
||||
)
|
||||
|
||||
if ctx.String("save_to") != "" {
|
||||
savePath = lncfg.CleanAndExpandPath(ctx.String("save_to"))
|
||||
}
|
||||
|
||||
if ctx.IsSet("root_key_id") {
|
||||
rootKeyID = ctx.Uint64("root_key_id")
|
||||
}
|
||||
|
||||
// A command line argument can't be an empty string. So we'll check each
|
||||
// entry if it's a valid entity:action tuple. The content itself is
|
||||
// validated server side. We just make sure we can parse it correctly.
|
||||
for _, permission := range args {
|
||||
tuple := strings.Split(permission, ":")
|
||||
if len(tuple) != 2 {
|
||||
return fmt.Errorf("unable to parse "+
|
||||
"permission tuple: %s", permission)
|
||||
}
|
||||
entity, action := tuple[0], tuple[1]
|
||||
if entity == "" {
|
||||
return fmt.Errorf("invalid permission [%s]. entity "+
|
||||
"cannot be empty", permission)
|
||||
}
|
||||
if action == "" {
|
||||
return fmt.Errorf("invalid permission [%s]. action "+
|
||||
"cannot be empty", permission)
|
||||
}
|
||||
|
||||
// No we can assume that we have a formally valid entity:action
|
||||
// tuple. The rest of the validation happens server side.
|
||||
parsedPermissions = append(
|
||||
parsedPermissions, &lnrpc.MacaroonPermission{
|
||||
Entity: entity,
|
||||
Action: action,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Now we have gathered all the input we need and can do the actual
|
||||
// RPC call.
|
||||
req := &lnrpc.BakeMacaroonRequest{
|
||||
Permissions: parsedPermissions,
|
||||
RootKeyId: rootKeyID,
|
||||
AllowExternalPermissions: ctx.Bool("allow_external_permissions"),
|
||||
}
|
||||
resp, err := client.BakeMacaroon(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now we should have gotten a valid macaroon. Unmarshal it so we can
|
||||
// add first-party caveats (if necessary) to it.
|
||||
macBytes, err := hex.DecodeString(resp.Macaroon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unmarshalMac := &macaroon.Macaroon{}
|
||||
if err = unmarshalMac.UnmarshalBinary(macBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now apply the desired constraints to the macaroon. This will always
|
||||
// create a new macaroon object, even if no constraints are added.
|
||||
constrainedMac, err := applyMacaroonConstraints(ctx, unmarshalMac)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
macBytes, err = constrainedMac.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now we can output the result. We either write it binary serialized to
|
||||
// a file or write to the standard output using hex encoding.
|
||||
switch {
|
||||
case savePath != "":
|
||||
err = os.WriteFile(savePath, macBytes, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Macaroon saved to %s\n", savePath)
|
||||
|
||||
default:
|
||||
fmt.Printf("%s\n", hex.EncodeToString(macBytes))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var listMacaroonIDsCommand = cli.Command{
|
||||
Name: "listmacaroonids",
|
||||
Category: "Macaroons",
|
||||
Usage: "List all macaroons root key IDs in use.",
|
||||
Action: actionDecorator(listMacaroonIDs),
|
||||
}
|
||||
|
||||
func listMacaroonIDs(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.ListMacaroonIDsRequest{}
|
||||
resp, err := client.ListMacaroonIDs(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var deleteMacaroonIDCommand = cli.Command{
|
||||
Name: "deletemacaroonid",
|
||||
Category: "Macaroons",
|
||||
Usage: "Delete a specific macaroon ID.",
|
||||
ArgsUsage: "root_key_id",
|
||||
Description: `
|
||||
Remove a macaroon ID using the specified root key ID. For example:
|
||||
|
||||
lncli deletemacaroonid 1
|
||||
|
||||
WARNING
|
||||
When the ID is deleted, all macaroons created from that root key will
|
||||
be invalidated.
|
||||
|
||||
Note that the default root key ID 0 cannot be deleted.
|
||||
`,
|
||||
Action: actionDecorator(deleteMacaroonID),
|
||||
}
|
||||
|
||||
func deleteMacaroonID(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
// Validate args length. Only one argument is allowed.
|
||||
if ctx.NArg() != 1 {
|
||||
return cli.ShowCommandHelp(ctx, "deletemacaroonid")
|
||||
}
|
||||
|
||||
rootKeyIDString := ctx.Args().First()
|
||||
|
||||
// Convert string into uint64.
|
||||
rootKeyID, err := strconv.ParseUint(rootKeyIDString, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("root key ID must be a positive integer")
|
||||
}
|
||||
|
||||
// Check that the value is not equal to DefaultRootKeyID. Note that the
|
||||
// server also validates the root key ID when removing it. However, we check
|
||||
// it here too so that we can give users a nice warning.
|
||||
if bytes.Equal([]byte(rootKeyIDString), macaroons.DefaultRootKeyID) {
|
||||
return fmt.Errorf("deleting the default root key ID 0 is not allowed")
|
||||
}
|
||||
|
||||
// Make the actual RPC call.
|
||||
req := &lnrpc.DeleteMacaroonIDRequest{
|
||||
RootKeyId: rootKeyID,
|
||||
}
|
||||
resp, err := client.DeleteMacaroonID(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var listPermissionsCommand = cli.Command{
|
||||
Name: "listpermissions",
|
||||
Category: "Macaroons",
|
||||
Usage: "Lists all RPC method URIs and the macaroon permissions they " +
|
||||
"require to be invoked.",
|
||||
Action: actionDecorator(listPermissions),
|
||||
}
|
||||
|
||||
func listPermissions(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
request := &lnrpc.ListPermissionsRequest{}
|
||||
response, err := client.ListPermissions(ctxc, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type macaroonContent struct {
|
||||
Version uint16 `json:"version"`
|
||||
Location string `json:"location"`
|
||||
RootKeyID string `json:"root_key_id"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Caveats []string `json:"caveats"`
|
||||
}
|
||||
|
||||
var printMacaroonCommand = cli.Command{
|
||||
Name: "printmacaroon",
|
||||
Category: "Macaroons",
|
||||
Usage: "Print the content of a macaroon in a human readable format.",
|
||||
ArgsUsage: "[macaroon_content_hex]",
|
||||
Description: `
|
||||
Decode a macaroon and show its content in a more human readable format.
|
||||
The macaroon can either be passed as a hex encoded positional parameter
|
||||
or loaded from a file.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "macaroon_file",
|
||||
Usage: "load the macaroon from a file instead of the " +
|
||||
"command line directly",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(printMacaroon),
|
||||
}
|
||||
|
||||
func printMacaroon(ctx *cli.Context) error {
|
||||
// Show command help if no arguments or flags are set.
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "printmacaroon")
|
||||
}
|
||||
|
||||
var (
|
||||
macBytes []byte
|
||||
err error
|
||||
args = ctx.Args()
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("macaroon_file"):
|
||||
macPath := lncfg.CleanAndExpandPath(ctx.String("macaroon_file"))
|
||||
|
||||
// Load the specified macaroon file.
|
||||
macBytes, err = os.ReadFile(macPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read macaroon path %v: %v",
|
||||
macPath, err)
|
||||
}
|
||||
|
||||
case args.Present():
|
||||
macBytes, err = hex.DecodeString(args.First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to hex decode macaroon: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("macaroon parameter missing")
|
||||
}
|
||||
|
||||
// Decode the macaroon and its protobuf encoded internal identifier.
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon: %w", err)
|
||||
}
|
||||
rawID := mac.Id()
|
||||
if rawID[0] != byte(bakery.LatestVersion) {
|
||||
return fmt.Errorf("invalid macaroon version: %x", rawID)
|
||||
}
|
||||
decodedID := &lnrpc.MacaroonId{}
|
||||
idProto := rawID[1:]
|
||||
err = proto.Unmarshal(idProto, decodedID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon version: %w", err)
|
||||
}
|
||||
|
||||
// Prepare everything to be printed in a more human-readable format.
|
||||
content := &macaroonContent{
|
||||
Version: uint16(mac.Version()),
|
||||
Location: mac.Location(),
|
||||
RootKeyID: string(decodedID.StorageId),
|
||||
Permissions: nil,
|
||||
Caveats: nil,
|
||||
}
|
||||
|
||||
for _, caveat := range mac.Caveats() {
|
||||
content.Caveats = append(content.Caveats, string(caveat.Id))
|
||||
}
|
||||
for _, op := range decodedID.Ops {
|
||||
for _, action := range op.Actions {
|
||||
permission := fmt.Sprintf("%s:%s", op.Entity, action)
|
||||
content.Permissions = append(
|
||||
content.Permissions, permission,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
printJSON(content)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var constrainMacaroonCommand = cli.Command{
|
||||
Name: "constrainmacaroon",
|
||||
Category: "Macaroons",
|
||||
Usage: "Adds one or more restriction(s) to an existing macaroon",
|
||||
ArgsUsage: "[--timeout=] [--ip_address=] [--custom_caveat_name= " +
|
||||
"[--custom_caveat_condition=]] input-macaroon-file " +
|
||||
"constrained-macaroon-file",
|
||||
Description: `
|
||||
Add one or more first-party caveat(s) (a.k.a. constraints/restrictions)
|
||||
to an existing macaroon.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
macTimeoutFlag,
|
||||
macIPAddressFlag,
|
||||
macCustomCaveatNameFlag,
|
||||
macCustomCaveatConditionFlag,
|
||||
},
|
||||
Action: actionDecorator(constrainMacaroon),
|
||||
}
|
||||
|
||||
func constrainMacaroon(ctx *cli.Context) error {
|
||||
// Show command help if not enough arguments.
|
||||
if ctx.NArg() != 2 {
|
||||
return cli.ShowCommandHelp(ctx, "constrainmacaroon")
|
||||
}
|
||||
args := ctx.Args()
|
||||
|
||||
sourceMacFile := lncfg.CleanAndExpandPath(args.First())
|
||||
args = args.Tail()
|
||||
|
||||
sourceMacBytes, err := os.ReadFile(sourceMacFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error trying to read source macaroon file "+
|
||||
"%s: %v", sourceMacFile, err)
|
||||
}
|
||||
|
||||
destMacFile := lncfg.CleanAndExpandPath(args.First())
|
||||
|
||||
// Now we should have gotten a valid macaroon. Unmarshal it so we can
|
||||
// add first-party caveats (if necessary) to it.
|
||||
sourceMac := &macaroon.Macaroon{}
|
||||
if err = sourceMac.UnmarshalBinary(sourceMacBytes); err != nil {
|
||||
return fmt.Errorf("error unmarshalling source macaroon file "+
|
||||
"%s: %v", sourceMacFile, err)
|
||||
}
|
||||
|
||||
// Now apply the desired constraints to the macaroon. This will always
|
||||
// create a new macaroon object, even if no constraints are added.
|
||||
constrainedMac, err := applyMacaroonConstraints(ctx, sourceMac)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destMacBytes, err := constrainedMac.MarshalBinary()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling destination macaroon "+
|
||||
"file: %v", err)
|
||||
}
|
||||
|
||||
// Now we can output the result.
|
||||
err = os.WriteFile(destMacFile, destMacBytes, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing destination macaroon file "+
|
||||
"%s: %v", destMacFile, err)
|
||||
}
|
||||
fmt.Printf("Macaroon saved to %s\n", destMacFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyMacaroonConstraints parses and applies all currently supported macaroon
|
||||
// condition flags from the command line to the given macaroon and returns a new
|
||||
// macaroon instance.
|
||||
func applyMacaroonConstraints(ctx *cli.Context,
|
||||
mac *macaroon.Macaroon) (*macaroon.Macaroon, error) {
|
||||
|
||||
macConstraints := make([]macaroons.Constraint, 0)
|
||||
|
||||
if ctx.IsSet(macTimeoutFlag.Name) {
|
||||
timeout := ctx.Int64(macTimeoutFlag.Name)
|
||||
if timeout <= 0 {
|
||||
return nil, fmt.Errorf("timeout must be greater than 0")
|
||||
}
|
||||
macConstraints = append(
|
||||
macConstraints, macaroons.TimeoutConstraint(timeout),
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.IsSet(macIPAddressFlag.Name) {
|
||||
ipAddress := net.ParseIP(ctx.String(macIPAddressFlag.Name))
|
||||
if ipAddress == nil {
|
||||
return nil, fmt.Errorf("unable to parse ip_address: %s",
|
||||
ctx.String("ip_address"))
|
||||
}
|
||||
|
||||
macConstraints = append(
|
||||
macConstraints,
|
||||
macaroons.IPLockConstraint(ipAddress.String()),
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.IsSet(macCustomCaveatNameFlag.Name) {
|
||||
customCaveatName := ctx.String(macCustomCaveatNameFlag.Name)
|
||||
if containsWhiteSpace(customCaveatName) {
|
||||
return nil, fmt.Errorf("unexpected white space found " +
|
||||
"in custom caveat name")
|
||||
}
|
||||
if customCaveatName == "" {
|
||||
return nil, fmt.Errorf("invalid custom caveat name")
|
||||
}
|
||||
|
||||
var customCaveatCond string
|
||||
if ctx.IsSet(macCustomCaveatConditionFlag.Name) {
|
||||
customCaveatCond = ctx.String(
|
||||
macCustomCaveatConditionFlag.Name,
|
||||
)
|
||||
if containsWhiteSpace(customCaveatCond) {
|
||||
return nil, fmt.Errorf("unexpected white " +
|
||||
"space found in custom caveat " +
|
||||
"condition")
|
||||
}
|
||||
if customCaveatCond == "" {
|
||||
return nil, fmt.Errorf("invalid custom " +
|
||||
"caveat condition")
|
||||
}
|
||||
}
|
||||
|
||||
// The custom caveat condition is optional, it could just be a
|
||||
// marker tag in the macaroon with just a name. The interceptor
|
||||
// itself doesn't care about the value anyway.
|
||||
macConstraints = append(
|
||||
macConstraints, macaroons.CustomConstraint(
|
||||
customCaveatName, customCaveatCond,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error adding constraints: %w", err)
|
||||
}
|
||||
|
||||
return constrainedMac, nil
|
||||
}
|
||||
|
||||
// containsWhiteSpace returns true if the given string contains any character
|
||||
// that is considered to be a white space or non-printable character such as
|
||||
// space, tabulator, newline, carriage return and some more exotic ones.
|
||||
func containsWhiteSpace(str string) bool {
|
||||
return strings.IndexFunc(str, unicode.IsSpace) >= 0
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/routing"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var getCfgCommand = cli.Command{
|
||||
Name: "getmccfg",
|
||||
Category: "Mission Control",
|
||||
Usage: "Display mission control's config.",
|
||||
Description: `
|
||||
Returns the config currently being used by mission control.
|
||||
`,
|
||||
Action: actionDecorator(getCfg),
|
||||
}
|
||||
|
||||
func getCfg(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
resp, err := client.GetMissionControlConfig(
|
||||
ctxc, &routerrpc.GetMissionControlConfigRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var setCfgCommand = cli.Command{
|
||||
Name: "setmccfg",
|
||||
Category: "Mission Control",
|
||||
Usage: "Set mission control's config.",
|
||||
Description: `
|
||||
Update the config values being used by mission control to calculate the
|
||||
probability that payment routes will succeed. The estimator type must be
|
||||
provided to set estimator-related parameters.`,
|
||||
Flags: []cli.Flag{
|
||||
// General settings.
|
||||
cli.UintFlag{
|
||||
Name: "pmtnr",
|
||||
Usage: "the number of payments mission control " +
|
||||
"should store",
|
||||
},
|
||||
cli.DurationFlag{
|
||||
Name: "failrelax",
|
||||
Usage: "the amount of time to wait after a failure " +
|
||||
"before raising failure amount",
|
||||
},
|
||||
// Probability estimator.
|
||||
cli.StringFlag{
|
||||
Name: "estimator",
|
||||
Usage: "the probability estimator to use, choose " +
|
||||
"between 'apriori' or 'bimodal' (bimodal is " +
|
||||
"experimental)",
|
||||
},
|
||||
// Apriori config.
|
||||
cli.DurationFlag{
|
||||
Name: "apriorihalflife",
|
||||
Usage: "the amount of time taken to restore a node " +
|
||||
"or channel to 50% probability of success.",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "apriorihopprob",
|
||||
Usage: "the probability of success assigned " +
|
||||
"to hops that we have no information about",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "aprioriweight",
|
||||
Usage: "the degree to which mission control should " +
|
||||
"rely on historical results, expressed as " +
|
||||
"value in [0, 1]",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "aprioricapacityfraction",
|
||||
Usage: "the fraction of channels' capacities that is " +
|
||||
"considered liquid in pathfinding, a value " +
|
||||
"between [0.75-1.0]. a value of 1.0 disables " +
|
||||
"this feature.",
|
||||
},
|
||||
// Bimodal config.
|
||||
cli.DurationFlag{
|
||||
Name: "bimodaldecaytime",
|
||||
Usage: "the time span after which we phase out " +
|
||||
"learnings from previous payment attempts",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "bimodalscale",
|
||||
Usage: "controls the assumed channel liquidity " +
|
||||
"imbalance in the network, measured in msat. " +
|
||||
"a low value (compared to typical channel " +
|
||||
"capacity) anticipates unbalanced channels.",
|
||||
},
|
||||
cli.Float64Flag{
|
||||
Name: "bimodalweight",
|
||||
Usage: "controls the degree to which the probability " +
|
||||
"estimator takes into account other channels " +
|
||||
"of a router",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(setCfg),
|
||||
}
|
||||
|
||||
func setCfg(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
// Fetch current mission control config which we update to create our
|
||||
// response.
|
||||
mcCfg, err := client.GetMissionControlConfig(
|
||||
ctxc, &routerrpc.GetMissionControlConfigRequest{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// haveValue is a helper variable to determine if a flag has been set or
|
||||
// the help should be displayed.
|
||||
var haveValue bool
|
||||
|
||||
// Handle general mission control settings.
|
||||
if ctx.IsSet("pmtnr") {
|
||||
haveValue = true
|
||||
mcCfg.Config.MaximumPaymentResults = uint32(ctx.Int("pmtnr"))
|
||||
}
|
||||
if ctx.IsSet("failrelax") {
|
||||
haveValue = true
|
||||
mcCfg.Config.MinimumFailureRelaxInterval = uint64(ctx.Duration(
|
||||
"failrelax",
|
||||
).Seconds())
|
||||
}
|
||||
|
||||
// We switch between estimators and set corresponding configs. If
|
||||
// estimator is not set, we ignore the values.
|
||||
if ctx.IsSet("estimator") {
|
||||
switch ctx.String("estimator") {
|
||||
case routing.AprioriEstimatorName:
|
||||
haveValue = true
|
||||
|
||||
// If we switch from another estimator, initialize with
|
||||
// default values.
|
||||
if mcCfg.Config.Model !=
|
||||
routerrpc.MissionControlConfig_APRIORI {
|
||||
|
||||
dCfg := routing.DefaultAprioriConfig()
|
||||
aParams := &routerrpc.AprioriParameters{
|
||||
HalfLifeSeconds: uint64(
|
||||
dCfg.PenaltyHalfLife.Seconds(),
|
||||
),
|
||||
HopProbability: dCfg.
|
||||
AprioriHopProbability,
|
||||
Weight: dCfg.AprioriWeight,
|
||||
CapacityFraction: dCfg.CapacityFraction,
|
||||
}
|
||||
|
||||
// We make sure the correct config is set.
|
||||
mcCfg.Config.EstimatorConfig =
|
||||
&routerrpc.MissionControlConfig_Apriori{
|
||||
Apriori: aParams,
|
||||
}
|
||||
}
|
||||
|
||||
// We update all values for the apriori estimator.
|
||||
mcCfg.Config.Model = routerrpc.
|
||||
MissionControlConfig_APRIORI
|
||||
|
||||
aCfg := mcCfg.Config.GetApriori()
|
||||
if ctx.IsSet("apriorihalflife") {
|
||||
aCfg.HalfLifeSeconds = uint64(ctx.Duration(
|
||||
"apriorihalflife",
|
||||
).Seconds())
|
||||
}
|
||||
|
||||
if ctx.IsSet("apriorihopprob") {
|
||||
aCfg.HopProbability = ctx.Float64(
|
||||
"apriorihopprob",
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.IsSet("aprioriweight") {
|
||||
aCfg.Weight = ctx.Float64("aprioriweight")
|
||||
}
|
||||
|
||||
if ctx.IsSet("aprioricapacityfraction") {
|
||||
aCfg.CapacityFraction =
|
||||
ctx.Float64("aprioricapacityfraction")
|
||||
}
|
||||
|
||||
case routing.BimodalEstimatorName:
|
||||
haveValue = true
|
||||
|
||||
// If we switch from another estimator, initialize with
|
||||
// default values.
|
||||
if mcCfg.Config.Model !=
|
||||
routerrpc.MissionControlConfig_BIMODAL {
|
||||
|
||||
dCfg := routing.DefaultBimodalConfig()
|
||||
bParams := &routerrpc.BimodalParameters{
|
||||
DecayTime: uint64(
|
||||
dCfg.BimodalDecayTime.Seconds(),
|
||||
),
|
||||
ScaleMsat: uint64(
|
||||
dCfg.BimodalScaleMsat,
|
||||
),
|
||||
NodeWeight: dCfg.BimodalNodeWeight,
|
||||
}
|
||||
|
||||
// We make sure the correct config is set.
|
||||
mcCfg.Config.EstimatorConfig =
|
||||
&routerrpc.MissionControlConfig_Bimodal{
|
||||
Bimodal: bParams,
|
||||
}
|
||||
}
|
||||
|
||||
// We update all values for the bimodal estimator.
|
||||
mcCfg.Config.Model = routerrpc.
|
||||
MissionControlConfig_BIMODAL
|
||||
|
||||
bCfg := mcCfg.Config.GetBimodal()
|
||||
if ctx.IsSet("bimodaldecaytime") {
|
||||
bCfg.DecayTime = uint64(ctx.Duration(
|
||||
"bimodaldecaytime",
|
||||
).Seconds())
|
||||
}
|
||||
|
||||
if ctx.IsSet("bimodalscale") {
|
||||
bCfg.ScaleMsat = ctx.Uint64("bimodalscale")
|
||||
}
|
||||
|
||||
if ctx.IsSet("bimodalweight") {
|
||||
bCfg.NodeWeight = ctx.Float64(
|
||||
"bimodalweight",
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown estimator %v",
|
||||
ctx.String("estimator"))
|
||||
}
|
||||
}
|
||||
|
||||
if !haveValue {
|
||||
return cli.ShowCommandHelp(ctx, "setmccfg")
|
||||
}
|
||||
|
||||
_, err = client.SetMissionControlConfig(
|
||||
ctxc, &routerrpc.SetMissionControlConfigRequest{
|
||||
Config: mcCfg.Config,
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
var queryMissionControlCommand = cli.Command{
|
||||
Name: "querymc",
|
||||
Category: "Mission Control",
|
||||
Usage: "Query the internal mission control state.",
|
||||
Action: actionDecorator(queryMissionControl),
|
||||
}
|
||||
|
||||
func queryMissionControl(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
req := &routerrpc.QueryMissionControlRequest{}
|
||||
snapshot, err := client.QueryMissionControl(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(snapshot)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var queryProbCommand = cli.Command{
|
||||
Name: "queryprob",
|
||||
Category: "Mission Control",
|
||||
Usage: "Deprecated. Estimate a success probability.",
|
||||
ArgsUsage: "from-node to-node amt",
|
||||
Action: actionDecorator(queryProb),
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func queryProb(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
args := ctx.Args()
|
||||
|
||||
if len(args) != 3 {
|
||||
return cli.ShowCommandHelp(ctx, "queryprob")
|
||||
}
|
||||
|
||||
fromNode, err := route.NewVertexFromStr(args.Get(0))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid from node key: %w", err)
|
||||
}
|
||||
|
||||
toNode, err := route.NewVertexFromStr(args.Get(1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid to node key: %w", err)
|
||||
}
|
||||
|
||||
amtSat, err := strconv.ParseUint(args.Get(2), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid amt: %w", err)
|
||||
}
|
||||
|
||||
amtMsat := lnwire.NewMSatFromSatoshis(
|
||||
btcutil.Amount(amtSat),
|
||||
)
|
||||
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
req := &routerrpc.QueryProbabilityRequest{
|
||||
FromNode: fromNode[:],
|
||||
ToNode: toNode[:],
|
||||
AmtMsat: int64(amtMsat),
|
||||
}
|
||||
|
||||
response, err := client.QueryProbability(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var resetMissionControlCommand = cli.Command{
|
||||
Name: "resetmc",
|
||||
Category: "Mission Control",
|
||||
Usage: "Reset internal mission control state.",
|
||||
Action: actionDecorator(resetMissionControl),
|
||||
}
|
||||
|
||||
func resetMissionControl(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
|
||||
req := &routerrpc.ResetMissionControlRequest{}
|
||||
_, err := client.ResetMissionControl(ctxc, req)
|
||||
return err
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,452 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// defaultLncliDir is the default directory to store the profile file
|
||||
// in. This defaults to:
|
||||
// C:\Users\<username>\AppData\Local\Lncli\ on Windows
|
||||
// ~/.lncli/ on Linux
|
||||
// ~/Library/Application Support/Lncli/ on MacOS
|
||||
defaultLncliDir = btcutil.AppDataDir("lncli", false)
|
||||
|
||||
// defaultProfileFile is the full, absolute path of the profile file.
|
||||
defaultProfileFile = path.Join(defaultLncliDir, "profiles.json")
|
||||
)
|
||||
|
||||
var profileSubCommand = cli.Command{
|
||||
Name: "profile",
|
||||
Category: "Profiles",
|
||||
Usage: "Create and manage lncli profiles.",
|
||||
Description: `
|
||||
Profiles for lncli are an easy and comfortable way to manage multiple
|
||||
nodes from the command line by storing node specific parameters like RPC
|
||||
host, network, TLS certificate path or macaroons in a named profile.
|
||||
|
||||
To use a predefined profile, just use the '--profile=myprofile' (or
|
||||
short version '-p=myprofile') with any lncli command.
|
||||
|
||||
A default profile can also be defined, lncli will then always use the
|
||||
connection/node parameters from that profile instead of the default
|
||||
values.
|
||||
|
||||
WARNING: Setting a default profile changes the default behavior of
|
||||
lncli! To disable the use of the default profile for a single command,
|
||||
set '--profile= '.
|
||||
|
||||
The profiles are stored in a file called profiles.json in the user's
|
||||
home directory, for example:
|
||||
C:\Users\<username>\AppData\Local\Lncli\profiles.json on Windows
|
||||
~/.lncli/profiles.json on Linux
|
||||
~/Library/Application Support/Lncli/profiles.json on MacOS
|
||||
`,
|
||||
Subcommands: []cli.Command{
|
||||
profileListCommand,
|
||||
profileAddCommand,
|
||||
profileRemoveCommand,
|
||||
profileSetDefaultCommand,
|
||||
profileUnsetDefaultCommand,
|
||||
profileAddMacaroonCommand,
|
||||
},
|
||||
}
|
||||
|
||||
var profileListCommand = cli.Command{
|
||||
Name: "list",
|
||||
Usage: "Lists all lncli profiles",
|
||||
Action: profileList,
|
||||
}
|
||||
|
||||
func profileList(_ *cli.Context) error {
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printJSON(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profileAddCommand = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Add a new profile.",
|
||||
ArgsUsage: "name",
|
||||
Description: `
|
||||
Add a new named profile to the main profiles.json. All global options
|
||||
(see 'lncli --help') passed into this command are stored in that named
|
||||
profile.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "the name of the new profile",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "set the new profile to be the default profile",
|
||||
},
|
||||
},
|
||||
Action: profileAdd,
|
||||
}
|
||||
|
||||
func profileAdd(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "add")
|
||||
}
|
||||
|
||||
// Load the default profile file or create a new one if it doesn't exist
|
||||
// yet.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
switch {
|
||||
case err == errNoProfileFile:
|
||||
f = &profileFile{}
|
||||
_ = os.MkdirAll(path.Dir(defaultProfileFile), 0700)
|
||||
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a profile struct from all the global options.
|
||||
profile, err := profileFromContext(ctx, true, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load global options: %w", err)
|
||||
}
|
||||
|
||||
// Finally, all that's left is to get the profile name from either
|
||||
// positional argument or flag.
|
||||
args := ctx.Args()
|
||||
switch {
|
||||
case ctx.IsSet("name"):
|
||||
profile.Name = ctx.String("name")
|
||||
case args.Present():
|
||||
profile.Name = args.First()
|
||||
default:
|
||||
return fmt.Errorf("name argument missing")
|
||||
}
|
||||
|
||||
// Is there already a profile with that name?
|
||||
for _, p := range f.Profiles {
|
||||
if p.Name == profile.Name {
|
||||
return fmt.Errorf("a profile with the name %s already "+
|
||||
"exists", profile.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to update the default entry to be this one?
|
||||
if ctx.Bool("default") {
|
||||
f.Default = profile.Name
|
||||
}
|
||||
|
||||
// All done, store the updated profile file.
|
||||
f.Profiles = append(f.Profiles, profile)
|
||||
if err = saveProfileFile(defaultProfileFile, f); err != nil {
|
||||
return fmt.Errorf("error writing profile file %s: %w",
|
||||
defaultProfileFile, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Profile %s added to file %s.\n", profile.Name,
|
||||
defaultProfileFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
var profileRemoveCommand = cli.Command{
|
||||
Name: "remove",
|
||||
Usage: "Remove a profile",
|
||||
ArgsUsage: "name",
|
||||
Description: `Remove the specified profile from the profile file.`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "the name of the profile to delete",
|
||||
},
|
||||
},
|
||||
Action: profileRemove,
|
||||
}
|
||||
|
||||
func profileRemove(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "remove")
|
||||
}
|
||||
|
||||
// Load the default profile file.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load profile file: %w", err)
|
||||
}
|
||||
|
||||
// Get the profile name from either positional argument or flag.
|
||||
var (
|
||||
args = ctx.Args()
|
||||
name string
|
||||
found = false
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("name"):
|
||||
name = ctx.String("name")
|
||||
case args.Present():
|
||||
name = args.First()
|
||||
default:
|
||||
return fmt.Errorf("name argument missing")
|
||||
}
|
||||
|
||||
if len(f.Profiles) == 0 {
|
||||
return fmt.Errorf("there are no existing profiles")
|
||||
}
|
||||
|
||||
// Create a copy of all profiles but don't include the one to delete.
|
||||
newProfiles := make([]*profileEntry, 0, len(f.Profiles)-1)
|
||||
for _, p := range f.Profiles {
|
||||
// Skip the one we want to delete.
|
||||
if p.Name == name {
|
||||
found = true
|
||||
|
||||
if p.Name == f.Default {
|
||||
fmt.Println("Warning: removing default profile.")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Keep all others.
|
||||
newProfiles = append(newProfiles, p)
|
||||
}
|
||||
|
||||
// If what we were looking for didn't exist in the first place, there's
|
||||
// no need for updating the file.
|
||||
if !found {
|
||||
return fmt.Errorf("profile with name %s not found in file",
|
||||
name)
|
||||
}
|
||||
|
||||
// Great, everything updated, now let's save the file.
|
||||
f.Profiles = newProfiles
|
||||
return saveProfileFile(defaultProfileFile, f)
|
||||
}
|
||||
|
||||
var profileSetDefaultCommand = cli.Command{
|
||||
Name: "setdefault",
|
||||
Usage: "Set the default profile.",
|
||||
ArgsUsage: "name",
|
||||
Description: `
|
||||
Set a specified profile to be used as the default profile.
|
||||
|
||||
WARNING: Setting a default profile changes the default behavior of
|
||||
lncli! To disable the use of the default profile for a single command,
|
||||
set '--profile= '.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "the name of the profile to set as default",
|
||||
},
|
||||
},
|
||||
Action: profileSetDefault,
|
||||
}
|
||||
|
||||
func profileSetDefault(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "setdefault")
|
||||
}
|
||||
|
||||
// Load the default profile file.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load profile file: %w", err)
|
||||
}
|
||||
|
||||
// Get the profile name from either positional argument or flag.
|
||||
var (
|
||||
args = ctx.Args()
|
||||
name string
|
||||
found = false
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("name"):
|
||||
name = ctx.String("name")
|
||||
case args.Present():
|
||||
name = args.First()
|
||||
default:
|
||||
return fmt.Errorf("name argument missing")
|
||||
}
|
||||
|
||||
// Make sure the new default profile actually exists.
|
||||
for _, p := range f.Profiles {
|
||||
if p.Name == name {
|
||||
found = true
|
||||
f.Default = p.Name
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the default profile doesn't exist, there's no need for updating
|
||||
// the file.
|
||||
if !found {
|
||||
return fmt.Errorf("profile with name %s not found in file",
|
||||
name)
|
||||
}
|
||||
|
||||
// Great, everything updated, now let's save the file.
|
||||
return saveProfileFile(defaultProfileFile, f)
|
||||
}
|
||||
|
||||
var profileUnsetDefaultCommand = cli.Command{
|
||||
Name: "unsetdefault",
|
||||
Usage: "Unsets the default profile.",
|
||||
Description: `
|
||||
Disables the use of a default profile and restores lncli to its original
|
||||
behavior.
|
||||
`,
|
||||
Action: profileUnsetDefault,
|
||||
}
|
||||
|
||||
func profileUnsetDefault(_ *cli.Context) error {
|
||||
// Load the default profile file.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load profile file: %w", err)
|
||||
}
|
||||
|
||||
// Save the file with the flag disabled.
|
||||
f.Default = ""
|
||||
return saveProfileFile(defaultProfileFile, f)
|
||||
}
|
||||
|
||||
var profileAddMacaroonCommand = cli.Command{
|
||||
Name: "addmacaroon",
|
||||
Usage: "Add a macaroon to a profile's macaroon jar.",
|
||||
ArgsUsage: "macaroon-name",
|
||||
Description: `
|
||||
Add an additional macaroon specified by the global option --macaroonpath
|
||||
to an existing profile's macaroon jar.
|
||||
|
||||
If no profile is selected, the macaroon is added to the default profile
|
||||
(if one exists). To add a macaroon to a specific profile, use the global
|
||||
--profile=myprofile option.
|
||||
|
||||
If multiple macaroons exist in a profile's macaroon jar, the one to use
|
||||
can be specified with the global option --macfromjar=xyz.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "the name of the macaroon",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "set the new macaroon to be the default " +
|
||||
"macaroon in the jar",
|
||||
},
|
||||
},
|
||||
Action: profileAddMacaroon,
|
||||
}
|
||||
|
||||
func profileAddMacaroon(ctx *cli.Context) error {
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
return cli.ShowCommandHelp(ctx, "addmacaroon")
|
||||
}
|
||||
|
||||
// Load the default profile file or create a new one if it doesn't exist
|
||||
// yet.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load profile file: %w", err)
|
||||
}
|
||||
|
||||
// Finally, all that's left is to get the profile name from either
|
||||
// positional argument or flag.
|
||||
var (
|
||||
args = ctx.Args()
|
||||
profileName string
|
||||
macName string
|
||||
)
|
||||
switch {
|
||||
case ctx.IsSet("name"):
|
||||
macName = ctx.String("name")
|
||||
case args.Present():
|
||||
macName = args.First()
|
||||
default:
|
||||
return fmt.Errorf("name argument missing")
|
||||
}
|
||||
|
||||
// Make sure the user actually set a macaroon path to use.
|
||||
if !ctx.GlobalIsSet("macaroonpath") {
|
||||
return fmt.Errorf("macaroonpath global option missing")
|
||||
}
|
||||
|
||||
// Find out which profile we should add the macaroon. The global flag
|
||||
// takes precedence over the default profile.
|
||||
if f.Default != "" {
|
||||
profileName = f.Default
|
||||
}
|
||||
if ctx.GlobalIsSet("profile") {
|
||||
profileName = ctx.GlobalString("profile")
|
||||
}
|
||||
if len(strings.TrimSpace(profileName)) == 0 {
|
||||
return fmt.Errorf("no profile specified and no default " +
|
||||
"profile exists")
|
||||
}
|
||||
|
||||
// Is there a profile with that name?
|
||||
var selectedProfile *profileEntry
|
||||
for _, p := range f.Profiles {
|
||||
if p.Name == profileName {
|
||||
selectedProfile = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if selectedProfile == nil {
|
||||
return fmt.Errorf("profile with name %s not found", profileName)
|
||||
}
|
||||
|
||||
// Does a macaroon with that name already exist?
|
||||
for _, m := range selectedProfile.Macaroons.Jar {
|
||||
if m.Name == macName {
|
||||
return fmt.Errorf("a macaroon with the name %s "+
|
||||
"already exists", macName)
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to update the default entry to be this one?
|
||||
if ctx.Bool("default") {
|
||||
selectedProfile.Macaroons.Default = macName
|
||||
}
|
||||
|
||||
// Now load and possibly encrypt the macaroon file.
|
||||
macPath := lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
||||
macBytes, err := os.ReadFile(macPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read macaroon path: %w", err)
|
||||
}
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return fmt.Errorf("unable to decode macaroon: %w", err)
|
||||
}
|
||||
macEntry := &macaroonEntry{
|
||||
Name: macName,
|
||||
}
|
||||
if err = macEntry.storeMacaroon(mac, nil); err != nil {
|
||||
return fmt.Errorf("unable to store macaroon: %w", err)
|
||||
}
|
||||
|
||||
// All done, store the updated profile file.
|
||||
selectedProfile.Macaroons.Jar = append(
|
||||
selectedProfile.Macaroons.Jar, macEntry,
|
||||
)
|
||||
if err = saveProfileFile(defaultProfileFile, f); err != nil {
|
||||
return fmt.Errorf("error writing profile file %s: %w",
|
||||
defaultProfileFile, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Macaroon %s added to profile %s in file %s.\n", macName,
|
||||
selectedProfile.Name, defaultProfileFile)
|
||||
return nil
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var getStateCommand = cli.Command{
|
||||
Name: "state",
|
||||
Category: "Startup",
|
||||
Usage: "Get the current state of the wallet and RPC",
|
||||
Description: `
|
||||
Get the current state of the wallet. The possible states are:
|
||||
- WAITING_TO_START: node is waiting to become the leader in a cluster
|
||||
and is not started yet.
|
||||
- NON_EXISTING: wallet has not yet been initialized.
|
||||
- LOCKED: wallet is locked.
|
||||
- UNLOCKED: wallet was unlocked successfully, but RPC server isn't ready.
|
||||
- RPC_ACTIVE: RPC server is active but not fully ready for calls.
|
||||
- SERVER_ACTIVE: RPC server is available and ready to accept calls.
|
||||
`,
|
||||
Flags: []cli.Flag{},
|
||||
Action: actionDecorator(getState),
|
||||
}
|
||||
|
||||
func getState(ctx *cli.Context) error {
|
||||
ctxb := context.Background()
|
||||
client, cleanUp := getStateServiceClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &lnrpc.SubscribeStateRequest{}
|
||||
stream, err := client.SubscribeState(ctxb, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get a single state, then exit.
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var updateChanStatusCommand = cli.Command{
|
||||
Name: "updatechanstatus",
|
||||
Category: "Channels",
|
||||
Usage: "Set the status of an existing channel on the network.",
|
||||
Description: `
|
||||
Set the status of an existing channel on the network. The actions can
|
||||
be "enable", "disable", or "auto". If the action changes the status, a
|
||||
message will be broadcast over the network.
|
||||
|
||||
Note that enabling / disabling a channel using this command ONLY affects
|
||||
what's advertised over the network. For example, disabling a channel
|
||||
using this command does not close it.
|
||||
|
||||
If a channel is manually disabled, automatic / background requests to
|
||||
re-enable the channel will be ignored. However, if a channel is
|
||||
manually enabled, automatic / background requests to disable the
|
||||
channel will succeed (such requests are usually made on channel close
|
||||
or when the peer is down).
|
||||
|
||||
The "auto" action restores automatic channel state management. Per
|
||||
the behavior described above, it's only needed to undo the effect of
|
||||
a prior "disable" action, and will be a no-op otherwise.`,
|
||||
ArgsUsage: "funding_txid [output_index] action",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "funding_txid",
|
||||
Usage: "the txid of the channel's funding transaction",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "output_index",
|
||||
Usage: "the output index for the funding output of " +
|
||||
"the funding transaction",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "chan_point",
|
||||
Usage: "the channel whose status should be updated. " +
|
||||
"Takes the form of: txid:output_index",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "action",
|
||||
Usage: `the action to take: must be one of "enable", ` +
|
||||
`"disable", or "auto"`,
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(updateChanStatus),
|
||||
}
|
||||
|
||||
func updateChanStatus(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
_ = cli.ShowCommandHelp(ctx, "updatechanstatus")
|
||||
return nil
|
||||
}
|
||||
|
||||
channelPoint, err := parseChannelPoint(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var action routerrpc.ChanStatusAction
|
||||
switch ctx.String("action") {
|
||||
case "enable":
|
||||
action = routerrpc.ChanStatusAction_ENABLE
|
||||
case "disable":
|
||||
action = routerrpc.ChanStatusAction_DISABLE
|
||||
case "auto":
|
||||
action = routerrpc.ChanStatusAction_AUTO
|
||||
default:
|
||||
return errors.New(`action must be one of "enable", "disable", ` +
|
||||
`or "auto"`)
|
||||
}
|
||||
req := &routerrpc.UpdateChanStatusRequest{
|
||||
ChanPoint: channelPoint,
|
||||
Action: action,
|
||||
}
|
||||
|
||||
client := routerrpc.NewRouterClient(conn)
|
||||
resp, err := client.UpdateChanStatus(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/lnclipb"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var versionCommand = cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Display lncli and lnd version info.",
|
||||
Description: `
|
||||
Returns version information about both lncli and lnd. If lncli is unable
|
||||
to connect to lnd, the command fails but still prints the lncli version.
|
||||
`,
|
||||
Action: actionDecorator(version),
|
||||
}
|
||||
|
||||
func version(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
versions := &lnclipb.VersionResponse{
|
||||
Lncli: &verrpc.Version{
|
||||
Commit: build.Commit,
|
||||
CommitHash: build.CommitHash,
|
||||
Version: build.Version(),
|
||||
AppMajor: uint32(build.AppMajor),
|
||||
AppMinor: uint32(build.AppMinor),
|
||||
AppPatch: uint32(build.AppPatch),
|
||||
AppPreRelease: build.AppPreRelease,
|
||||
BuildTags: build.Tags(),
|
||||
GoVersion: build.GoVersion,
|
||||
},
|
||||
}
|
||||
|
||||
client := verrpc.NewVersionerClient(conn)
|
||||
|
||||
lndVersion, err := client.GetVersion(ctxc, &verrpc.VersionRequest{})
|
||||
if err != nil {
|
||||
printRespJSON(versions)
|
||||
return fmt.Errorf("unable fetch version from lnd: %w", err)
|
||||
}
|
||||
versions.Lnd = lndVersion
|
||||
|
||||
printRespJSON(versions)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,891 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
statelessInitFlag = cli.BoolFlag{
|
||||
Name: "stateless_init",
|
||||
Usage: "do not create any macaroon files in the file " +
|
||||
"system of the daemon",
|
||||
}
|
||||
saveToFlag = cli.StringFlag{
|
||||
Name: "save_to",
|
||||
Usage: "save returned admin macaroon to this file",
|
||||
}
|
||||
)
|
||||
|
||||
var createCommand = cli.Command{
|
||||
Name: "create",
|
||||
Category: "Startup",
|
||||
Usage: "Initialize a wallet when starting lnd for the first time.",
|
||||
Description: `
|
||||
The create command is used to initialize an lnd wallet from scratch for
|
||||
the very first time. This is interactive command with one required
|
||||
argument (the password), and one optional argument (the mnemonic
|
||||
passphrase).
|
||||
|
||||
The first argument (the password) is required and MUST be greater than
|
||||
8 characters. This will be used to encrypt the wallet within lnd. This
|
||||
MUST be remembered as it will be required to fully start up the daemon.
|
||||
|
||||
The second argument is an optional 24-word mnemonic derived from BIP
|
||||
39. If provided, then the internal wallet will use the seed derived
|
||||
from this mnemonic to generate all keys.
|
||||
|
||||
This command returns a 24-word seed in the scenario that NO mnemonic
|
||||
was provided by the user. This should be written down as it can be used
|
||||
to potentially recover all on-chain funds, and most off-chain funds as
|
||||
well.
|
||||
|
||||
If the --stateless_init flag is set, no macaroon files are created by
|
||||
the daemon. Instead, the binary serialized admin macaroon is returned
|
||||
in the answer. This answer MUST be stored somewhere, otherwise all
|
||||
access to the RPC server will be lost and the wallet must be recreated
|
||||
to re-gain access.
|
||||
If the --save_to parameter is set, the macaroon is saved to this file,
|
||||
otherwise it is printed to standard out.
|
||||
|
||||
Finally, it's also possible to use this command and a set of static
|
||||
channel backups to trigger a recover attempt for the provided Static
|
||||
Channel Backups. Only one of the three parameters will be accepted. See
|
||||
the restorechanbackup command for further details w.r.t the format
|
||||
accepted.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "single_backup",
|
||||
Usage: "a hex encoded single channel backup obtained " +
|
||||
"from exportchanbackup",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "multi_backup",
|
||||
Usage: "a hex encoded multi-channel backup obtained " +
|
||||
"from exportchanbackup",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "multi_file",
|
||||
Usage: "the path to a multi-channel back up file",
|
||||
},
|
||||
statelessInitFlag,
|
||||
saveToFlag,
|
||||
},
|
||||
Action: actionDecorator(create),
|
||||
}
|
||||
|
||||
// monowidthColumns takes a set of words, and the number of desired columns,
|
||||
// and returns a new set of words that have had white space appended to the
|
||||
// word in order to create a mono-width column.
|
||||
func monowidthColumns(words []string, ncols int) []string {
|
||||
// Determine max size of words in each column.
|
||||
colWidths := make([]int, ncols)
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
curWidth := colWidths[col]
|
||||
if len(word) > curWidth {
|
||||
colWidths[col] = len(word)
|
||||
}
|
||||
}
|
||||
|
||||
// Append whitespace to each word to make columns mono-width.
|
||||
finalWords := make([]string, len(words))
|
||||
for i, word := range words {
|
||||
col := i % ncols
|
||||
width := colWidths[col]
|
||||
|
||||
diff := width - len(word)
|
||||
finalWords[i] = word + strings.Repeat(" ", diff)
|
||||
}
|
||||
|
||||
return finalWords
|
||||
}
|
||||
|
||||
func create(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
var (
|
||||
chanBackups *lnrpc.ChanBackupSnapshot
|
||||
|
||||
// We use var restoreSCB to track if we will be including an SCB
|
||||
// recovery in the init wallet request.
|
||||
restoreSCB = false
|
||||
)
|
||||
|
||||
backups, err := parseChanBackups(ctx)
|
||||
|
||||
// We'll check to see if the user provided any static channel backups (SCB),
|
||||
// if so, we will warn the user that SCB recovery closes all open channels
|
||||
// and ask them to confirm their intention.
|
||||
// If the user agrees, we'll add the SCB recovery onto the final init wallet
|
||||
// request.
|
||||
switch {
|
||||
// parseChanBackups returns an errMissingBackup error (which we ignore) if
|
||||
// the user did not request a SCB recovery.
|
||||
case err == errMissingChanBackup:
|
||||
|
||||
// Passed an invalid channel backup file.
|
||||
case err != nil:
|
||||
return fmt.Errorf("unable to parse chan backups: %w", err)
|
||||
|
||||
// We have an SCB recovery option with a valid backup file.
|
||||
default:
|
||||
|
||||
warningLoop:
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Printf("WARNING: You are attempting to restore from a " +
|
||||
"static channel backup (SCB) file.\nThis action will CLOSE " +
|
||||
"all currently open channels, and you will pay on-chain fees." +
|
||||
"\n\nAre you sure you want to recover funds from a" +
|
||||
" static channel backup? (Enter y/n): ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
answer = strings.TrimSpace(answer)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
switch answer {
|
||||
case "y":
|
||||
restoreSCB = true
|
||||
break warningLoop
|
||||
case "n":
|
||||
fmt.Println("Aborting SCB recovery")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with SCB recovery.
|
||||
if restoreSCB {
|
||||
fmt.Println("Static Channel Backup (SCB) recovery selected!")
|
||||
if backups != nil {
|
||||
switch {
|
||||
case backups.GetChanBackups() != nil:
|
||||
singleBackup := backups.GetChanBackups()
|
||||
chanBackups = &lnrpc.ChanBackupSnapshot{
|
||||
SingleChanBackups: singleBackup,
|
||||
}
|
||||
|
||||
case backups.GetMultiChanBackup() != nil:
|
||||
multiBackup := backups.GetMultiChanBackup()
|
||||
chanBackups = &lnrpc.ChanBackupSnapshot{
|
||||
MultiChanBackup: &lnrpc.MultiChanBackup{
|
||||
MultiChanBackup: multiBackup,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Should the daemon be initialized stateless? Then we expect an answer
|
||||
// with the admin macaroon later. Because the --save_to is related to
|
||||
// stateless init, it doesn't make sense to be set on its own.
|
||||
statelessInit := ctx.Bool(statelessInitFlag.Name)
|
||||
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
|
||||
return fmt.Errorf("cannot set save_to parameter without " +
|
||||
"stateless_init")
|
||||
}
|
||||
|
||||
walletPassword, err := capturePassword(
|
||||
"Input wallet password: ", false, walletunlocker.ValidatePassword,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Next, we'll see if the user has 24-word mnemonic they want to use to
|
||||
// derive a seed within the wallet or if they want to specify an
|
||||
// extended master root key (xprv) directly.
|
||||
var (
|
||||
hasMnemonic bool
|
||||
hasXprv bool
|
||||
)
|
||||
|
||||
mnemonicCheck:
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Printf("Do you have an existing cipher seed " +
|
||||
"mnemonic or extended master root key you want to " +
|
||||
"use?\nEnter 'y' to use an existing cipher seed " +
|
||||
"mnemonic, 'x' to use an extended master root key " +
|
||||
"\nor 'n' to create a new seed (Enter y/x/n): ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
answer = strings.TrimSpace(answer)
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
switch answer {
|
||||
case "y":
|
||||
hasMnemonic = true
|
||||
break mnemonicCheck
|
||||
|
||||
case "x":
|
||||
hasXprv = true
|
||||
break mnemonicCheck
|
||||
|
||||
case "n":
|
||||
break mnemonicCheck
|
||||
}
|
||||
}
|
||||
|
||||
// If the user *does* have an existing seed or root key they want to
|
||||
// use, then we'll read that in directly from the terminal.
|
||||
var (
|
||||
cipherSeedMnemonic []string
|
||||
aezeedPass []byte
|
||||
extendedRootKey string
|
||||
extendedRootKeyBirthday uint64
|
||||
recoveryWindow int32
|
||||
)
|
||||
switch {
|
||||
// Use an existing cipher seed mnemonic in the aezeed format.
|
||||
case hasMnemonic:
|
||||
// We'll now prompt the user to enter in their 24-word
|
||||
// mnemonic.
|
||||
fmt.Printf("Input your 24-word mnemonic separated by spaces: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
mnemonic, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll trim off extra spaces, and ensure the mnemonic is all
|
||||
// lower case, then populate our request.
|
||||
mnemonic = strings.TrimSpace(mnemonic)
|
||||
mnemonic = strings.ToLower(mnemonic)
|
||||
|
||||
cipherSeedMnemonic = strings.Split(mnemonic, " ")
|
||||
|
||||
fmt.Println()
|
||||
|
||||
if len(cipherSeedMnemonic) != 24 {
|
||||
return fmt.Errorf("wrong cipher seed mnemonic "+
|
||||
"length: got %v words, expecting %v words",
|
||||
len(cipherSeedMnemonic), 24)
|
||||
}
|
||||
|
||||
// Additionally, the user may have a passphrase, that will also
|
||||
// need to be provided so the daemon can properly decipher the
|
||||
// cipher seed.
|
||||
aezeedPass, err = readPassword("Input your cipher seed " +
|
||||
"passphrase (press enter if your seed doesn't have a " +
|
||||
"passphrase): ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recoveryWindow, err = askRecoveryWindow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use an existing extended master root key to create the wallet.
|
||||
case hasXprv:
|
||||
// We'll now prompt the user to enter in their extended master
|
||||
// root key.
|
||||
fmt.Printf("Input your extended master root key (usually " +
|
||||
"starting with xprv... on mainnet): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
extendedRootKey, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extendedRootKey = strings.TrimSpace(extendedRootKey)
|
||||
|
||||
extendedRootKeyBirthday, err = askBirthdayTimestamp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recoveryWindow, err = askRecoveryWindow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Neither a seed nor a master root key was specified, the user wants
|
||||
// to create a new seed.
|
||||
default:
|
||||
// Otherwise, if the user doesn't have a mnemonic that they
|
||||
// want to use, we'll generate a fresh one with the GenSeed
|
||||
// command.
|
||||
fmt.Println("Your cipher seed can optionally be encrypted.")
|
||||
|
||||
instruction := "Input your passphrase if you wish to encrypt it " +
|
||||
"(or press enter to proceed without a cipher seed " +
|
||||
"passphrase): "
|
||||
aezeedPass, err = capturePassword(
|
||||
instruction, true, func(_ []byte) error { return nil },
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("Generating fresh cipher seed...")
|
||||
fmt.Println()
|
||||
|
||||
genSeedReq := &lnrpc.GenSeedRequest{
|
||||
AezeedPassphrase: aezeedPass,
|
||||
}
|
||||
seedResp, err := client.GenSeed(ctxc, genSeedReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate seed: %w", err)
|
||||
}
|
||||
|
||||
cipherSeedMnemonic = seedResp.CipherSeedMnemonic
|
||||
}
|
||||
|
||||
// Before we initialize the wallet, we'll display the cipher seed to
|
||||
// the user so they can write it down.
|
||||
if len(cipherSeedMnemonic) > 0 {
|
||||
printCipherSeedWords(cipherSeedMnemonic)
|
||||
}
|
||||
|
||||
// With either the user's prior cipher seed, or a newly generated one,
|
||||
// we'll go ahead and initialize the wallet.
|
||||
req := &lnrpc.InitWalletRequest{
|
||||
WalletPassword: walletPassword,
|
||||
CipherSeedMnemonic: cipherSeedMnemonic,
|
||||
AezeedPassphrase: aezeedPass,
|
||||
ExtendedMasterKey: extendedRootKey,
|
||||
ExtendedMasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
|
||||
RecoveryWindow: recoveryWindow,
|
||||
ChannelBackups: chanBackups,
|
||||
StatelessInit: statelessInit,
|
||||
}
|
||||
response, err := client.InitWallet(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nlnd successfully initialized!")
|
||||
|
||||
if statelessInit {
|
||||
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// capturePassword returns a password value that has been entered twice by the
|
||||
// user, to ensure that the user knows what password they have entered. The user
|
||||
// will be prompted to retry until the passwords match. If the optional param is
|
||||
// true, the function may return an empty byte array if the user opts against
|
||||
// using a password.
|
||||
func capturePassword(instruction string, optional bool,
|
||||
validate func([]byte) error) ([]byte, error) {
|
||||
|
||||
for {
|
||||
password, err := readPassword(instruction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do not require users to repeat password if
|
||||
// it is optional and they are not using one.
|
||||
if len(password) == 0 && optional {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If the password provided is not valid, restart
|
||||
// password capture process from the beginning.
|
||||
if err := validate(password); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Println()
|
||||
continue
|
||||
}
|
||||
|
||||
passwordConfirmed, err := readPassword("Confirm password: ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bytes.Equal(password, passwordConfirmed) {
|
||||
return password, nil
|
||||
}
|
||||
|
||||
fmt.Println("Passwords don't match, please try again")
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
var unlockCommand = cli.Command{
|
||||
Name: "unlock",
|
||||
Category: "Startup",
|
||||
Usage: "Unlock an encrypted wallet at startup.",
|
||||
Description: `
|
||||
The unlock command is used to decrypt lnd's wallet state in order to
|
||||
start up. This command MUST be run after booting up lnd before it's
|
||||
able to carry out its duties. An exception is if a user is running with
|
||||
--noseedbackup, then a default passphrase will be used.
|
||||
|
||||
If the --stateless_init flag is set, no macaroon files are created by
|
||||
the daemon. This should be set for every unlock if the daemon was
|
||||
initially initialized stateless. Otherwise the daemon will create
|
||||
unencrypted macaroon files which could leak information to the system
|
||||
that the daemon runs on.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "recovery_window",
|
||||
Usage: "address lookahead to resume recovery rescan, " +
|
||||
"value should be non-zero -- To recover all " +
|
||||
"funds, this should be greater than the " +
|
||||
"maximum number of consecutive, unused " +
|
||||
"addresses ever generated by the wallet.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "stdin",
|
||||
Usage: "read password from standard input instead of " +
|
||||
"prompting for it. THIS IS CONSIDERED TO " +
|
||||
"BE DANGEROUS if the password is located in " +
|
||||
"a file that can be read by another user. " +
|
||||
"This flag should only be used in " +
|
||||
"combination with some sort of password " +
|
||||
"manager or secrets vault.",
|
||||
},
|
||||
statelessInitFlag,
|
||||
},
|
||||
Action: actionDecorator(unlock),
|
||||
}
|
||||
|
||||
func unlock(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
var (
|
||||
pw []byte
|
||||
err error
|
||||
)
|
||||
switch {
|
||||
// Read the password from standard in as if it were a file. This should
|
||||
// only be used if the password is piped into lncli from some sort of
|
||||
// password manager. If the user types the password instead, it will be
|
||||
// echoed in the console.
|
||||
case ctx.IsSet("stdin"):
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
pw, err = reader.ReadBytes('\n')
|
||||
|
||||
// Remove carriage return and newline characters.
|
||||
pw = bytes.Trim(pw, "\r\n")
|
||||
|
||||
// Read the password from a terminal by default. This requires the
|
||||
// terminal to be a real tty and will fail if a string is piped into
|
||||
// lncli.
|
||||
default:
|
||||
pw, err = readPassword("Input wallet password: ")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
// Parse the optional recovery window if it is specified. By default,
|
||||
// the recovery window will be 0, indicating no lookahead should be
|
||||
// used.
|
||||
var recoveryWindow int32
|
||||
switch {
|
||||
case ctx.IsSet("recovery_window"):
|
||||
recoveryWindow = int32(ctx.Int64("recovery_window"))
|
||||
case args.Present():
|
||||
window, err := strconv.ParseInt(args.First(), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recoveryWindow = int32(window)
|
||||
}
|
||||
|
||||
req := &lnrpc.UnlockWalletRequest{
|
||||
WalletPassword: pw,
|
||||
RecoveryWindow: recoveryWindow,
|
||||
StatelessInit: ctx.Bool(statelessInitFlag.Name),
|
||||
}
|
||||
_, err = client.UnlockWallet(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nlnd successfully unlocked!")
|
||||
|
||||
// TODO(roasbeef): add ability to accept hex single and multi backups
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var changePasswordCommand = cli.Command{
|
||||
Name: "changepassword",
|
||||
Category: "Startup",
|
||||
Usage: "Change an encrypted wallet's password at startup.",
|
||||
Description: `
|
||||
The changepassword command is used to Change lnd's encrypted wallet's
|
||||
password. It will automatically unlock the daemon if the password change
|
||||
is successful.
|
||||
|
||||
If one did not specify a password for their wallet (running lnd with
|
||||
--noseedbackup), one must restart their daemon without
|
||||
--noseedbackup and use this command. The "current password" field
|
||||
should be left empty.
|
||||
|
||||
If the daemon was originally initialized stateless, then the
|
||||
--stateless_init flag needs to be set for the change password request
|
||||
as well! Otherwise the daemon will generate unencrypted macaroon files
|
||||
in its file system again and possibly leak sensitive information.
|
||||
Changing the password will by default not change the macaroon root key
|
||||
(just re-encrypt the macaroon database with the new password). So all
|
||||
macaroons will still be valid.
|
||||
If one wants to make sure that all previously created macaroons are
|
||||
invalidated, a new macaroon root key can be generated by using the
|
||||
--new_mac_root_key flag.
|
||||
|
||||
After a successful password change with the --stateless_init flag set,
|
||||
the current or new admin macaroon is returned binary serialized in the
|
||||
answer. This answer MUST then be stored somewhere, otherwise
|
||||
all access to the RPC server will be lost and the wallet must be re-
|
||||
created to re-gain access. If the --save_to parameter is set, the
|
||||
macaroon is saved to this file, otherwise it is printed to standard out.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
statelessInitFlag,
|
||||
saveToFlag,
|
||||
cli.BoolFlag{
|
||||
Name: "new_mac_root_key",
|
||||
Usage: "rotate the macaroon root key resulting in " +
|
||||
"all previously created macaroons to be " +
|
||||
"invalidated",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(changePassword),
|
||||
}
|
||||
|
||||
func changePassword(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
currentPw, err := readPassword("Input current wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPw, err := readPassword("Input new wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confirmPw, err := readPassword("Confirm new wallet password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(newPw, confirmPw) {
|
||||
return fmt.Errorf("passwords don't match")
|
||||
}
|
||||
|
||||
// Should the daemon be initialized stateless? Then we expect an answer
|
||||
// with the admin macaroon later. Because the --save_to is related to
|
||||
// stateless init, it doesn't make sense to be set on its own.
|
||||
statelessInit := ctx.Bool(statelessInitFlag.Name)
|
||||
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
|
||||
return fmt.Errorf("cannot set save_to parameter without " +
|
||||
"stateless_init")
|
||||
}
|
||||
|
||||
req := &lnrpc.ChangePasswordRequest{
|
||||
CurrentPassword: currentPw,
|
||||
NewPassword: newPw,
|
||||
StatelessInit: statelessInit,
|
||||
NewMacaroonRootKey: ctx.Bool("new_mac_root_key"),
|
||||
}
|
||||
|
||||
response, err := client.ChangePassword(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if statelessInit {
|
||||
return storeOrPrintAdminMac(ctx, response.AdminMacaroon)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var createWatchOnlyCommand = cli.Command{
|
||||
Name: "createwatchonly",
|
||||
Category: "Startup",
|
||||
ArgsUsage: "accounts-json-file",
|
||||
Usage: "Initialize a watch-only wallet after starting lnd for the " +
|
||||
"first time.",
|
||||
Description: `
|
||||
The create command is used to initialize an lnd wallet from scratch for
|
||||
the very first time, in watch-only mode. Watch-only means, there will be
|
||||
no private keys in lnd's wallet. This is only useful in combination with
|
||||
a remote signer or when lnd should be used as an on-chain wallet with
|
||||
PSBT interaction only.
|
||||
|
||||
This is an interactive command that takes a JSON file as its first and
|
||||
only argument. The JSON is in the same format as the output of the
|
||||
'lncli wallet accounts list' command. This makes it easy to initialize
|
||||
the remote signer with the seed, then export the extended public account
|
||||
keys (xpubs) to import the watch-only wallet.
|
||||
|
||||
Example JSON (non-mandatory or ignored fields are omitted):
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"extended_public_key": "upub5Eep7....",
|
||||
"derivation_path": "m/49'/0'/0'"
|
||||
},
|
||||
{
|
||||
"extended_public_key": "vpub5ZU1PH...",
|
||||
"derivation_path": "m/84'/0'/0'"
|
||||
},
|
||||
{
|
||||
"extended_public_key": "tpubDDXFH...",
|
||||
"derivation_path": "m/1017'/1'/0'"
|
||||
},
|
||||
...
|
||||
{
|
||||
"extended_public_key": "tpubDDXFH...",
|
||||
"derivation_path": "m/1017'/1'/9'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
There must be an account for each of the existing key families that lnd
|
||||
uses internally (currently 0-9, see keychain/derivation.go).
|
||||
|
||||
Read the documentation under docs/remote-signing.md for more information
|
||||
on how to set up a remote signing node over RPC.
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
statelessInitFlag,
|
||||
saveToFlag,
|
||||
},
|
||||
Action: actionDecorator(createWatchOnly),
|
||||
}
|
||||
|
||||
func createWatchOnly(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getWalletUnlockerClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
if ctx.NArg() != 1 {
|
||||
return cli.ShowCommandHelp(ctx, "createwatchonly")
|
||||
}
|
||||
|
||||
// Should the daemon be initialized stateless? Then we expect an answer
|
||||
// with the admin macaroon later. Because the --save_to is related to
|
||||
// stateless init, it doesn't make sense to be set on its own.
|
||||
statelessInit := ctx.Bool(statelessInitFlag.Name)
|
||||
if !statelessInit && ctx.IsSet(saveToFlag.Name) {
|
||||
return fmt.Errorf("cannot set save_to parameter without " +
|
||||
"stateless_init")
|
||||
}
|
||||
|
||||
jsonFile := lncfg.CleanAndExpandPath(ctx.Args().First())
|
||||
jsonBytes, err := os.ReadFile(jsonFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading JSON from file %v: %v",
|
||||
jsonFile, err)
|
||||
}
|
||||
|
||||
jsonAccts := &walletrpc.ListAccountsResponse{}
|
||||
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(jsonBytes, jsonAccts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON: %w", err)
|
||||
}
|
||||
if len(jsonAccts.Accounts) == 0 {
|
||||
return fmt.Errorf("cannot import empty account list")
|
||||
}
|
||||
|
||||
walletPassword, err := capturePassword(
|
||||
"Input wallet password: ", false,
|
||||
walletunlocker.ValidatePassword,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extendedRootKeyBirthday, err := askBirthdayTimestamp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recoveryWindow, err := askRecoveryWindow()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rpcAccounts, err := walletrpc.AccountsToWatchOnly(jsonAccts.Accounts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rpcResp := &lnrpc.WatchOnly{
|
||||
MasterKeyBirthdayTimestamp: extendedRootKeyBirthday,
|
||||
Accounts: rpcAccounts,
|
||||
}
|
||||
|
||||
// We assume that all accounts were exported from the same master root
|
||||
// key. So if one is set, we just forward that. If other accounts should
|
||||
// be watched later on, they should be imported into the watch-only
|
||||
// node, that then also forwards the import request to the remote
|
||||
// signer.
|
||||
for _, acct := range jsonAccts.Accounts {
|
||||
if len(acct.MasterKeyFingerprint) > 0 {
|
||||
rpcResp.MasterKeyFingerprint = acct.MasterKeyFingerprint
|
||||
}
|
||||
}
|
||||
|
||||
initResp, err := client.InitWallet(ctxc, &lnrpc.InitWalletRequest{
|
||||
WalletPassword: walletPassword,
|
||||
WatchOnly: rpcResp,
|
||||
RecoveryWindow: recoveryWindow,
|
||||
StatelessInit: statelessInit,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if statelessInit {
|
||||
return storeOrPrintAdminMac(ctx, initResp.AdminMacaroon)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeOrPrintAdminMac either stores the admin macaroon to a file specified or
|
||||
// prints it to standard out, depending on the user flags set.
|
||||
func storeOrPrintAdminMac(ctx *cli.Context, adminMac []byte) error {
|
||||
// The user specified the optional --save_to parameter. We'll save the
|
||||
// macaroon to that file.
|
||||
if ctx.IsSet(saveToFlag.Name) {
|
||||
macSavePath := lncfg.CleanAndExpandPath(ctx.String(
|
||||
saveToFlag.Name,
|
||||
))
|
||||
err := os.WriteFile(macSavePath, adminMac, 0644)
|
||||
if err != nil {
|
||||
_ = os.Remove(macSavePath)
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Admin macaroon saved to %s\n", macSavePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise we just print it. The user MUST store this macaroon
|
||||
// somewhere so we either save it to a provided file path or just print
|
||||
// it to standard output.
|
||||
fmt.Printf("Admin macaroon: %s\n", hex.EncodeToString(adminMac))
|
||||
return nil
|
||||
}
|
||||
|
||||
func askRecoveryWindow() (int32, error) {
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Printf("Input an optional address look-ahead used to scan "+
|
||||
"for used keys (default %d): ", defaultRecoveryWindow)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
answer = strings.TrimSpace(answer)
|
||||
|
||||
if len(answer) == 0 {
|
||||
return defaultRecoveryWindow, nil
|
||||
}
|
||||
|
||||
lookAhead, err := strconv.ParseInt(answer, 10, 32)
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to parse recovery window: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return int32(lookAhead), nil
|
||||
}
|
||||
}
|
||||
|
||||
func askBirthdayTimestamp() (uint64, error) {
|
||||
for {
|
||||
fmt.Println()
|
||||
fmt.Printf("Input an optional wallet birthday unix timestamp " +
|
||||
"of first block to start scanning from (default 0): ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
answer = strings.TrimSpace(answer)
|
||||
|
||||
if len(answer) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
birthdayTimestamp, err := strconv.ParseUint(answer, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to parse birthday timestamp: %v\n",
|
||||
err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
return birthdayTimestamp, nil
|
||||
}
|
||||
}
|
||||
|
||||
func printCipherSeedWords(mnemonicWords []string) {
|
||||
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
fmt.Println()
|
||||
|
||||
fmt.Println("---------------BEGIN LND CIPHER SEED---------------")
|
||||
|
||||
numCols := 4
|
||||
colWords := monowidthColumns(mnemonicWords, numCols)
|
||||
for i := 0; i < len(colWords); i += numCols {
|
||||
fmt.Printf("%2d. %3s %2d. %3s %2d. %3s %2d. %3s\n",
|
||||
i+1, colWords[i], i+2, colWords[i+1], i+3,
|
||||
colWords[i+2], i+4, colWords[i+3])
|
||||
}
|
||||
|
||||
fmt.Println("---------------END LND CIPHER SEED-----------------")
|
||||
|
||||
fmt.Println("\n!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
||||
"RESTORE THE WALLET!!!")
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,122 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestParseChanPoint tests parseChanPoint with various
|
||||
// valid and invalid input values and verifies the output.
|
||||
func TestParseChanPoint(t *testing.T) {
|
||||
testCases := []struct {
|
||||
channelPoinStr string
|
||||
channelPointIsNil bool
|
||||
outputIndex uint32
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"24581424081379576b4a7580ace91db10925d996a2a8d45c8034" +
|
||||
"3a5a467dc0bc:0",
|
||||
false,
|
||||
0,
|
||||
nil,
|
||||
}, {
|
||||
"24581424081379576b4a7580ace91db10925d996a2a8d45c8034" +
|
||||
"3a5a467dc0bc:4",
|
||||
false,
|
||||
4,
|
||||
nil,
|
||||
}, {
|
||||
":",
|
||||
true,
|
||||
0,
|
||||
errBadChanPoint,
|
||||
}, {
|
||||
":0",
|
||||
true,
|
||||
0,
|
||||
errBadChanPoint,
|
||||
}, {
|
||||
"24581424081379576b4a7580ace91db10925d996a2a8d45c8034" +
|
||||
"3a5a467dc0bc:",
|
||||
true,
|
||||
0,
|
||||
errBadChanPoint,
|
||||
}, {
|
||||
"24581424081379576b4a7580ace91db10925d996a2a8d45c8034" +
|
||||
"3a5a467dc0bc:string",
|
||||
true,
|
||||
0,
|
||||
strconv.ErrSyntax,
|
||||
}, {
|
||||
"not_hex:0",
|
||||
true,
|
||||
0,
|
||||
hex.InvalidByteError('n'),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cp, err := parseChanPoint(tc.channelPoinStr)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
|
||||
require.Equal(t, tc.channelPointIsNil, cp == nil)
|
||||
if !tc.channelPointIsNil {
|
||||
require.Equal(t, tc.outputIndex, cp.OutputIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseTimeLockDelta tests parseTimeLockDelta with various
|
||||
// valid and invalid input values and verifies the output.
|
||||
func TestParseTimeLockDelta(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
timeLockDeltaStr string
|
||||
expectedTimeLockDelta uint16
|
||||
expectErr bool
|
||||
expectedErrContent string
|
||||
}{
|
||||
{
|
||||
timeLockDeltaStr: "-1",
|
||||
expectErr: true,
|
||||
expectedErrContent: fmt.Sprintf(
|
||||
"failed to parse time_lock_delta: %d "+
|
||||
"to uint64", -1,
|
||||
),
|
||||
},
|
||||
{
|
||||
timeLockDeltaStr: "0",
|
||||
},
|
||||
{
|
||||
timeLockDeltaStr: "3",
|
||||
expectedTimeLockDelta: 3,
|
||||
},
|
||||
{
|
||||
timeLockDeltaStr: strconv.FormatUint(
|
||||
uint64(math.MaxUint16), 10,
|
||||
),
|
||||
expectedTimeLockDelta: math.MaxUint16,
|
||||
},
|
||||
{
|
||||
timeLockDeltaStr: "18446744073709551616",
|
||||
expectErr: true,
|
||||
expectedErrContent: fmt.Sprint(
|
||||
"failed to parse time_lock_delta:" +
|
||||
" 18446744073709551616 to uint64"),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
timeLockDelta, err := parseTimeLockDelta(tc.timeLockDeltaStr)
|
||||
require.Equal(t, tc.expectedTimeLockDelta, timeLockDelta)
|
||||
if tc.expectErr {
|
||||
require.ErrorContains(t, err, tc.expectedErrContent)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/devrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// devCommands will return the set of commands to enable for devrpc builds.
|
||||
func devCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "importgraph",
|
||||
Category: "Development",
|
||||
Description: "Imports graph from describegraph JSON",
|
||||
Usage: "Import the network graph.",
|
||||
ArgsUsage: "graph-json-file",
|
||||
Action: actionDecorator(importGraph),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getDevClient(ctx *cli.Context) (devrpc.DevClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
return devrpc.NewDevClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func importGraph(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getDevClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
jsonFile := lncfg.CleanAndExpandPath(ctx.Args().First())
|
||||
jsonBytes, err := os.ReadFile(jsonFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading JSON from file %v: %v",
|
||||
jsonFile, err)
|
||||
}
|
||||
|
||||
jsonGraph := &lnrpc.ChannelGraph{}
|
||||
err = lnrpc.ProtoJSONUnmarshalOpts.Unmarshal(jsonBytes, jsonGraph)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing JSON: %w", err)
|
||||
}
|
||||
res, err := client.ImportGraph(ctxc, jsonGraph)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(res)
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// devCommands will return nil for non-devrpc builds.
|
||||
func devCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
//go:build invoicesrpc
|
||||
// +build invoicesrpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// invoicesCommands will return nil for non-invoicesrpc builds.
|
||||
func invoicesCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
cancelInvoiceCommand,
|
||||
addHoldInvoiceCommand,
|
||||
settleInvoiceCommand,
|
||||
}
|
||||
}
|
||||
|
||||
func getInvoicesClient(ctx *cli.Context) (invoicesrpc.InvoicesClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return invoicesrpc.NewInvoicesClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var settleInvoiceCommand = cli.Command{
|
||||
Name: "settleinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Reveal a preimage and use it to settle the corresponding invoice.",
|
||||
Description: `
|
||||
Todo.`,
|
||||
ArgsUsage: "preimage",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "preimage",
|
||||
Usage: "the hex-encoded preimage (32 byte) which will " +
|
||||
"allow settling an incoming HTLC payable to this " +
|
||||
"preimage.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(settleInvoice),
|
||||
}
|
||||
|
||||
func settleInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
preimage []byte
|
||||
err error
|
||||
)
|
||||
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getInvoicesClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("preimage"):
|
||||
preimage, err = hex.DecodeString(ctx.String("preimage"))
|
||||
case args.Present():
|
||||
preimage, err = hex.DecodeString(args.First())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse preimage: %w", err)
|
||||
}
|
||||
|
||||
invoice := &invoicesrpc.SettleInvoiceMsg{
|
||||
Preimage: preimage,
|
||||
}
|
||||
|
||||
resp, err := client.SettleInvoice(ctxc, invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var cancelInvoiceCommand = cli.Command{
|
||||
Name: "cancelinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Cancels a (hold) invoice.",
|
||||
Description: `
|
||||
Todo.`,
|
||||
ArgsUsage: "paymenthash",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "paymenthash",
|
||||
Usage: "the hex-encoded payment hash (32 byte) for which the " +
|
||||
"corresponding invoice will be canceled.",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(cancelInvoice),
|
||||
}
|
||||
|
||||
func cancelInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
paymentHash []byte
|
||||
err error
|
||||
)
|
||||
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getInvoicesClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
switch {
|
||||
case ctx.IsSet("paymenthash"):
|
||||
paymentHash, err = hex.DecodeString(ctx.String("paymenthash"))
|
||||
case args.Present():
|
||||
paymentHash, err = hex.DecodeString(args.First())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse preimage: %w", err)
|
||||
}
|
||||
|
||||
invoice := &invoicesrpc.CancelInvoiceMsg{
|
||||
PaymentHash: paymentHash,
|
||||
}
|
||||
|
||||
resp, err := client.CancelInvoice(ctxc, invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var addHoldInvoiceCommand = cli.Command{
|
||||
Name: "addholdinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Add a new hold invoice.",
|
||||
Description: `
|
||||
Add a new invoice, expressing intent for a future payment.
|
||||
|
||||
Invoices without an amount can be created by not supplying any
|
||||
parameters or providing an amount of 0. These invoices allow the payer
|
||||
to specify the amount of satoshis they wish to send.`,
|
||||
ArgsUsage: "hash [amt]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "memo",
|
||||
Usage: "a description of the payment to attach along " +
|
||||
"with the invoice (default=\"\")",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "amt",
|
||||
Usage: "the amt of satoshis in this invoice",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "amt_msat",
|
||||
Usage: "the amt of millisatoshis in this invoice",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "description_hash",
|
||||
Usage: "SHA-256 hash of the description of the payment. " +
|
||||
"Used if the purpose of payment cannot naturally " +
|
||||
"fit within the memo. If provided this will be " +
|
||||
"used instead of the description(memo) field in " +
|
||||
"the encoded invoice.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "fallback_addr",
|
||||
Usage: "fallback on-chain address that can be used in " +
|
||||
"case the lightning payment fails",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "expiry",
|
||||
Usage: "the invoice's expiry time in seconds. If not " +
|
||||
"specified, an expiry of " +
|
||||
"86400 seconds (24 hours) is implied.",
|
||||
},
|
||||
cli.Uint64Flag{
|
||||
Name: "cltv_expiry_delta",
|
||||
Usage: "The minimum CLTV delta to use for the final " +
|
||||
"hop. If this is set to 0, the default value " +
|
||||
"is used. The default value for " +
|
||||
"cltv_expiry_delta is configured by the " +
|
||||
"'bitcoin.timelockdelta' option.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "private",
|
||||
Usage: "encode routing hints in the invoice with " +
|
||||
"private channels in order to assist the " +
|
||||
"payer in reaching you",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(addHoldInvoice),
|
||||
}
|
||||
|
||||
func addHoldInvoice(ctx *cli.Context) error {
|
||||
var (
|
||||
descHash []byte
|
||||
err error
|
||||
)
|
||||
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getInvoicesClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
args := ctx.Args()
|
||||
if ctx.NArg() == 0 {
|
||||
cli.ShowCommandHelp(ctx, "addholdinvoice")
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := hex.DecodeString(args.First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse hash: %w", err)
|
||||
}
|
||||
|
||||
args = args.Tail()
|
||||
|
||||
amt := ctx.Int64("amt")
|
||||
amtMsat := ctx.Int64("amt_msat")
|
||||
|
||||
if !ctx.IsSet("amt") && !ctx.IsSet("amt_msat") && args.Present() {
|
||||
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to decode amt argument: %w",
|
||||
err)
|
||||
}
|
||||
}
|
||||
|
||||
descHash, err = hex.DecodeString(ctx.String("description_hash"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse description_hash: %w", err)
|
||||
}
|
||||
|
||||
invoice := &invoicesrpc.AddHoldInvoiceRequest{
|
||||
Memo: ctx.String("memo"),
|
||||
Hash: hash,
|
||||
Value: amt,
|
||||
ValueMsat: amtMsat,
|
||||
DescriptionHash: descHash,
|
||||
FallbackAddr: ctx.String("fallback_addr"),
|
||||
Expiry: ctx.Int64("expiry"),
|
||||
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
|
||||
Private: ctx.Bool("private"),
|
||||
}
|
||||
|
||||
resp, err := client.AddHoldInvoice(ctxc, invoice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !invoicesrpc
|
||||
// +build !invoicesrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// invoicesCommands will return nil for non-invoicesrpc builds.
|
||||
func invoicesCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcwallet/snacl"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
encryptionPrefix = "snacl:"
|
||||
)
|
||||
|
||||
// getPasswordFn is a function that asks the user to type a password after
|
||||
// presenting it the given prompt.
|
||||
type getPasswordFn func(prompt string) ([]byte, error)
|
||||
|
||||
// macaroonJar is a struct that represents all macaroons of a profile.
|
||||
type macaroonJar struct {
|
||||
Default string `json:"default,omitempty"`
|
||||
Timeout int64 `json:"timeout,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Jar []*macaroonEntry `json:"jar"`
|
||||
}
|
||||
|
||||
// macaroonEntry is a struct that represents a single macaroon. Its content can
|
||||
// either be cleartext (hex encoded) or encrypted (snacl secretbox).
|
||||
type macaroonEntry struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// loadMacaroon returns the fully usable macaroon instance from the entry. This
|
||||
// detects whether the macaroon needs to be decrypted and does so if necessary.
|
||||
// An encrypted macaroon that needs to be decrypted will prompt for the user's
|
||||
// password by calling the provided password callback. Normally that should
|
||||
// result in the user being prompted for the password in the terminal.
|
||||
func (e *macaroonEntry) loadMacaroon(
|
||||
pwCallback getPasswordFn) (*macaroon.Macaroon, error) {
|
||||
|
||||
if len(strings.TrimSpace(e.Data)) == 0 {
|
||||
return nil, fmt.Errorf("macaroon data is empty")
|
||||
}
|
||||
|
||||
var (
|
||||
macBytes []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// Either decrypt or simply decode the macaroon data.
|
||||
if strings.HasPrefix(e.Data, encryptionPrefix) {
|
||||
parts := strings.Split(e.Data, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("invalid encrypted macaroon " +
|
||||
"format, expected 'snacl:<key_base64>:" +
|
||||
"<encrypted_macaroon_base64>'")
|
||||
}
|
||||
|
||||
pw, err := pwCallback("Enter macaroon encryption password: ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read password from "+
|
||||
"terminal: %v", err)
|
||||
}
|
||||
|
||||
macBytes, err = decryptMacaroon(parts[1], parts[2], pw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decrypt macaroon: %w",
|
||||
err)
|
||||
}
|
||||
} else {
|
||||
macBytes, err = hex.DecodeString(e.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to hex decode "+
|
||||
"macaroon: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the macaroon data into its native struct.
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode macaroon: %w", err)
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
|
||||
// storeMacaroon stores a native macaroon instance to the entry. If a non-nil
|
||||
// password is provided, then the macaroon is encrypted with that password. If
|
||||
// not, the macaroon is stored as plain text.
|
||||
func (e *macaroonEntry) storeMacaroon(mac *macaroon.Macaroon, pw []byte) error {
|
||||
// First of all, make sure we can serialize the macaroon.
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal macaroon: %w", err)
|
||||
}
|
||||
|
||||
if len(pw) == 0 {
|
||||
e.Data = hex.EncodeToString(macBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The user did set a password. Let's derive an encryption key from it.
|
||||
key, err := snacl.NewSecretKey(
|
||||
&pw, snacl.DefaultN, snacl.DefaultR, snacl.DefaultP,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create encryption key: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt the macaroon data with the derived key and store it in the
|
||||
// human readable format snacl:<key_base64>:<encrypted_macaroon_base64>.
|
||||
encryptedMac, err := key.Encrypt(macBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to encrypt macaroon: %w", err)
|
||||
}
|
||||
|
||||
keyB64 := base64.StdEncoding.EncodeToString(key.Marshal())
|
||||
dataB64 := base64.StdEncoding.EncodeToString(encryptedMac)
|
||||
e.Data = fmt.Sprintf("%s%s:%s", encryptionPrefix, keyB64, dataB64)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decryptMacaroon decrypts the cipher text macaroon by using the serialized
|
||||
// encryption key and the password.
|
||||
func decryptMacaroon(keyB64, dataB64 string, pw []byte) ([]byte, error) {
|
||||
// Base64 decode both the marshalled encryption key and macaroon data.
|
||||
keyData, err := base64.StdEncoding.DecodeString(keyB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not base64 decode encryption "+
|
||||
"key: %v", err)
|
||||
}
|
||||
encryptedMac, err := base64.StdEncoding.DecodeString(dataB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not base64 decode macaroon "+
|
||||
"data: %v", err)
|
||||
}
|
||||
|
||||
// Unmarshal the encryption key and ask the user for the password.
|
||||
key := &snacl.SecretKey{}
|
||||
err = key.Unmarshal(keyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal encryption key: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
// Derive the final encryption key and then decrypt the macaroon with
|
||||
// it.
|
||||
err = key.DeriveKey(&pw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not derive encryption key, "+
|
||||
"possibly due to incorrect password: %v", err)
|
||||
}
|
||||
macBytes, err := key.Decrypt(encryptedMac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not decrypt macaroon data: %w",
|
||||
err)
|
||||
}
|
||||
return macBytes, nil
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
dummyMacStr = "0201047465737402067788991234560000062052d26ed139ea5af8" +
|
||||
"3e675500c4ccb2471f62191b745bab820f129e5588a255d2"
|
||||
dummyMac, _ = hex.DecodeString(dummyMacStr)
|
||||
encryptedEntry = &macaroonEntry{
|
||||
Name: "encryptedMac",
|
||||
Data: "snacl:exX8xbUOb6Gih88ybL2jZGo+DBDPU2tYKkvo0eVVmbDGDoFP" +
|
||||
"zlv5xvqNK5eml0LKLcB8LdZRw43qXK1W2OLs/gBAAAAAAAAACAAA" +
|
||||
"AAAAAAABAAAAAAAAAA==:C8TN/aDOvSLiBCX+IdoPTx+UUWhVdGj" +
|
||||
"NQvbcaWp+KXQWqPfpRZpjJQ6B2PDx5mJxImcezJGPx8ShAqMdxWe" +
|
||||
"l2precU+1cOjk7HQFkYuu943eJ00s6JerAY+ssg==",
|
||||
}
|
||||
plaintextEntry = &macaroonEntry{
|
||||
Name: "plaintextMac",
|
||||
Data: dummyMacStr,
|
||||
}
|
||||
|
||||
testPassword = []byte("S3curePazzw0rd")
|
||||
pwCallback = func(string) ([]byte, error) {
|
||||
return testPassword, nil
|
||||
}
|
||||
noPwCallback = func(string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
)
|
||||
|
||||
// TestMacaroonJarEncrypted tests that a macaroon can be stored and retrieved
|
||||
// safely by encrypting/decrypting it with a password.
|
||||
func TestMacaroonJarEncrypted(t *testing.T) {
|
||||
// Create a new macaroon entry from the dummy macaroon and encrypt it
|
||||
// with the test password.
|
||||
newEntry := &macaroonEntry{
|
||||
Name: "encryptedMac",
|
||||
}
|
||||
err := newEntry.storeMacaroon(toMacaroon(t, dummyMac), testPassword)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now decrypt it again and make sure we get the same content back.
|
||||
mac, err := newEntry.loadMacaroon(pwCallback)
|
||||
require.NoError(t, err)
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyMac, macBytes)
|
||||
|
||||
// The encrypted data of the entry we just created shouldn't be the
|
||||
// same as our test entry because of the salt snacl uses.
|
||||
require.NotEqual(t, encryptedEntry.Data, newEntry.Data)
|
||||
|
||||
// Decrypt the hard coded test entry and make sure the decrypted content
|
||||
// matches our created entry.
|
||||
mac, err = encryptedEntry.loadMacaroon(pwCallback)
|
||||
require.NoError(t, err)
|
||||
macBytes, err = mac.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyMac, macBytes)
|
||||
}
|
||||
|
||||
// TestMacaroonJarPlaintext tests that a macaroon can be stored and retrieved
|
||||
// as plaintext as well.
|
||||
func TestMacaroonJarPlaintext(t *testing.T) {
|
||||
// Create a new macaroon entry from the dummy macaroon and encrypt it
|
||||
// with the test password.
|
||||
newEntry := &macaroonEntry{
|
||||
Name: "plaintextMac",
|
||||
}
|
||||
err := newEntry.storeMacaroon(toMacaroon(t, dummyMac), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now decrypt it again and make sure we get the same content back.
|
||||
mac, err := newEntry.loadMacaroon(noPwCallback)
|
||||
require.NoError(t, err)
|
||||
macBytes, err := mac.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyMac, macBytes)
|
||||
require.Equal(t, plaintextEntry.Data, newEntry.Data)
|
||||
|
||||
// Load the hard coded plaintext test entry and make sure the loaded
|
||||
// content matches our created entry.
|
||||
mac, err = plaintextEntry.loadMacaroon(noPwCallback)
|
||||
require.NoError(t, err)
|
||||
macBytes, err = mac.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, dummyMac, macBytes)
|
||||
}
|
||||
|
||||
func toMacaroon(t *testing.T, macData []byte) *macaroon.Macaroon {
|
||||
mac := &macaroon.Macaroon{}
|
||||
err := mac.UnmarshalBinary(macData)
|
||||
require.NoError(t, err)
|
||||
|
||||
return mac
|
||||
}
|
||||
+3
-586
@@ -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()
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
//go:build neutrinorpc
|
||||
// +build neutrinorpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func getNeutrinoKitClient(ctx *cli.Context) (neutrinorpc.NeutrinoKitClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return neutrinorpc.NewNeutrinoKitClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var getNeutrinoStatusCommand = cli.Command{
|
||||
Name: "status",
|
||||
Usage: "Returns the status of the running neutrino instance.",
|
||||
Category: "Neutrino",
|
||||
Description: "Returns the status of the light client neutrino " +
|
||||
"instance, along with height and hash of the best block, and " +
|
||||
"a list of connected peers.",
|
||||
Action: actionDecorator(getNeutrinoStatus),
|
||||
}
|
||||
|
||||
func getNeutrinoStatus(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.StatusRequest{}
|
||||
|
||||
resp, err := client.Status(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var addPeerCommand = cli.Command{
|
||||
Name: "addpeer",
|
||||
Usage: "Add a peer.",
|
||||
Category: "Neutrino",
|
||||
Description: "Adds a new peer that has already been connected to the " +
|
||||
"server.",
|
||||
ArgsUsage: "address",
|
||||
Action: actionDecorator(addNeutrinoPeer),
|
||||
}
|
||||
|
||||
func addNeutrinoPeer(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "addpeer")
|
||||
}
|
||||
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.AddPeerRequest{
|
||||
PeerAddrs: ctx.Args().First(),
|
||||
}
|
||||
|
||||
// Add a peer to the neutrino server.
|
||||
resp, err := client.AddPeer(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var disconnectPeerCommand = cli.Command{
|
||||
Name: "disconnectpeer",
|
||||
Usage: "Disconnect a peer.",
|
||||
Category: "Neutrino",
|
||||
Description: "Disconnects a peer by target address. Both outbound and" +
|
||||
"inbound nodes will be searched for the target node. An error " +
|
||||
"message will be returned if the peer was not found.",
|
||||
ArgsUsage: "address",
|
||||
Action: actionDecorator(disconnectNeutrinoPeer),
|
||||
}
|
||||
|
||||
func disconnectNeutrinoPeer(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "disconnectpeer")
|
||||
}
|
||||
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.DisconnectPeerRequest{
|
||||
PeerAddrs: ctx.Args().First(),
|
||||
}
|
||||
|
||||
// Disconnect a peer to the neutrino server.
|
||||
resp, err := client.DisconnectPeer(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var isBannedCommand = cli.Command{
|
||||
Name: "isbanned",
|
||||
Usage: "Get ban status.",
|
||||
Category: "Neutrino",
|
||||
Description: "Returns true if the peer is banned, otherwise false.",
|
||||
ArgsUsage: "address",
|
||||
Action: actionDecorator(isBanned),
|
||||
}
|
||||
|
||||
func isBanned(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if ctx.NArg() != 1 {
|
||||
return cli.ShowCommandHelp(ctx, "isbanned")
|
||||
}
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.IsBannedRequest{
|
||||
PeerAddrs: ctx.Args().First(),
|
||||
}
|
||||
|
||||
// Check if the peer is banned.
|
||||
resp, err := client.IsBanned(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getBlockHeaderNeutrinoCommand = cli.Command{
|
||||
Name: "getblockheader",
|
||||
Usage: "Get a block header.",
|
||||
Category: "Neutrino",
|
||||
Description: "Returns a block header with a particular block hash.",
|
||||
ArgsUsage: "hash",
|
||||
Action: actionDecorator(getBlockHeaderNeutrino),
|
||||
}
|
||||
|
||||
func getBlockHeaderNeutrino(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
args := ctx.Args()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if !args.Present() {
|
||||
return cli.ShowCommandHelp(ctx, "getblockheader")
|
||||
}
|
||||
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.GetBlockHeaderRequest{
|
||||
Hash: ctx.Args().First(),
|
||||
}
|
||||
|
||||
resp, err := client.GetBlockHeader(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getCFilterCommand = cli.Command{
|
||||
Name: "getcfilter",
|
||||
Usage: "Get a compact filter.",
|
||||
Category: "Neutrino",
|
||||
Description: "Returns a compact filter of a particular block.",
|
||||
ArgsUsage: "hash",
|
||||
Action: actionDecorator(getCFilter),
|
||||
}
|
||||
|
||||
func getCFilter(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
args := ctx.Args()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if !args.Present() {
|
||||
return cli.ShowCommandHelp(ctx, "getcfilter")
|
||||
}
|
||||
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &neutrinorpc.GetCFilterRequest{Hash: args.First()}
|
||||
|
||||
resp, err := client.GetCFilter(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// neutrinoCommands will return the set of commands to enable for neutrinorpc
|
||||
// builds.
|
||||
func neutrinoCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "neutrino",
|
||||
Category: "Neutrino",
|
||||
Usage: "Interact with a running neutrino instance.",
|
||||
Description: "",
|
||||
Subcommands: []cli.Command{
|
||||
getNeutrinoStatusCommand,
|
||||
addPeerCommand,
|
||||
disconnectPeerCommand,
|
||||
isBannedCommand,
|
||||
getBlockHeaderNeutrinoCommand,
|
||||
getCFilterCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !neutrinorpc
|
||||
// +build !neutrinorpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// neutrinoCommands will return nil for non-neutrinorpc builds.
|
||||
func neutrinoCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
//go:build peersrpc
|
||||
// +build peersrpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnrpc/peersrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// peersCommands will return the set of commands to enable for peersrpc
|
||||
// builds.
|
||||
func peersCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "peers",
|
||||
Category: "Peers",
|
||||
Usage: "Interacts with the other nodes of the " +
|
||||
"network",
|
||||
Subcommands: []cli.Command{
|
||||
updateNodeAnnouncementCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getPeersClient(ctx *cli.Context) (peersrpc.PeersClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
return peersrpc.NewPeersClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var updateNodeAnnouncementCommand = cli.Command{
|
||||
Name: "updatenodeannouncement",
|
||||
Category: "Peers",
|
||||
Usage: "update and broadcast a new node announcement",
|
||||
Description: `
|
||||
Update the node's information and broadcast a new node announcement.
|
||||
|
||||
Add or remove addresses where your node can be reached at, change the
|
||||
alias/color of the node or enable/disable supported feature bits without
|
||||
restarting the node. A node announcement with the new information will
|
||||
be created and brodcasted to the network.`,
|
||||
ArgsUsage: "[--address_add=] [--address_remove=] [--alias=] " +
|
||||
"[--color=] [--feature_bit_add=] [--feature_bit_remove=]",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringSliceFlag{
|
||||
Name: "address_add",
|
||||
Usage: "a new address that should be added to the " +
|
||||
"set of URIs of this node. Can be set " +
|
||||
"multiple times in the same command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "address_remove",
|
||||
Usage: "an address that needs to be removed from the " +
|
||||
"set of URIs of this node. Can be set " +
|
||||
"multiple times in the same command",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "alias",
|
||||
Usage: "the new alias for this node, e.g. \"bob\"",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "color",
|
||||
Usage: "the new color for this node, e.g. #c42a81",
|
||||
},
|
||||
cli.Int64SliceFlag{
|
||||
Name: "feature_bit_add",
|
||||
Usage: "a feature bit index that needs to be enabled. " +
|
||||
"Can be set multiple times in the same command",
|
||||
},
|
||||
cli.Int64SliceFlag{
|
||||
Name: "feature_bit_remove",
|
||||
Usage: "a feature bit that needs to be disabled. " +
|
||||
"Can be set multiple times in the same command",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(updateNodeAnnouncement),
|
||||
}
|
||||
|
||||
func updateNodeAnnouncement(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getPeersClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
change := false
|
||||
|
||||
req := &peersrpc.NodeAnnouncementUpdateRequest{}
|
||||
|
||||
if ctx.IsSet("address_add") {
|
||||
change = true
|
||||
for _, addr := range ctx.StringSlice("address_add") {
|
||||
action := &peersrpc.UpdateAddressAction{
|
||||
Action: peersrpc.UpdateAction_ADD,
|
||||
Address: addr,
|
||||
}
|
||||
req.AddressUpdates = append(req.AddressUpdates, action)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.IsSet("address_remove") {
|
||||
change = true
|
||||
for _, addr := range ctx.StringSlice("address_remove") {
|
||||
action := &peersrpc.UpdateAddressAction{
|
||||
Action: peersrpc.UpdateAction_REMOVE,
|
||||
Address: addr,
|
||||
}
|
||||
req.AddressUpdates = append(req.AddressUpdates, action)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.IsSet("alias") {
|
||||
change = true
|
||||
req.Alias = ctx.String("alias")
|
||||
}
|
||||
|
||||
if ctx.IsSet("color") {
|
||||
change = true
|
||||
req.Color = ctx.String("color")
|
||||
}
|
||||
|
||||
if ctx.IsSet("feature_bit_add") {
|
||||
change = true
|
||||
for _, feature := range ctx.Int64Slice("feature_bit_add") {
|
||||
action := &peersrpc.UpdateFeatureAction{
|
||||
Action: peersrpc.UpdateAction_ADD,
|
||||
FeatureBit: lnrpc.FeatureBit(feature),
|
||||
}
|
||||
req.FeatureUpdates = append(req.FeatureUpdates, action)
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.IsSet("feature_bit_remove") {
|
||||
change = true
|
||||
for _, feature := range ctx.Int64Slice("feature_bit_remove") {
|
||||
action := &peersrpc.UpdateFeatureAction{
|
||||
Action: peersrpc.UpdateAction_REMOVE,
|
||||
FeatureBit: lnrpc.FeatureBit(feature),
|
||||
}
|
||||
req.FeatureUpdates = append(req.FeatureUpdates, action)
|
||||
}
|
||||
}
|
||||
|
||||
if !change {
|
||||
return fmt.Errorf("no changes for the node information " +
|
||||
"detected")
|
||||
}
|
||||
|
||||
resp, err := client.UpdateNodeAnnouncement(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !peersrpc
|
||||
// +build !peersrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// peersCommands will return nil for non-peersrpc builds.
|
||||
func peersCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/macaroon.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoProfileFile = errors.New("no profile file found")
|
||||
)
|
||||
|
||||
// profileEntry is a struct that represents all settings for one specific
|
||||
// profile.
|
||||
type profileEntry struct {
|
||||
Name string `json:"name"`
|
||||
RPCServer string `json:"rpcserver"`
|
||||
LndDir string `json:"lnddir"`
|
||||
Network string `json:"network"`
|
||||
NoMacaroons bool `json:"no-macaroons,omitempty"` // nolint:tagliatelle
|
||||
TLSCert string `json:"tlscert"`
|
||||
Macaroons *macaroonJar `json:"macaroons"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
Insecure bool `json:"insecure,omitempty"`
|
||||
}
|
||||
|
||||
// cert returns the profile's TLS certificate as a x509 certificate pool.
|
||||
func (e *profileEntry) cert() (*x509.CertPool, error) {
|
||||
if e.TLSCert == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM([]byte(e.TLSCert)) {
|
||||
return nil, fmt.Errorf("credentials: failed to append " +
|
||||
"certificate")
|
||||
}
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// getGlobalOptions returns the global connection options. If a profile file
|
||||
// exists, these global options might be read from a predefined profile. If no
|
||||
// profile exists, the global options from the command line are returned as an
|
||||
// ephemeral profile entry.
|
||||
func getGlobalOptions(ctx *cli.Context, skipMacaroons bool) (*profileEntry,
|
||||
error) {
|
||||
|
||||
var profileName string
|
||||
|
||||
// Try to load the default profile file and depending on its existence
|
||||
// what profile to use.
|
||||
f, err := loadProfileFile(defaultProfileFile)
|
||||
switch {
|
||||
// The legacy case where no profile file exists and the user also didn't
|
||||
// request to use one. We only consider the global options here.
|
||||
case err == errNoProfileFile && !ctx.GlobalIsSet("profile"):
|
||||
return profileFromContext(ctx, false, skipMacaroons)
|
||||
|
||||
// The file doesn't exist but the user specified an explicit profile.
|
||||
case err == errNoProfileFile && ctx.GlobalIsSet("profile"):
|
||||
return nil, fmt.Errorf("profile file %s does not exist",
|
||||
defaultProfileFile)
|
||||
|
||||
// There is a file but we couldn't read/parse it.
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("could not read profile file %s: "+
|
||||
"%v", defaultProfileFile, err)
|
||||
|
||||
// The user explicitly disabled the use of profiles for this command by
|
||||
// setting the flag to an empty string. We fall back to the default/old
|
||||
// behavior.
|
||||
case ctx.GlobalIsSet("profile") && ctx.GlobalString("profile") == "":
|
||||
return profileFromContext(ctx, false, skipMacaroons)
|
||||
|
||||
// There is a file, but no default profile is specified. The user also
|
||||
// didn't specify a profile to use so we fall back to the default/old
|
||||
// behavior.
|
||||
case !ctx.GlobalIsSet("profile") && len(f.Default) == 0:
|
||||
return profileFromContext(ctx, false, skipMacaroons)
|
||||
|
||||
// The user didn't specify a profile but there is a default one defined.
|
||||
case !ctx.GlobalIsSet("profile") && len(f.Default) > 0:
|
||||
profileName = f.Default
|
||||
|
||||
// The user specified a specific profile to use.
|
||||
case ctx.GlobalIsSet("profile"):
|
||||
profileName = ctx.GlobalString("profile")
|
||||
}
|
||||
|
||||
// If we got to here, we do have a profile file and know the name of the
|
||||
// profile to use. Now we just need to make sure it does exist.
|
||||
for _, prof := range f.Profiles {
|
||||
if prof.Name == profileName {
|
||||
return prof, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("profile '%s' not found in file %s", profileName,
|
||||
defaultProfileFile)
|
||||
}
|
||||
|
||||
// profileFromContext creates an ephemeral profile entry from the global options
|
||||
// set in the CLI context.
|
||||
func profileFromContext(ctx *cli.Context, store, skipMacaroons bool) (
|
||||
*profileEntry, error) {
|
||||
|
||||
// Parse the paths of the cert and macaroon. This will validate the
|
||||
// chain and network value as well.
|
||||
tlsCertPath, macPath, err := extractPathArgs(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
insecure := ctx.GlobalBool("insecure")
|
||||
|
||||
// Load the certificate file now, if specified. We store it as plain PEM
|
||||
// directly.
|
||||
var tlsCert []byte
|
||||
if tlsCertPath != "" && !insecure {
|
||||
var err error
|
||||
tlsCert, err = os.ReadFile(tlsCertPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load TLS cert "+
|
||||
"file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
metadata := make(map[string]string)
|
||||
for _, m := range ctx.GlobalStringSlice("metadata") {
|
||||
pair := strings.Split(m, ":")
|
||||
if len(pair) != 2 {
|
||||
return nil, fmt.Errorf("invalid format for metadata " +
|
||||
"flag; expected \"key:value\"")
|
||||
}
|
||||
|
||||
metadata[pair[0]] = pair[1]
|
||||
}
|
||||
|
||||
entry := &profileEntry{
|
||||
RPCServer: ctx.GlobalString("rpcserver"),
|
||||
LndDir: lncfg.CleanAndExpandPath(
|
||||
ctx.GlobalString("lnddir"),
|
||||
),
|
||||
Network: ctx.GlobalString("network"),
|
||||
NoMacaroons: ctx.GlobalBool("no-macaroons"),
|
||||
TLSCert: string(tlsCert),
|
||||
Metadata: metadata,
|
||||
Insecure: insecure,
|
||||
}
|
||||
|
||||
// If we aren't using macaroons in general (flag --no-macaroons) or
|
||||
// don't need macaroons for this command (wallet unlocker), we can now
|
||||
// return already.
|
||||
if skipMacaroons || ctx.GlobalBool("no-macaroons") {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// Now load and possibly encrypt the macaroon file.
|
||||
macBytes, err := os.ReadFile(macPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read macaroon path (check "+
|
||||
"the network setting!): %v", err)
|
||||
}
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err = mac.UnmarshalBinary(macBytes); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode macaroon: %w", err)
|
||||
}
|
||||
|
||||
var pw []byte
|
||||
if store {
|
||||
// Read a password from the terminal. If it's empty, we won't
|
||||
// encrypt the macaroon and store it plaintext.
|
||||
pw, err = capturePassword(
|
||||
"Enter password to encrypt macaroon with or leave "+
|
||||
"blank to store in plaintext: ", true,
|
||||
walletunlocker.ValidatePassword,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get encryption "+
|
||||
"password: %v", err)
|
||||
}
|
||||
}
|
||||
macEntry := &macaroonEntry{}
|
||||
if err = macEntry.storeMacaroon(mac, pw); err != nil {
|
||||
return nil, fmt.Errorf("unable to store macaroon: %w", err)
|
||||
}
|
||||
|
||||
// We determine the name of the macaroon from the file itself but cut
|
||||
// off the ".macaroon" at the end.
|
||||
macEntry.Name = path.Base(macPath)
|
||||
if path.Ext(macEntry.Name) == "macaroon" {
|
||||
macEntry.Name = strings.TrimSuffix(macEntry.Name, ".macaroon")
|
||||
}
|
||||
|
||||
// Now that we have the macaroon jar as well, let's return the entry
|
||||
// with all the values populated.
|
||||
entry.Macaroons = &macaroonJar{
|
||||
Default: macEntry.Name,
|
||||
Timeout: ctx.GlobalInt64("macaroontimeout"),
|
||||
IP: ctx.GlobalString("macaroonip"),
|
||||
Jar: []*macaroonEntry{macEntry},
|
||||
}
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// loadProfileFile tries to load the file specified and JSON deserialize it into
|
||||
// the profile file struct.
|
||||
func loadProfileFile(file string) (*profileFile, error) {
|
||||
if !lnrpc.FileExists(file) {
|
||||
return nil, errNoProfileFile
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load profile file %s: %w",
|
||||
file, err)
|
||||
}
|
||||
f := &profileFile{}
|
||||
err = f.unmarshalJSON(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal profile file %s: "+
|
||||
"%v", file, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// saveProfileFile stores the given profile file struct in the specified file,
|
||||
// overwriting it if it already existed.
|
||||
func saveProfileFile(file string, f *profileFile) error {
|
||||
content, err := f.marshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal profile: %w", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(file, content, 0644)
|
||||
}
|
||||
|
||||
// profileFile is a struct that represents the whole content of a profile file.
|
||||
type profileFile struct {
|
||||
Default string `json:"default,omitempty"`
|
||||
Profiles []*profileEntry `json:"profiles"`
|
||||
}
|
||||
|
||||
// unmarshalJSON tries to parse the given JSON and unmarshal it into the
|
||||
// receiving instance.
|
||||
func (f *profileFile) unmarshalJSON(content []byte) error {
|
||||
return json.Unmarshal(content, f)
|
||||
}
|
||||
|
||||
// marshalJSON serializes the receiving instance to formatted/indented JSON.
|
||||
func (f *profileFile) marshalJSON() ([]byte, error) {
|
||||
b, err := json.Marshal(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error JSON marshalling profile: %w",
|
||||
err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
err = json.Indent(&out, b, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error indenting profile JSON: %w", err)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// routerCommands returns a list of routerrpc commands.
|
||||
func routerCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
queryMissionControlCommand,
|
||||
importMissionControlCommand,
|
||||
queryProbCommand,
|
||||
resetMissionControlCommand,
|
||||
buildRouteCommand,
|
||||
getCfgCommand,
|
||||
setCfgCommand,
|
||||
updateChanStatusCommand,
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
||||
// OutPoint displays an outpoint string in the form "<txid>:<output-index>".
|
||||
type OutPoint string
|
||||
|
||||
// NewOutPointFromProto formats the lnrpc.OutPoint into an OutPoint for display.
|
||||
func NewOutPointFromProto(op *lnrpc.OutPoint) OutPoint {
|
||||
var hash chainhash.Hash
|
||||
copy(hash[:], op.TxidBytes)
|
||||
return OutPoint(fmt.Sprintf("%v:%d", hash, op.OutputIndex))
|
||||
}
|
||||
|
||||
// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint
|
||||
// type.
|
||||
func NewProtoOutPoint(op string) (*lnrpc.OutPoint, error) {
|
||||
parts := strings.Split(op, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("outpoint should be of the form txid:index")
|
||||
}
|
||||
txid := parts[0]
|
||||
if hex.DecodedLen(len(txid)) != chainhash.HashSize {
|
||||
return nil, fmt.Errorf("invalid hex-encoded txid %v", txid)
|
||||
}
|
||||
outputIndex, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid output index: %w", err)
|
||||
}
|
||||
return &lnrpc.OutPoint{
|
||||
TxidStr: txid,
|
||||
OutputIndex: uint32(outputIndex),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Utxo displays information about an unspent output, including its address,
|
||||
// amount, pkscript, and confirmations.
|
||||
type Utxo struct {
|
||||
Type lnrpc.AddressType `json:"address_type"`
|
||||
Address string `json:"address"`
|
||||
AmountSat int64 `json:"amount_sat"`
|
||||
PkScript string `json:"pk_script"`
|
||||
OutPoint OutPoint `json:"outpoint"`
|
||||
Confirmations int64 `json:"confirmations"`
|
||||
}
|
||||
|
||||
// NewUtxoFromProto creates a display Utxo from the Utxo proto. This filters out
|
||||
// the raw txid bytes from the provided outpoint, which will otherwise be
|
||||
// printed in base64.
|
||||
func NewUtxoFromProto(utxo *lnrpc.Utxo) *Utxo {
|
||||
return &Utxo{
|
||||
Type: utxo.AddressType,
|
||||
Address: utxo.Address,
|
||||
AmountSat: utxo.AmountSat,
|
||||
PkScript: utxo.PkScript,
|
||||
OutPoint: NewOutPointFromProto(utxo.Outpoint),
|
||||
Confirmations: utxo.Confirmations,
|
||||
}
|
||||
}
|
||||
|
||||
// FailedUpdate displays information about a failed update, including its
|
||||
// address, reason and update error.
|
||||
type FailedUpdate struct {
|
||||
OutPoint OutPoint `json:"outpoint"`
|
||||
Reason string `json:"reason"`
|
||||
UpdateError string `json:"update_error"`
|
||||
}
|
||||
|
||||
// NewFailedUpdateFromProto creates a display from the FailedUpdate
|
||||
// proto. This filters out the raw txid bytes from the provided outpoint,
|
||||
// which will otherwise be printed in base64.
|
||||
func NewFailedUpdateFromProto(update *lnrpc.FailedUpdate) *FailedUpdate {
|
||||
return &FailedUpdate{
|
||||
OutPoint: NewOutPointFromProto(update.Outpoint),
|
||||
Reason: update.Reason.String(),
|
||||
UpdateError: update.UpdateError,
|
||||
}
|
||||
}
|
||||
|
||||
// UtxosToOutpoints converts a slice of UTXO strings into a slice of OutPoint
|
||||
// protobuf objects. It returns an error if no UTXOs are specified or if any
|
||||
// UTXO string cannot be parsed into an OutPoint.
|
||||
func UtxosToOutpoints(utxos []string) ([]*lnrpc.OutPoint, error) {
|
||||
var outpoints []*lnrpc.OutPoint
|
||||
if len(utxos) == 0 {
|
||||
return nil, fmt.Errorf("no utxos specified")
|
||||
}
|
||||
for _, utxo := range utxos {
|
||||
outpoint, err := NewProtoOutPoint(utxo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outpoints = append(outpoints, outpoint)
|
||||
}
|
||||
|
||||
return outpoints, nil
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +0,0 @@
|
||||
//go:build !walletrpc
|
||||
// +build !walletrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// walletCommands will return nil for non-walletrpc builds.
|
||||
func walletCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
|
||||
// PendingSweep is a CLI-friendly type of the walletrpc.PendingSweep proto. We
|
||||
// use this to show more useful string versions of byte slices and enums.
|
||||
type PendingSweep struct {
|
||||
OutPoint OutPoint `json:"outpoint"`
|
||||
WitnessType string `json:"witness_type"`
|
||||
AmountSat uint32 `json:"amount_sat"`
|
||||
SatPerVByte uint32 `json:"sat_per_vbyte"`
|
||||
BroadcastAttempts uint32 `json:"broadcast_attempts"`
|
||||
RequestedSatPerVByte uint32 `json:"requested_sat_per_vbyte"`
|
||||
Immediate bool `json:"immediate"`
|
||||
Budget uint64 `json:"budget"`
|
||||
DeadlineHeight uint32 `json:"deadline_height"`
|
||||
|
||||
NextBroadcastHeight uint32 `json:"next_broadcast_height"`
|
||||
RequestedConfTarget uint32 `json:"requested_conf_target"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
// NewPendingSweepFromProto converts the walletrpc.PendingSweep proto type into
|
||||
// its corresponding CLI-friendly type.
|
||||
func NewPendingSweepFromProto(pendingSweep *walletrpc.PendingSweep) *PendingSweep {
|
||||
return &PendingSweep{
|
||||
OutPoint: NewOutPointFromProto(pendingSweep.Outpoint),
|
||||
WitnessType: pendingSweep.WitnessType.String(),
|
||||
AmountSat: pendingSweep.AmountSat,
|
||||
SatPerVByte: uint32(pendingSweep.SatPerVbyte),
|
||||
BroadcastAttempts: pendingSweep.BroadcastAttempts,
|
||||
RequestedSatPerVByte: uint32(pendingSweep.RequestedSatPerVbyte),
|
||||
Immediate: pendingSweep.Immediate,
|
||||
Budget: pendingSweep.Budget,
|
||||
DeadlineHeight: pendingSweep.DeadlineHeight,
|
||||
|
||||
// Deprecated fields.
|
||||
NextBroadcastHeight: pendingSweep.NextBroadcastHeight,
|
||||
RequestedConfTarget: pendingSweep.RequestedConfTarget,
|
||||
Force: pendingSweep.Force,
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//go:build watchtowerrpc
|
||||
// +build watchtowerrpc
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func watchtowerCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "tower",
|
||||
Usage: "Interact with the watchtower.",
|
||||
Category: "Watchtower",
|
||||
Subcommands: []cli.Command{
|
||||
towerInfoCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getWatchtowerClient(ctx *cli.Context) (watchtowerrpc.WatchtowerClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
cleanup := func() {
|
||||
conn.Close()
|
||||
}
|
||||
return watchtowerrpc.NewWatchtowerClient(conn), cleanup
|
||||
}
|
||||
|
||||
var towerInfoCommand = cli.Command{
|
||||
Name: "info",
|
||||
Usage: "Returns basic information related to the active watchtower.",
|
||||
Action: actionDecorator(towerInfo),
|
||||
}
|
||||
|
||||
func towerInfo(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
if ctx.NArg() != 0 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "info")
|
||||
}
|
||||
|
||||
client, cleanup := getWatchtowerClient(ctx)
|
||||
defer cleanup()
|
||||
|
||||
req := &watchtowerrpc.GetInfoRequest{}
|
||||
resp, err := client.GetInfo(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build !watchtowerrpc
|
||||
// +build !watchtowerrpc
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
// watchtowerCommands will return nil for non-watchtowerrpc builds.
|
||||
func watchtowerCommands() []cli.Command {
|
||||
return nil
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc/wtclientrpc"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// wtclientCommands is a list of commands that can be used to interact with the
|
||||
// watchtower client.
|
||||
func wtclientCommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
Name: "wtclient",
|
||||
Usage: "Interact with the watchtower client.",
|
||||
Category: "Watchtower",
|
||||
Subcommands: []cli.Command{
|
||||
addTowerCommand,
|
||||
removeTowerCommand,
|
||||
deactivateTowerCommand,
|
||||
listTowersCommand,
|
||||
getTowerCommand,
|
||||
statsCommand,
|
||||
policyCommand,
|
||||
sessionCommands,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getWtclient initializes a connection to the watchtower client RPC in order to
|
||||
// interact with it.
|
||||
func getWtclient(ctx *cli.Context) (wtclientrpc.WatchtowerClientClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
return wtclientrpc.NewWatchtowerClientClient(conn), cleanUp
|
||||
}
|
||||
|
||||
var addTowerCommand = cli.Command{
|
||||
Name: "add",
|
||||
Usage: "Register a watchtower to use for future sessions/backups.",
|
||||
Description: "If the watchtower has already been registered, then " +
|
||||
"this command serves as a way of updating the watchtower " +
|
||||
"with new addresses it is reachable over.",
|
||||
ArgsUsage: "pubkey@address",
|
||||
Action: actionDecorator(addTower),
|
||||
}
|
||||
|
||||
func addTower(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "add")
|
||||
}
|
||||
|
||||
parts := strings.Split(ctx.Args().First(), "@")
|
||||
if len(parts) != 2 {
|
||||
return errors.New("expected tower of format pubkey@address")
|
||||
}
|
||||
pubKey, err := hex.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
address := parts[1]
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.AddTowerRequest{
|
||||
Pubkey: pubKey,
|
||||
Address: address,
|
||||
}
|
||||
resp, err := client.AddTower(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var deactivateTowerCommand = cli.Command{
|
||||
Name: "deactivate",
|
||||
Usage: "Deactivate a watchtower to temporarily prevent its use for " +
|
||||
"sessions/backups.",
|
||||
ArgsUsage: "pubkey",
|
||||
Action: actionDecorator(deactivateTower),
|
||||
}
|
||||
|
||||
func deactivateTower(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "deactivate")
|
||||
}
|
||||
|
||||
pubKey, err := hex.DecodeString(ctx.Args().First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.DeactivateTowerRequest{
|
||||
Pubkey: pubKey,
|
||||
}
|
||||
resp, err := client.DeactivateTower(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var removeTowerCommand = cli.Command{
|
||||
Name: "remove",
|
||||
Usage: "Remove a watchtower to prevent its use for future " +
|
||||
"sessions/backups.",
|
||||
Description: "An optional address can be provided to remove, " +
|
||||
"indicating that the watchtower is no longer reachable at " +
|
||||
"this address. If an address isn't provided, then the " +
|
||||
"watchtower will no longer be used for future sessions/backups.",
|
||||
ArgsUsage: "pubkey | pubkey@address",
|
||||
Action: actionDecorator(removeTower),
|
||||
}
|
||||
|
||||
func removeTower(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "remove")
|
||||
}
|
||||
|
||||
// The command can have only one argument, but it can be interpreted in
|
||||
// either of the following formats:
|
||||
//
|
||||
// pubkey or pubkey@address
|
||||
//
|
||||
// The hex-encoded public key of the watchtower is always required,
|
||||
// while the second is an optional address we'll remove from the
|
||||
// watchtower's database record.
|
||||
parts := strings.Split(ctx.Args().First(), "@")
|
||||
if len(parts) > 2 {
|
||||
return errors.New("expected tower of format pubkey@address")
|
||||
}
|
||||
pubKey, err := hex.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
var address string
|
||||
if len(parts) == 2 {
|
||||
address = parts[1]
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.RemoveTowerRequest{
|
||||
Pubkey: pubKey,
|
||||
Address: address,
|
||||
}
|
||||
resp, err := client.RemoveTower(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var listTowersCommand = cli.Command{
|
||||
Name: "towers",
|
||||
Usage: "Display information about all registered watchtowers.",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "include_sessions",
|
||||
Usage: "include sessions with the watchtower in the " +
|
||||
"response",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "exclude_exhausted_sessions",
|
||||
Usage: "Whether to exclude exhausted sessions in " +
|
||||
"the response info. This option is only " +
|
||||
"meaningful if include_sessions is true",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(listTowers),
|
||||
}
|
||||
|
||||
func listTowers(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() > 0 || ctx.NumFlags() > 2 {
|
||||
return cli.ShowCommandHelp(ctx, "towers")
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.ListTowersRequest{
|
||||
IncludeSessions: ctx.Bool("include_sessions"),
|
||||
ExcludeExhaustedSessions: ctx.Bool(
|
||||
"exclude_exhausted_sessions",
|
||||
),
|
||||
}
|
||||
resp, err := client.ListTowers(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var getTowerCommand = cli.Command{
|
||||
Name: "tower",
|
||||
Usage: "Display information about a specific registered watchtower.",
|
||||
ArgsUsage: "pubkey",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "include_sessions",
|
||||
Usage: "include sessions with the watchtower in the " +
|
||||
"response",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "exclude_exhausted_sessions",
|
||||
Usage: "Whether to exclude exhausted sessions in " +
|
||||
"the response info. This option is only " +
|
||||
"meaningful if include_sessions is true",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(getTower),
|
||||
}
|
||||
|
||||
func getTower(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() != 1 || ctx.NumFlags() > 2 {
|
||||
return cli.ShowCommandHelp(ctx, "tower")
|
||||
}
|
||||
|
||||
// The command only has one argument, which we expect to be the
|
||||
// hex-encoded public key of the watchtower we'll display information
|
||||
// about.
|
||||
pubKey, err := hex.DecodeString(ctx.Args().Get(0))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key: %w", err)
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.GetTowerInfoRequest{
|
||||
Pubkey: pubKey,
|
||||
IncludeSessions: ctx.Bool("include_sessions"),
|
||||
ExcludeExhaustedSessions: ctx.Bool(
|
||||
"exclude_exhausted_sessions",
|
||||
),
|
||||
}
|
||||
resp, err := client.GetTowerInfo(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var statsCommand = cli.Command{
|
||||
Name: "stats",
|
||||
Usage: "Display the session stats of the watchtower client.",
|
||||
Action: actionDecorator(stats),
|
||||
}
|
||||
|
||||
func stats(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() > 0 || ctx.NumFlags() > 0 {
|
||||
return cli.ShowCommandHelp(ctx, "stats")
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.StatsRequest{}
|
||||
resp, err := client.Stats(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var policyCommand = cli.Command{
|
||||
Name: "policy",
|
||||
Usage: "Display the active watchtower client policy configuration.",
|
||||
Action: actionDecorator(policy),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "legacy",
|
||||
Usage: "Retrieve the legacy tower client's current " +
|
||||
"policy. (default)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "anchor",
|
||||
Usage: "Retrieve the anchor tower client's current " +
|
||||
"policy.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "taproot",
|
||||
Usage: "Retrieve the taproot tower client's current " +
|
||||
"policy.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func policy(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() > 0 || ctx.NumFlags() > 1 {
|
||||
return cli.ShowCommandHelp(ctx, "policy")
|
||||
}
|
||||
|
||||
var policyType wtclientrpc.PolicyType
|
||||
switch {
|
||||
case ctx.Bool("anchor"):
|
||||
policyType = wtclientrpc.PolicyType_ANCHOR
|
||||
case ctx.Bool("legacy"):
|
||||
policyType = wtclientrpc.PolicyType_LEGACY
|
||||
case ctx.Bool("taproot"):
|
||||
policyType = wtclientrpc.PolicyType_TAPROOT
|
||||
|
||||
// For backwards compatibility with original rpc behavior.
|
||||
default:
|
||||
policyType = wtclientrpc.PolicyType_LEGACY
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
req := &wtclientrpc.PolicyRequest{
|
||||
PolicyType: policyType,
|
||||
}
|
||||
resp, err := client.Policy(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
var sessionCommands = cli.Command{
|
||||
Name: "session",
|
||||
Subcommands: []cli.Command{
|
||||
terminateSessionCommand,
|
||||
},
|
||||
}
|
||||
|
||||
var terminateSessionCommand = cli.Command{
|
||||
Name: "terminate",
|
||||
ArgsUsage: "id",
|
||||
Action: actionDecorator(terminateSession),
|
||||
}
|
||||
|
||||
func terminateSession(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
|
||||
// Display the command's help message if the number of arguments/flags
|
||||
// is not what we expect.
|
||||
if ctx.NArg() > 1 || ctx.NumFlags() != 0 {
|
||||
return cli.ShowCommandHelp(ctx, "terminate")
|
||||
}
|
||||
|
||||
client, cleanUp := getWtclient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
sessionID, err := hex.DecodeString(ctx.Args().First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid session ID: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.TerminateSession(
|
||||
ctxc, &wtclientrpc.TerminateSessionRequest{
|
||||
SessionId: sessionID,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user