b2c712fb67
In this commit, we add a new flag to the sendcoins command that allows callers to sweep all funds out of the daemon's wallet. This CANNOT be set at the same time that an amount is specified.
3444 lines
87 KiB
Go
3444 lines
87 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/golang/protobuf/jsonpb"
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/urfave/cli"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
"golang.org/x/net/context"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// TODO(roasbeef): cli logic for supporting both positional and unix style
|
|
// arguments.
|
|
|
|
// TODO(roasbeef): expose all fee conf targets
|
|
|
|
const defaultRecoveryWindow int32 = 250
|
|
|
|
func printJSON(resp interface{}) {
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
|
|
var out bytes.Buffer
|
|
json.Indent(&out, b, "", "\t")
|
|
out.WriteString("\n")
|
|
out.WriteTo(os.Stdout)
|
|
}
|
|
|
|
func printRespJSON(resp proto.Message) {
|
|
jsonMarshaler := &jsonpb.Marshaler{
|
|
EmitDefaults: true,
|
|
Indent: " ",
|
|
}
|
|
|
|
jsonStr, err := jsonMarshaler.MarshalToString(resp)
|
|
if err != nil {
|
|
fmt.Println("unable to decode response: ", err)
|
|
return
|
|
}
|
|
|
|
fmt.Println(jsonStr)
|
|
}
|
|
|
|
// actionDecorator is used to add additional information and error handling
|
|
// to command actions.
|
|
func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
|
|
return func(c *cli.Context) error {
|
|
if err := f(c); err != nil {
|
|
s, ok := status.FromError(err)
|
|
|
|
// If it's a command for the UnlockerService (like
|
|
// 'create' or 'unlock') but the wallet is already
|
|
// unlocked, then these methods aren't recognized any
|
|
// more because this service is shut down after
|
|
// successful unlock. That's why the code
|
|
// 'Unimplemented' means something different for these
|
|
// two commands.
|
|
if s.Code() == codes.Unimplemented &&
|
|
(c.Command.Name == "create" ||
|
|
c.Command.Name == "unlock") {
|
|
return fmt.Errorf("Wallet is already unlocked")
|
|
}
|
|
|
|
// lnd might be active, but not possible to contact
|
|
// using RPC if the wallet is encrypted. If we get
|
|
// error code Unimplemented, it means that lnd is
|
|
// running, but the RPC server is not active yet (only
|
|
// WalletUnlocker server active) and most likely this
|
|
// is because of an encrypted wallet.
|
|
if ok && s.Code() == codes.Unimplemented {
|
|
return fmt.Errorf("Wallet is encrypted. " +
|
|
"Please unlock using 'lncli unlock', " +
|
|
"or set password using 'lncli create'" +
|
|
" if this is the first time starting " +
|
|
"lnd.")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var newAddressCommand = cli.Command{
|
|
Name: "newaddress",
|
|
Category: "Wallet",
|
|
Usage: "Generates a new address.",
|
|
ArgsUsage: "address-type",
|
|
Description: `
|
|
Generate a wallet new address. Address-types has to be one of:
|
|
- p2wkh: Pay to witness key hash
|
|
- np2wkh: Pay to nested witness key hash`,
|
|
Action: actionDecorator(newAddress),
|
|
}
|
|
|
|
func newAddress(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
stringAddrType := ctx.Args().First()
|
|
|
|
// Map the string encoded address type, to the concrete typed address
|
|
// type enum. An unrecognized address type will result in an error.
|
|
var addrType lnrpc.AddressType
|
|
switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
|
|
case "p2wkh":
|
|
addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
|
|
case "np2wkh":
|
|
addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
|
|
default:
|
|
return fmt.Errorf("invalid address type %v, support address type "+
|
|
"are: p2wkh and np2wkh", stringAddrType)
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
addr, err := client.NewAddress(ctxb, &lnrpc.NewAddressRequest{
|
|
Type: addrType,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(addr)
|
|
return nil
|
|
}
|
|
|
|
var sendCoinsCommand = cli.Command{
|
|
Name: "sendcoins",
|
|
Category: "On-chain",
|
|
Usage: "Send bitcoin on-chain to an address.",
|
|
ArgsUsage: "addr amt",
|
|
Description: `
|
|
Send amt coins in satoshis to the BASE58 encoded bitcoin address addr.
|
|
|
|
Fees used when sending the transaction can be specified via the --conf_target, or
|
|
--sat_per_byte optional flags.
|
|
|
|
Positional arguments and flags can be used interchangeably but not at the same time!
|
|
`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "addr",
|
|
Usage: "the BASE58 encoded bitcoin address to send coins to on-chain",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "sweepall",
|
|
Usage: "if set, then the amount field will be ignored, " +
|
|
"and all the wallet will attempt to sweep all " +
|
|
"outputs within the wallet to the target " +
|
|
"address",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "amt",
|
|
Usage: "the number of bitcoin denominated in satoshis to send",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "conf_target",
|
|
Usage: "(optional) the number of blocks that the " +
|
|
"transaction *should* confirm in, will be " +
|
|
"used for fee estimation",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "sat_per_byte",
|
|
Usage: "(optional) a manual fee expressed in " +
|
|
"sat/byte that should be used when crafting " +
|
|
"the transaction",
|
|
},
|
|
},
|
|
Action: actionDecorator(sendCoins),
|
|
}
|
|
|
|
func sendCoins(ctx *cli.Context) error {
|
|
var (
|
|
addr string
|
|
amt int64
|
|
err error
|
|
)
|
|
args := ctx.Args()
|
|
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "sendcoins")
|
|
return nil
|
|
}
|
|
|
|
if ctx.IsSet("conf_target") && ctx.IsSet("sat_per_byte") {
|
|
return fmt.Errorf("either conf_target or sat_per_byte should be " +
|
|
"set, but not both")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("addr"):
|
|
addr = ctx.String("addr")
|
|
case args.Present():
|
|
addr = args.First()
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("Address argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("amt"):
|
|
amt = ctx.Int64("amt")
|
|
case args.Present():
|
|
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
|
case !ctx.Bool("sweepall"):
|
|
return fmt.Errorf("Amount argument missing")
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode amount: %v", err)
|
|
}
|
|
|
|
if amt != 0 && ctx.Bool("sweepall") {
|
|
return fmt.Errorf("amount cannot be set if attempting to " +
|
|
"sweep all coins out of the wallet")
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.SendCoinsRequest{
|
|
Addr: addr,
|
|
Amount: amt,
|
|
TargetConf: int32(ctx.Int64("conf_target")),
|
|
SatPerByte: ctx.Int64("sat_per_byte"),
|
|
SendAll: ctx.Bool("sweepall"),
|
|
}
|
|
txid, err := client.SendCoins(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(txid)
|
|
return nil
|
|
}
|
|
|
|
var listUnspentCommand = cli.Command{
|
|
Name: "listunspent",
|
|
Category: "On-chain",
|
|
Usage: "List utxos available for spending.",
|
|
ArgsUsage: "min-confs max-confs",
|
|
Description: `
|
|
For each spendable utxo currently in the wallet, with at least min_confs
|
|
confirmations, and at most max_confs confirmations, lists the txid, index,
|
|
amount, address, address type, scriptPubkey and number of confirmations.
|
|
Use --min_confs=0 to include unconfirmed coins. To list all coins
|
|
with at least min_confs confirmations, omit the second argument or flag
|
|
'--max_confs'.
|
|
`,
|
|
Flags: []cli.Flag{
|
|
cli.Int64Flag{
|
|
Name: "min_confs",
|
|
Usage: "the minimum number of confirmations for a utxo",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "max_confs",
|
|
Usage: "the maximum number of confirmations for a utxo",
|
|
},
|
|
},
|
|
Action: actionDecorator(listUnspent),
|
|
}
|
|
|
|
func listUnspent(ctx *cli.Context) error {
|
|
var (
|
|
minConfirms int64
|
|
maxConfirms int64
|
|
err error
|
|
)
|
|
args := ctx.Args()
|
|
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "listunspent")
|
|
return nil
|
|
}
|
|
|
|
if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
|
|
return fmt.Errorf("max_confs cannot be set without " +
|
|
"min_confs being set")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("min_confs"):
|
|
minConfirms = ctx.Int64("min_confs")
|
|
case args.Present():
|
|
minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
cli.ShowCommandHelp(ctx, "listunspent")
|
|
return nil
|
|
}
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("minimum confirmations argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("max_confs"):
|
|
maxConfirms = ctx.Int64("max_confs")
|
|
case args.Present():
|
|
maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
cli.ShowCommandHelp(ctx, "listunspent")
|
|
return nil
|
|
}
|
|
args = args.Tail()
|
|
default:
|
|
// No maxconfs was specified; we use max as flag;
|
|
// the default for ctx.Int64 (0) is *not* appropriate here.
|
|
maxConfirms = math.MaxInt32
|
|
}
|
|
|
|
if minConfirms < 0 || maxConfirms < minConfirms {
|
|
return fmt.Errorf("maximum confirmations must be greater or " +
|
|
"equal to minimum confirmations")
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ListUnspentRequest{
|
|
MinConfs: int32(minConfirms),
|
|
MaxConfs: int32(maxConfirms),
|
|
}
|
|
jsonResponse, err := client.ListUnspent(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printRespJSON(jsonResponse)
|
|
return nil
|
|
}
|
|
|
|
var sendManyCommand = cli.Command{
|
|
Name: "sendmany",
|
|
Category: "On-chain",
|
|
Usage: "Send bitcoin on-chain to multiple addresses.",
|
|
ArgsUsage: "send-json-string [--conf_target=N] [--sat_per_byte=P]",
|
|
Description: `
|
|
Create and broadcast a transaction paying the specified amount(s) to the passed address(es).
|
|
|
|
The send-json-string' param decodes addresses and the amount to send
|
|
respectively in the following format:
|
|
|
|
'{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'
|
|
`,
|
|
Flags: []cli.Flag{
|
|
cli.Int64Flag{
|
|
Name: "conf_target",
|
|
Usage: "(optional) the number of blocks that the transaction *should* " +
|
|
"confirm in, will be used for fee estimation",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "sat_per_byte",
|
|
Usage: "(optional) a manual fee expressed in sat/byte that should be " +
|
|
"used when crafting the transaction",
|
|
},
|
|
},
|
|
Action: actionDecorator(sendMany),
|
|
}
|
|
|
|
func sendMany(ctx *cli.Context) error {
|
|
var amountToAddr map[string]int64
|
|
|
|
jsonMap := ctx.Args().First()
|
|
if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx.IsSet("conf_target") && ctx.IsSet("sat_per_byte") {
|
|
return fmt.Errorf("either conf_target or sat_per_byte should be " +
|
|
"set, but not both")
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
txid, err := client.SendMany(ctxb, &lnrpc.SendManyRequest{
|
|
AddrToAmount: amountToAddr,
|
|
TargetConf: int32(ctx.Int64("conf_target")),
|
|
SatPerByte: ctx.Int64("sat_per_byte"),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(txid)
|
|
return nil
|
|
}
|
|
|
|
var connectCommand = cli.Command{
|
|
Name: "connect",
|
|
Category: "Peers",
|
|
Usage: "Connect to a remote lnd peer.",
|
|
ArgsUsage: "<pubkey>@host",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "perm",
|
|
Usage: "If set, the daemon will attempt to persistently " +
|
|
"connect to the target peer.\n" +
|
|
" If not, the call will be synchronous.",
|
|
},
|
|
},
|
|
Action: actionDecorator(connectPeer),
|
|
}
|
|
|
|
func connectPeer(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
targetAddress := ctx.Args().First()
|
|
splitAddr := strings.Split(targetAddress, "@")
|
|
if len(splitAddr) != 2 {
|
|
return fmt.Errorf("target address expected in format: " +
|
|
"pubkey@host:port")
|
|
}
|
|
|
|
addr := &lnrpc.LightningAddress{
|
|
Pubkey: splitAddr[0],
|
|
Host: splitAddr[1],
|
|
}
|
|
req := &lnrpc.ConnectPeerRequest{
|
|
Addr: addr,
|
|
Perm: ctx.Bool("perm"),
|
|
}
|
|
|
|
lnid, err := client.ConnectPeer(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(lnid)
|
|
return nil
|
|
}
|
|
|
|
var disconnectCommand = cli.Command{
|
|
Name: "disconnect",
|
|
Category: "Peers",
|
|
Usage: "Disconnect a remote lnd peer identified by public key.",
|
|
ArgsUsage: "<pubkey>",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "node_key",
|
|
Usage: "The hex-encoded compressed public key of the peer " +
|
|
"to disconnect from",
|
|
},
|
|
},
|
|
Action: actionDecorator(disconnectPeer),
|
|
}
|
|
|
|
func disconnectPeer(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var pubKey string
|
|
switch {
|
|
case ctx.IsSet("node_key"):
|
|
pubKey = ctx.String("node_key")
|
|
case ctx.Args().Present():
|
|
pubKey = ctx.Args().First()
|
|
default:
|
|
return fmt.Errorf("must specify target public key")
|
|
}
|
|
|
|
req := &lnrpc.DisconnectPeerRequest{
|
|
PubKey: pubKey,
|
|
}
|
|
|
|
lnid, err := client.DisconnectPeer(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(lnid)
|
|
return nil
|
|
}
|
|
|
|
// TODO(roasbeef): change default number of confirmations
|
|
var openChannelCommand = cli.Command{
|
|
Name: "openchannel",
|
|
Category: "Channels",
|
|
Usage: "Open a channel to a node or an existing peer.",
|
|
Description: `
|
|
Attempt to open a new channel to an existing peer with the key node-key
|
|
optionally blocking until the channel is 'open'.
|
|
|
|
One can also connect to a node before opening a new channel to it by
|
|
setting its host:port via the --connect argument. For this to work,
|
|
the node_key must be provided, rather than the peer_id. This is optional.
|
|
|
|
The channel will be initialized with local-amt satoshis local and push-amt
|
|
satoshis for the remote node. Note that specifying push-amt means you give that
|
|
amount to the remote node as part of the channel opening. Once the channel is open,
|
|
a channelPoint (txid:vout) of the funding output is returned.
|
|
|
|
One can manually set the fee to be used for the funding transaction via either
|
|
the --conf_target or --sat_per_byte arguments. This is optional.`,
|
|
ArgsUsage: "node-key local-amt push-amt",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "node_key",
|
|
Usage: "the identity public key of the target node/peer " +
|
|
"serialized in compressed format",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "connect",
|
|
Usage: "(optional) the host:port of the target node",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "local_amt",
|
|
Usage: "the number of satoshis the wallet should commit to the channel",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "push_amt",
|
|
Usage: "the number of satoshis to give the remote side " +
|
|
"as part of the initial commitment state, " +
|
|
"this is equivalent to first opening a " +
|
|
"channel and sending the remote party funds, " +
|
|
"but done all in one step",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "block",
|
|
Usage: "block and wait until the channel is fully open",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "conf_target",
|
|
Usage: "(optional) the number of blocks that the " +
|
|
"transaction *should* confirm in, will be " +
|
|
"used for fee estimation",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "sat_per_byte",
|
|
Usage: "(optional) a manual fee expressed in " +
|
|
"sat/byte that should be used when crafting " +
|
|
"the transaction",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "private",
|
|
Usage: "make the channel private, such that it won't " +
|
|
"be announced to the greater network, and " +
|
|
"nodes other than the two channel endpoints " +
|
|
"must be explicitly told about it to be able " +
|
|
"to route through it",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "min_htlc_msat",
|
|
Usage: "(optional) the minimum value we will require " +
|
|
"for incoming HTLCs on the channel",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "remote_csv_delay",
|
|
Usage: "(optional) the number of blocks we will require " +
|
|
"our channel counterparty to wait before accessing " +
|
|
"its funds in case of unilateral close. If this is " +
|
|
"not set, we will scale the value according to the " +
|
|
"channel size",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "min_confs",
|
|
Usage: "(optional) the minimum number of confirmations " +
|
|
"each one of your outputs used for the funding " +
|
|
"transaction must satisfy",
|
|
Value: 1,
|
|
},
|
|
},
|
|
Action: actionDecorator(openChannel),
|
|
}
|
|
|
|
func openChannel(ctx *cli.Context) error {
|
|
// TODO(roasbeef): add deadline to context
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
args := ctx.Args()
|
|
var err error
|
|
|
|
// Show command help if no arguments provided
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "openchannel")
|
|
return nil
|
|
}
|
|
|
|
req := &lnrpc.OpenChannelRequest{
|
|
TargetConf: int32(ctx.Int64("conf_target")),
|
|
SatPerByte: ctx.Int64("sat_per_byte"),
|
|
MinHtlcMsat: ctx.Int64("min_htlc_msat"),
|
|
RemoteCsvDelay: uint32(ctx.Uint64("remote_csv_delay")),
|
|
MinConfs: int32(ctx.Uint64("min_confs")),
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("node_key"):
|
|
nodePubHex, err := hex.DecodeString(ctx.String("node_key"))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode node public key: %v", err)
|
|
}
|
|
req.NodePubkey = nodePubHex
|
|
|
|
case args.Present():
|
|
nodePubHex, err := hex.DecodeString(args.First())
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode node public key: %v", err)
|
|
}
|
|
args = args.Tail()
|
|
req.NodePubkey = nodePubHex
|
|
default:
|
|
return fmt.Errorf("node id argument missing")
|
|
}
|
|
|
|
// As soon as we can confirm that the node's node_key was set, rather
|
|
// than the peer_id, we can check if the host:port was also set to
|
|
// connect to it before opening the channel.
|
|
if req.NodePubkey != nil && ctx.IsSet("connect") {
|
|
addr := &lnrpc.LightningAddress{
|
|
Pubkey: hex.EncodeToString(req.NodePubkey),
|
|
Host: ctx.String("connect"),
|
|
}
|
|
|
|
req := &lnrpc.ConnectPeerRequest{
|
|
Addr: addr,
|
|
Perm: false,
|
|
}
|
|
|
|
// Check if connecting to the node was successful.
|
|
// We discard the peer id returned as it is not needed.
|
|
_, err := client.ConnectPeer(ctxb, req)
|
|
if err != nil &&
|
|
!strings.Contains(err.Error(), "already connected") {
|
|
return err
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("local_amt"):
|
|
req.LocalFundingAmount = int64(ctx.Int("local_amt"))
|
|
case args.Present():
|
|
req.LocalFundingAmount, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode local amt: %v", err)
|
|
}
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("local amt argument missing")
|
|
}
|
|
|
|
if ctx.IsSet("push_amt") {
|
|
req.PushSat = int64(ctx.Int("push_amt"))
|
|
} else if args.Present() {
|
|
req.PushSat, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode push amt: %v", err)
|
|
}
|
|
}
|
|
|
|
req.Private = ctx.Bool("private")
|
|
|
|
stream, err := client.OpenChannel(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
resp, err := stream.Recv()
|
|
if err == io.EOF {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch update := resp.Update.(type) {
|
|
case *lnrpc.OpenStatusUpdate_ChanPending:
|
|
txid, err := chainhash.NewHash(update.ChanPending.Txid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printJSON(struct {
|
|
FundingTxid string `json:"funding_txid"`
|
|
}{
|
|
FundingTxid: txid.String(),
|
|
},
|
|
)
|
|
|
|
if !ctx.Bool("block") {
|
|
return nil
|
|
}
|
|
|
|
case *lnrpc.OpenStatusUpdate_ChanOpen:
|
|
channelPoint := update.ChanOpen.ChannelPoint
|
|
|
|
// A channel point's funding txid can be get/set as a
|
|
// byte slice or a string. In the case it is a string,
|
|
// decode it.
|
|
var txidHash []byte
|
|
switch channelPoint.GetFundingTxid().(type) {
|
|
case *lnrpc.ChannelPoint_FundingTxidBytes:
|
|
txidHash = channelPoint.GetFundingTxidBytes()
|
|
case *lnrpc.ChannelPoint_FundingTxidStr:
|
|
s := channelPoint.GetFundingTxidStr()
|
|
h, err := chainhash.NewHashFromStr(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txidHash = h[:]
|
|
}
|
|
|
|
txid, err := chainhash.NewHash(txidHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
index := channelPoint.OutputIndex
|
|
printJSON(struct {
|
|
ChannelPoint string `json:"channel_point"`
|
|
}{
|
|
ChannelPoint: fmt.Sprintf("%v:%v", txid, index),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(roasbeef): also allow short relative channel ID.
|
|
|
|
var closeChannelCommand = cli.Command{
|
|
Name: "closechannel",
|
|
Category: "Channels",
|
|
Usage: "Close an existing channel.",
|
|
Description: `
|
|
Close an existing channel. The channel can be closed either cooperatively,
|
|
or unilaterally (--force).
|
|
|
|
A unilateral channel closure means that the latest commitment
|
|
transaction will be broadcast to the network. As a result, any settled
|
|
funds will be time locked for a few blocks before they can be spent.
|
|
|
|
In the case of a cooperative closure, One can manually set the fee to
|
|
be used for the closing transaction via either the --conf_target or
|
|
--sat_per_byte arguments. This will be the starting value used during
|
|
fee negotiation. This is optional.
|
|
|
|
To view which funding_txids/output_indexes can be used for a channel close,
|
|
see the channel_point values within the listchannels command output.
|
|
The format for a channel_point is 'funding_txid:output_index'.`,
|
|
ArgsUsage: "funding_txid [output_index [time_limit]]",
|
|
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: "time_limit",
|
|
Usage: "a relative deadline afterwhich the attempt should be " +
|
|
"abandoned",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "after the time limit has passed, attempt an " +
|
|
"uncooperative closure",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "block",
|
|
Usage: "block until the channel is closed",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "conf_target",
|
|
Usage: "(optional) the number of blocks that the " +
|
|
"transaction *should* confirm in, will be " +
|
|
"used for fee estimation",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "sat_per_byte",
|
|
Usage: "(optional) a manual fee expressed in " +
|
|
"sat/byte that should be used when crafting " +
|
|
"the transaction",
|
|
},
|
|
},
|
|
Action: actionDecorator(closeChannel),
|
|
}
|
|
|
|
func closeChannel(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
// Show command help if no arguments and flags were provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "closechannel")
|
|
return nil
|
|
}
|
|
|
|
channelPoint, err := parseChannelPoint(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(roasbeef): implement time deadline within server
|
|
req := &lnrpc.CloseChannelRequest{
|
|
ChannelPoint: channelPoint,
|
|
Force: ctx.Bool("force"),
|
|
TargetConf: int32(ctx.Int64("conf_target")),
|
|
SatPerByte: ctx.Int64("sat_per_byte"),
|
|
}
|
|
|
|
// After parsing the request, we'll spin up a goroutine that will
|
|
// retrieve the closing transaction ID when attempting to close the
|
|
// channel. We do this to because `executeChannelClose` can block, so we
|
|
// would like to present the closing transaction ID to the user as soon
|
|
// as it is broadcasted.
|
|
var wg sync.WaitGroup
|
|
txidChan := make(chan string, 1)
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
printJSON(struct {
|
|
ClosingTxid string `json:"closing_txid"`
|
|
}{
|
|
ClosingTxid: <-txidChan,
|
|
})
|
|
}()
|
|
|
|
err = executeChannelClose(client, req, txidChan, ctx.Bool("block"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// In the case that the user did not provide the `block` flag, then we
|
|
// need to wait for the goroutine to be done to prevent it from being
|
|
// destroyed when exiting before printing the closing transaction ID.
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// executeChannelClose attempts to close the channel from a request. The closing
|
|
// transaction ID is sent through `txidChan` as soon as it is broadcasted to the
|
|
// network. The block boolean is used to determine if we should block until the
|
|
// closing transaction receives all of its required confirmations.
|
|
func executeChannelClose(client lnrpc.LightningClient, req *lnrpc.CloseChannelRequest,
|
|
txidChan chan<- string, block bool) error {
|
|
|
|
stream, err := client.CloseChannel(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
resp, err := stream.Recv()
|
|
if err == io.EOF {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch update := resp.Update.(type) {
|
|
case *lnrpc.CloseStatusUpdate_ClosePending:
|
|
closingHash := update.ClosePending.Txid
|
|
txid, err := chainhash.NewHash(closingHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
txidChan <- txid.String()
|
|
|
|
if !block {
|
|
return nil
|
|
}
|
|
case *lnrpc.CloseStatusUpdate_ChanClose:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
var closeAllChannelsCommand = cli.Command{
|
|
Name: "closeallchannels",
|
|
Category: "Channels",
|
|
Usage: "Close all existing channels.",
|
|
Description: `
|
|
Close all existing channels.
|
|
|
|
Channels will be closed either cooperatively or unilaterally, depending
|
|
on whether the channel is active or not. If the channel is inactive, any
|
|
settled funds within it will be time locked for a few blocks before they
|
|
can be spent.
|
|
|
|
One can request to close inactive channels only by using the
|
|
--inactive_only flag.
|
|
|
|
By default, one is prompted for confirmation every time an inactive
|
|
channel is requested to be closed. To avoid this, one can set the
|
|
--force flag, which will only prompt for confirmation once for all
|
|
inactive channels and proceed to close them.`,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "inactive_only",
|
|
Usage: "close inactive channels only",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "ask for confirmation once before attempting " +
|
|
"to close existing channels",
|
|
},
|
|
},
|
|
Action: actionDecorator(closeAllChannels),
|
|
}
|
|
|
|
func closeAllChannels(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
listReq := &lnrpc.ListChannelsRequest{}
|
|
openChannels, err := client.ListChannels(context.Background(), listReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to fetch open channels: %v", err)
|
|
}
|
|
|
|
if len(openChannels.Channels) == 0 {
|
|
return errors.New("no open channels to close")
|
|
}
|
|
|
|
var channelsToClose []*lnrpc.Channel
|
|
|
|
switch {
|
|
case ctx.Bool("force") && ctx.Bool("inactive_only"):
|
|
msg := "Unilaterally close all inactive channels? The funds " +
|
|
"within these channels will be locked for some blocks " +
|
|
"(CSV delay) before they can be spent. (yes/no): "
|
|
|
|
confirmed := promptForConfirmation(msg)
|
|
|
|
// We can safely exit if the user did not confirm.
|
|
if !confirmed {
|
|
return nil
|
|
}
|
|
|
|
// Go through the list of open channels and only add inactive
|
|
// channels to the closing list.
|
|
for _, channel := range openChannels.Channels {
|
|
if !channel.GetActive() {
|
|
channelsToClose = append(
|
|
channelsToClose, channel,
|
|
)
|
|
}
|
|
}
|
|
case ctx.Bool("force"):
|
|
msg := "Close all active and inactive channels? Inactive " +
|
|
"channels will be closed unilaterally, so funds " +
|
|
"within them will be locked for a few blocks (CSV " +
|
|
"delay) before they can be spent. (yes/no): "
|
|
|
|
confirmed := promptForConfirmation(msg)
|
|
|
|
// We can safely exit if the user did not confirm.
|
|
if !confirmed {
|
|
return nil
|
|
}
|
|
|
|
channelsToClose = openChannels.Channels
|
|
default:
|
|
// Go through the list of open channels and determine which
|
|
// should be added to the closing list.
|
|
for _, channel := range openChannels.Channels {
|
|
// If the channel is inactive, we'll attempt to
|
|
// unilaterally close the channel, so we should prompt
|
|
// the user for confirmation beforehand.
|
|
if !channel.GetActive() {
|
|
msg := fmt.Sprintf("Unilaterally close channel "+
|
|
"with node %s and channel point %s? "+
|
|
"The closing transaction will need %d "+
|
|
"confirmations before the funds can be "+
|
|
"spent. (yes/no): ", channel.RemotePubkey,
|
|
channel.ChannelPoint, channel.CsvDelay)
|
|
|
|
confirmed := promptForConfirmation(msg)
|
|
|
|
if confirmed {
|
|
channelsToClose = append(
|
|
channelsToClose, channel,
|
|
)
|
|
}
|
|
} else if !ctx.Bool("inactive_only") {
|
|
// Otherwise, we'll only add active channels if
|
|
// we were not requested to close inactive
|
|
// channels only.
|
|
channelsToClose = append(
|
|
channelsToClose, channel,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// result defines the result of closing a channel. The closing
|
|
// transaction ID is populated if a channel is successfully closed.
|
|
// Otherwise, the error that prevented closing the channel is populated.
|
|
type result struct {
|
|
RemotePubKey string `json:"remote_pub_key"`
|
|
ChannelPoint string `json:"channel_point"`
|
|
ClosingTxid string `json:"closing_txid"`
|
|
FailErr string `json:"error"`
|
|
}
|
|
|
|
// Launch each channel closure in a goroutine in order to execute them
|
|
// in parallel. Once they're all executed, we will print the results as
|
|
// they come.
|
|
resultChan := make(chan result, len(channelsToClose))
|
|
for _, channel := range channelsToClose {
|
|
go func(channel *lnrpc.Channel) {
|
|
res := result{}
|
|
res.RemotePubKey = channel.RemotePubkey
|
|
res.ChannelPoint = channel.ChannelPoint
|
|
defer func() {
|
|
resultChan <- res
|
|
}()
|
|
|
|
// Parse the channel point in order to create the close
|
|
// channel request.
|
|
s := strings.Split(res.ChannelPoint, ":")
|
|
if len(s) != 2 {
|
|
res.FailErr = "expected channel point with " +
|
|
"format txid:index"
|
|
return
|
|
}
|
|
index, err := strconv.ParseUint(s[1], 10, 32)
|
|
if err != nil {
|
|
res.FailErr = fmt.Sprintf("unable to parse "+
|
|
"channel point output index: %v", err)
|
|
return
|
|
}
|
|
|
|
req := &lnrpc.CloseChannelRequest{
|
|
ChannelPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: s[0],
|
|
},
|
|
OutputIndex: uint32(index),
|
|
},
|
|
Force: !channel.GetActive(),
|
|
}
|
|
|
|
txidChan := make(chan string, 1)
|
|
err = executeChannelClose(client, req, txidChan, false)
|
|
if err != nil {
|
|
res.FailErr = fmt.Sprintf("unable to close "+
|
|
"channel: %v", err)
|
|
return
|
|
}
|
|
|
|
res.ClosingTxid = <-txidChan
|
|
}(channel)
|
|
}
|
|
|
|
for range channelsToClose {
|
|
res := <-resultChan
|
|
printJSON(res)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// promptForConfirmation continuously prompts the user for the message until
|
|
// receiving a response of "yes" or "no" and returns their answer as a bool.
|
|
func promptForConfirmation(msg string) bool {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
for {
|
|
fmt.Print(msg)
|
|
|
|
answer, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
answer = strings.ToLower(strings.TrimSpace(answer))
|
|
|
|
switch {
|
|
case answer == "yes":
|
|
return true
|
|
case answer == "no":
|
|
return false
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
var abandonChannelCommand = cli.Command{
|
|
Name: "abandonchannel",
|
|
Category: "Channels",
|
|
Usage: "Abandons an existing channel.",
|
|
Description: `
|
|
Removes all channel state from the database except for a close
|
|
summary. This method can be used to get rid of permanently unusable
|
|
channels due to bugs fixed in newer versions of lnd.
|
|
|
|
Only available when lnd is built in debug mode.
|
|
|
|
To view which funding_txids/output_indexes can be used for this command,
|
|
see the channel_point values within the listchannels command output.
|
|
The format for a channel_point is 'funding_txid:output_index'.`,
|
|
ArgsUsage: "funding_txid [output_index]",
|
|
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",
|
|
},
|
|
},
|
|
Action: actionDecorator(abandonChannel),
|
|
}
|
|
|
|
func abandonChannel(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
// Show command help if no arguments and flags were provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "abandonchannel")
|
|
return nil
|
|
}
|
|
|
|
channelPoint, err := parseChannelPoint(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := &lnrpc.AbandonChannelRequest{
|
|
ChannelPoint: channelPoint,
|
|
}
|
|
|
|
resp, err := client.AbandonChannel(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
// parseChannelPoint parses a funding txid and output index from the command
|
|
// line. Both named options as well as unnamed parameters are supported.
|
|
func parseChannelPoint(ctx *cli.Context) (*lnrpc.ChannelPoint, error) {
|
|
channelPoint := &lnrpc.ChannelPoint{}
|
|
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("funding_txid"):
|
|
channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: ctx.String("funding_txid"),
|
|
}
|
|
case args.Present():
|
|
channelPoint.FundingTxid = &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: args.First(),
|
|
}
|
|
args = args.Tail()
|
|
default:
|
|
return nil, fmt.Errorf("funding txid argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("output_index"):
|
|
channelPoint.OutputIndex = uint32(ctx.Int("output_index"))
|
|
case args.Present():
|
|
index, err := strconv.ParseUint(args.First(), 10, 32)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode output index: %v", err)
|
|
}
|
|
channelPoint.OutputIndex = uint32(index)
|
|
default:
|
|
channelPoint.OutputIndex = 0
|
|
}
|
|
|
|
return channelPoint, nil
|
|
}
|
|
|
|
var listPeersCommand = cli.Command{
|
|
Name: "listpeers",
|
|
Category: "Peers",
|
|
Usage: "List all active, currently connected peers.",
|
|
Action: actionDecorator(listPeers),
|
|
}
|
|
|
|
func listPeers(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ListPeersRequest{}
|
|
resp, err := client.ListPeers(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
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.
|
|
`,
|
|
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 {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
defer cleanUp()
|
|
|
|
// First, we'll prompt the user for their passphrase twice to ensure
|
|
// both attempts match up properly.
|
|
fmt.Printf("Input wallet password: ")
|
|
pw1, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Printf("Confirm wallet password: ")
|
|
pw2, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
// If the passwords don't match, then we'll return an error.
|
|
if !bytes.Equal(pw1, pw2) {
|
|
return fmt.Errorf("passwords don't match")
|
|
}
|
|
|
|
// Next, we'll see if the user has 24-word mnemonic they want to use to
|
|
// derive a seed within the wallet.
|
|
var (
|
|
hasMnemonic bool
|
|
)
|
|
|
|
mnemonicCheck:
|
|
for {
|
|
fmt.Println()
|
|
fmt.Printf("Do you have an existing cipher seed " +
|
|
"mnemonic you want to use? (Enter y/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 "n":
|
|
hasMnemonic = false
|
|
break mnemonicCheck
|
|
}
|
|
}
|
|
|
|
// If the user *does* have an existing seed they want to use, then
|
|
// we'll read that in directly from the terminal.
|
|
var (
|
|
cipherSeedMnemonic []string
|
|
aezeedPass []byte
|
|
recoveryWindow int32
|
|
)
|
|
if 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.
|
|
fmt.Printf("Input your cipher seed passphrase (press enter if " +
|
|
"your seed doesn't have a passphrase): ")
|
|
passphrase, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
aezeedPass = []byte(passphrase)
|
|
|
|
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 err
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
answer = strings.TrimSpace(answer)
|
|
|
|
if len(answer) == 0 {
|
|
recoveryWindow = defaultRecoveryWindow
|
|
break
|
|
}
|
|
|
|
lookAhead, err := strconv.Atoi(answer)
|
|
if err != nil {
|
|
fmt.Println("Unable to parse recovery "+
|
|
"window: %v", err)
|
|
continue
|
|
}
|
|
|
|
recoveryWindow = int32(lookAhead)
|
|
break
|
|
}
|
|
} else {
|
|
// 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.")
|
|
fmt.Printf("Input your passphrase if you wish to encrypt it " +
|
|
"(or press enter to proceed without a cipher seed " +
|
|
"passphrase): ")
|
|
aezeedPass1, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
if len(aezeedPass1) != 0 {
|
|
fmt.Printf("Confirm cipher seed passphrase: ")
|
|
aezeedPass2, err := terminal.ReadPassword(
|
|
int(syscall.Stdin),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
// If the passwords don't match, then we'll return an
|
|
// error.
|
|
if !bytes.Equal(aezeedPass1, aezeedPass2) {
|
|
return fmt.Errorf("cipher seed pass phrases " +
|
|
"don't match")
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("Generating fresh cipher seed...")
|
|
fmt.Println()
|
|
|
|
genSeedReq := &lnrpc.GenSeedRequest{
|
|
AezeedPassphrase: aezeedPass1,
|
|
}
|
|
seedResp, err := client.GenSeed(ctxb, genSeedReq)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to generate seed: %v", err)
|
|
}
|
|
|
|
cipherSeedMnemonic = seedResp.CipherSeedMnemonic
|
|
aezeedPass = aezeedPass1
|
|
}
|
|
|
|
// Before we initialize the wallet, we'll display the cipher seed to
|
|
// the user so they can write it down.
|
|
mnemonicWords := cipherSeedMnemonic
|
|
|
|
fmt.Println("!!!YOU MUST WRITE DOWN THIS SEED TO BE ABLE TO " +
|
|
"RESTORE THE WALLET!!!\n")
|
|
|
|
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!!!")
|
|
|
|
// 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: pw1,
|
|
CipherSeedMnemonic: cipherSeedMnemonic,
|
|
AezeedPassphrase: aezeedPass,
|
|
RecoveryWindow: recoveryWindow,
|
|
}
|
|
if _, err := client.InitWallet(ctxb, req); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("\nlnd successfully initialized!")
|
|
|
|
return nil
|
|
}
|
|
|
|
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.
|
|
`,
|
|
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.",
|
|
},
|
|
},
|
|
Action: actionDecorator(unlock),
|
|
}
|
|
|
|
func unlock(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
defer cleanUp()
|
|
|
|
fmt.Printf("Input wallet password: ")
|
|
pw, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
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,
|
|
}
|
|
_, err = client.UnlockWallet(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println("\nlnd successfully unlocked!")
|
|
|
|
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.
|
|
`,
|
|
Action: actionDecorator(changePassword),
|
|
}
|
|
|
|
func changePassword(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getWalletUnlockerClient(ctx)
|
|
defer cleanUp()
|
|
|
|
fmt.Printf("Input current wallet password: ")
|
|
currentPw, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Printf("Input new wallet password: ")
|
|
newPw, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
fmt.Printf("Confirm new wallet password: ")
|
|
confirmPw, err := terminal.ReadPassword(int(syscall.Stdin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
|
|
if !bytes.Equal(newPw, confirmPw) {
|
|
return fmt.Errorf("passwords don't match")
|
|
}
|
|
|
|
req := &lnrpc.ChangePasswordRequest{
|
|
CurrentPassword: currentPw,
|
|
NewPassword: newPw,
|
|
}
|
|
|
|
_, err = client.ChangePassword(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var walletBalanceCommand = cli.Command{
|
|
Name: "walletbalance",
|
|
Category: "Wallet",
|
|
Usage: "Compute and display the wallet's current balance.",
|
|
Action: actionDecorator(walletBalance),
|
|
}
|
|
|
|
func walletBalance(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.WalletBalanceRequest{}
|
|
resp, err := client.WalletBalance(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var channelBalanceCommand = cli.Command{
|
|
Name: "channelbalance",
|
|
Category: "Channels",
|
|
Usage: "Returns the sum of the total available channel balance across " +
|
|
"all open channels.",
|
|
Action: actionDecorator(channelBalance),
|
|
}
|
|
|
|
func channelBalance(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ChannelBalanceRequest{}
|
|
resp, err := client.ChannelBalance(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var getInfoCommand = cli.Command{
|
|
Name: "getinfo",
|
|
Usage: "Returns basic information related to the active daemon.",
|
|
Action: actionDecorator(getInfo),
|
|
}
|
|
|
|
type chain struct {
|
|
Chain string `json:"chain"`
|
|
Network string `json:"network"`
|
|
}
|
|
|
|
func getInfo(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.GetInfoRequest{}
|
|
resp, err := client.GetInfo(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
chains := make([]chain, len(resp.Chains))
|
|
for i, c := range resp.Chains {
|
|
chains[i] = chain{
|
|
Chain: c.Chain,
|
|
Network: c.Network,
|
|
}
|
|
}
|
|
|
|
// We print a struct that mimics the proto definition of GetInfoResponse
|
|
// but has a better ordering for the same list of fields.
|
|
printJSON(struct {
|
|
Version string `json:"version"`
|
|
IdentityPubkey string `json:"identity_pubkey"`
|
|
Alias string `json:"alias"`
|
|
NumPendingChannels uint32 `json:"num_pending_channels"`
|
|
NumActiveChannels uint32 `json:"num_active_channels"`
|
|
NumInactiveChannels uint32 `json:"num_inactive_channels"`
|
|
NumPeers uint32 `json:"num_peers"`
|
|
BlockHeight uint32 `json:"block_height"`
|
|
BlockHash string `json:"block_hash"`
|
|
BestHeaderTimestamp int64 `json:"best_header_timestamp"`
|
|
SyncedToChain bool `json:"synced_to_chain"`
|
|
Testnet bool `json:"testnet"`
|
|
Chains []chain `json:"chains"`
|
|
Uris []string `json:"uris"`
|
|
}{
|
|
Version: resp.Version,
|
|
IdentityPubkey: resp.IdentityPubkey,
|
|
Alias: resp.Alias,
|
|
NumPendingChannels: resp.NumPendingChannels,
|
|
NumActiveChannels: resp.NumActiveChannels,
|
|
NumInactiveChannels: resp.NumInactiveChannels,
|
|
NumPeers: resp.NumPeers,
|
|
BlockHeight: resp.BlockHeight,
|
|
BlockHash: resp.BlockHash,
|
|
BestHeaderTimestamp: resp.BestHeaderTimestamp,
|
|
SyncedToChain: resp.SyncedToChain,
|
|
Testnet: resp.Testnet,
|
|
Chains: chains,
|
|
Uris: resp.Uris,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
var pendingChannelsCommand = cli.Command{
|
|
Name: "pendingchannels",
|
|
Category: "Channels",
|
|
Usage: "Display information pertaining to pending channels.",
|
|
Action: actionDecorator(pendingChannels),
|
|
}
|
|
|
|
func pendingChannels(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.PendingChannelsRequest{}
|
|
resp, err := client.PendingChannels(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var listChannelsCommand = cli.Command{
|
|
Name: "listchannels",
|
|
Category: "Channels",
|
|
Usage: "List all open channels.",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "active_only",
|
|
Usage: "only list channels which are currently active",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "inactive_only",
|
|
Usage: "only list channels which are currently inactive",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "public_only",
|
|
Usage: "only list channels which are currently public",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "private_only",
|
|
Usage: "only list channels which are currently private",
|
|
},
|
|
},
|
|
Action: actionDecorator(listChannels),
|
|
}
|
|
|
|
func listChannels(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ListChannelsRequest{
|
|
ActiveOnly: ctx.Bool("active_only"),
|
|
InactiveOnly: ctx.Bool("inactive_only"),
|
|
PublicOnly: ctx.Bool("public_only"),
|
|
PrivateOnly: ctx.Bool("private_only"),
|
|
}
|
|
|
|
resp, err := client.ListChannels(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(roasbeef): defer close the client for the all
|
|
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var closedChannelsCommand = cli.Command{
|
|
Name: "closedchannels",
|
|
Category: "Channels",
|
|
Usage: "List all closed channels.",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "cooperative",
|
|
Usage: "list channels that were closed cooperatively",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "local_force",
|
|
Usage: "list channels that were force-closed " +
|
|
"by the local node",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "remote_force",
|
|
Usage: "list channels that were force-closed " +
|
|
"by the remote node",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "breach",
|
|
Usage: "list channels for which the remote node " +
|
|
"attempted to broadcast a prior " +
|
|
"revoked channel state",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "funding_canceled",
|
|
Usage: "list channels that were never fully opened",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "abandoned",
|
|
Usage: "list channels that were abandoned by " +
|
|
"the local node",
|
|
},
|
|
},
|
|
Action: actionDecorator(closedChannels),
|
|
}
|
|
|
|
func closedChannels(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ClosedChannelsRequest{
|
|
Cooperative: ctx.Bool("cooperative"),
|
|
LocalForce: ctx.Bool("local_force"),
|
|
RemoteForce: ctx.Bool("remote_force"),
|
|
Breach: ctx.Bool("breach"),
|
|
FundingCanceled: ctx.Bool("funding_cancelled"),
|
|
Abandoned: ctx.Bool("abandoned"),
|
|
}
|
|
|
|
resp, err := client.ClosedChannels(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var sendPaymentCommand = cli.Command{
|
|
Name: "sendpayment",
|
|
Category: "Payments",
|
|
Usage: "Send a payment over lightning.",
|
|
Description: `
|
|
Send a payment over Lightning. One can either specify the full
|
|
parameters of the payment, or just use a payment request which encodes
|
|
all the payment details.
|
|
|
|
If payment isn't manually specified, then only a payment request needs
|
|
to be passed using the --pay_req argument.
|
|
|
|
If the payment *is* manually specified, then all four alternative
|
|
arguments need to be specified in order to complete the payment:
|
|
* --dest=N
|
|
* --amt=A
|
|
* --final_cltv_delta=T
|
|
* --payment_hash=H
|
|
|
|
The --debug_send flag is provided for usage *purely* in test
|
|
environments. If specified, then the payment hash isn't required, as
|
|
it'll use the hash of all zeroes. This mode allows one to quickly test
|
|
payment connectivity without having to create an invoice at the
|
|
destination.
|
|
`,
|
|
ArgsUsage: "dest amt payment_hash final_cltv_delta | --pay_req=[payment request]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "dest, d",
|
|
Usage: "the compressed identity pubkey of the " +
|
|
"payment recipient",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "amt, a",
|
|
Usage: "number of satoshis to send",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit",
|
|
Usage: "maximum fee allowed in satoshis when sending" +
|
|
"the payment",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit_percent",
|
|
Usage: "percentage of the payment's amount used as the" +
|
|
"maximum fee allowed when sending the payment",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "payment_hash, r",
|
|
Usage: "the hash to use within the payment's HTLC",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "debug_send",
|
|
Usage: "use the debug rHash when sending the HTLC",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "pay_req",
|
|
Usage: "a zpay32 encoded payment request to fulfill",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "final_cltv_delta",
|
|
Usage: "the number of blocks the last hop has to reveal the preimage",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force, f",
|
|
Usage: "will skip payment request confirmation",
|
|
},
|
|
},
|
|
Action: sendPayment,
|
|
}
|
|
|
|
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
|
// flags passed.
|
|
func retrieveFeeLimit(ctx *cli.Context) (*lnrpc.FeeLimit, error) {
|
|
switch {
|
|
case ctx.IsSet("fee_limit") && ctx.IsSet("fee_limit_percent"):
|
|
return nil, fmt.Errorf("either fee_limit or fee_limit_percent " +
|
|
"can be set, but not both")
|
|
case ctx.IsSet("fee_limit"):
|
|
return &lnrpc.FeeLimit{
|
|
Limit: &lnrpc.FeeLimit_Fixed{
|
|
Fixed: ctx.Int64("fee_limit"),
|
|
},
|
|
}, nil
|
|
case ctx.IsSet("fee_limit_percent"):
|
|
return &lnrpc.FeeLimit{
|
|
Limit: &lnrpc.FeeLimit_Percent{
|
|
Percent: ctx.Int64("fee_limit_percent"),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Since the fee limit flags aren't required, we don't return an error
|
|
// if they're not set.
|
|
return nil, nil
|
|
}
|
|
|
|
func confirmPayReq(ctx *cli.Context, client lnrpc.LightningClient, payReq string) error {
|
|
ctxb := context.Background()
|
|
|
|
req := &lnrpc.PayReqString{PayReq: payReq}
|
|
resp, err := client.DecodePayReq(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the amount was not included in the invoice, then we let
|
|
// the payee specify the amount of satoshis they wish to send.
|
|
amt := resp.GetNumSatoshis()
|
|
if amt == 0 {
|
|
amt = ctx.Int64("amt")
|
|
if amt == 0 {
|
|
return fmt.Errorf("amount must be specified when " +
|
|
"paying a zero amount invoice")
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Description: %v\n", resp.GetDescription())
|
|
fmt.Printf("Amount (in satoshis): %v\n", amt)
|
|
fmt.Printf("Destination: %v\n", resp.GetDestination())
|
|
|
|
confirm := promptForConfirmation("Confirm payment (yes/no): ")
|
|
if !confirm {
|
|
return fmt.Errorf("payment not confirmed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func sendPayment(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
// Show command help if no arguments provided
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "sendpayment")
|
|
return nil
|
|
}
|
|
|
|
// First, we'll retrieve the fee limit value passed since it can apply
|
|
// to both ways of sending payments (with the payment request or
|
|
// providing the details manually).
|
|
feeLimit, err := retrieveFeeLimit(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If a payment request was provided, we can exit early since all of the
|
|
// details of the payment are encoded within the request.
|
|
if ctx.IsSet("pay_req") {
|
|
if !ctx.Bool("force") {
|
|
err = confirmPayReq(ctx, client, ctx.String("pay_req"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
req := &lnrpc.SendRequest{
|
|
PaymentRequest: ctx.String("pay_req"),
|
|
Amt: ctx.Int64("amt"),
|
|
FeeLimit: feeLimit,
|
|
}
|
|
|
|
return sendPaymentRequest(client, req)
|
|
}
|
|
|
|
var (
|
|
destNode []byte
|
|
amount int64
|
|
)
|
|
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("dest"):
|
|
destNode, err = hex.DecodeString(ctx.String("dest"))
|
|
case args.Present():
|
|
destNode, err = hex.DecodeString(args.First())
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("destination txid argument missing")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(destNode) != 33 {
|
|
return fmt.Errorf("dest node pubkey must be exactly 33 bytes, is "+
|
|
"instead: %v", len(destNode))
|
|
}
|
|
|
|
if ctx.IsSet("amt") {
|
|
amount = ctx.Int64("amt")
|
|
} else if args.Present() {
|
|
amount, err = strconv.ParseInt(args.First(), 10, 64)
|
|
args = args.Tail()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode payment amount: %v", err)
|
|
}
|
|
}
|
|
|
|
req := &lnrpc.SendRequest{
|
|
Dest: destNode,
|
|
Amt: amount,
|
|
FeeLimit: feeLimit,
|
|
}
|
|
|
|
if ctx.Bool("debug_send") && (ctx.IsSet("payment_hash") || args.Present()) {
|
|
return fmt.Errorf("do not provide a payment hash with debug send")
|
|
} else if !ctx.Bool("debug_send") {
|
|
var rHash []byte
|
|
|
|
switch {
|
|
case ctx.IsSet("payment_hash"):
|
|
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
|
case args.Present():
|
|
rHash, err = hex.DecodeString(args.First())
|
|
default:
|
|
return fmt.Errorf("payment hash argument missing")
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(rHash) != 32 {
|
|
return fmt.Errorf("payment hash must be exactly 32 "+
|
|
"bytes, is instead %v", len(rHash))
|
|
}
|
|
req.PaymentHash = rHash
|
|
|
|
switch {
|
|
case ctx.IsSet("final_cltv_delta"):
|
|
req.FinalCltvDelta = int32(ctx.Int64("final_cltv_delta"))
|
|
case args.Present():
|
|
delta, err := strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.FinalCltvDelta = int32(delta)
|
|
}
|
|
}
|
|
|
|
return sendPaymentRequest(client, req)
|
|
}
|
|
|
|
func sendPaymentRequest(client lnrpc.LightningClient, req *lnrpc.SendRequest) error {
|
|
paymentStream, err := client.SendPayment(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := paymentStream.Send(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := paymentStream.Recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
paymentStream.CloseSend()
|
|
|
|
printJSON(struct {
|
|
E string `json:"payment_error"`
|
|
P string `json:"payment_preimage"`
|
|
R *lnrpc.Route `json:"payment_route"`
|
|
}{
|
|
E: resp.PaymentError,
|
|
P: hex.EncodeToString(resp.PaymentPreimage),
|
|
R: resp.PaymentRoute,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var payInvoiceCommand = cli.Command{
|
|
Name: "payinvoice",
|
|
Category: "Payments",
|
|
Usage: "Pay an invoice over lightning.",
|
|
ArgsUsage: "pay_req",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "pay_req",
|
|
Usage: "a zpay32 encoded payment request to fulfill",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "amt",
|
|
Usage: "(optional) number of satoshis to fulfill the " +
|
|
"invoice",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit",
|
|
Usage: "maximum fee allowed in satoshis when sending " +
|
|
"the payment",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit_percent",
|
|
Usage: "percentage of the payment's amount used as the" +
|
|
"maximum fee allowed when sending the payment",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force, f",
|
|
Usage: "will skip payment request confirmation",
|
|
},
|
|
},
|
|
Action: actionDecorator(payInvoice),
|
|
}
|
|
|
|
func payInvoice(ctx *cli.Context) error {
|
|
args := ctx.Args()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var payReq string
|
|
switch {
|
|
case ctx.IsSet("pay_req"):
|
|
payReq = ctx.String("pay_req")
|
|
case args.Present():
|
|
payReq = args.First()
|
|
default:
|
|
return fmt.Errorf("pay_req argument missing")
|
|
}
|
|
|
|
feeLimit, err := retrieveFeeLimit(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ctx.Bool("force") {
|
|
err = confirmPayReq(ctx, client, payReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
req := &lnrpc.SendRequest{
|
|
PaymentRequest: payReq,
|
|
Amt: ctx.Int64("amt"),
|
|
FeeLimit: feeLimit,
|
|
}
|
|
return sendPaymentRequest(client, req)
|
|
}
|
|
|
|
var sendToRouteCommand = cli.Command{
|
|
Name: "sendtoroute",
|
|
Usage: "send a payment over a predefined route",
|
|
Description: `
|
|
Send a payment over Lightning using a specific route. One must specify
|
|
a list of routes to attempt and the payment hash. This command can even
|
|
be chained with the response to queryroutes. This command can be used
|
|
to implement channel rebalancing by crafting a self-route, or even
|
|
atomic swaps using a self-route that crosses multiple chains.
|
|
|
|
There are three ways to specify routes:
|
|
* using the --routes parameter to manually specify a JSON encoded
|
|
set of routes in the format of the return value of queryroutes:
|
|
(lncli sendtoroute --payment_hash=<pay_hash> --routes=<route>)
|
|
|
|
* passing the routes as a positional argument:
|
|
(lncli sendtoroute --payment_hash=pay_hash <route>)
|
|
|
|
* or reading in the routes from stdin, which can allow chaining the
|
|
response from queryroutes, or even read in a file with a set of
|
|
pre-computed routes:
|
|
(lncli queryroutes --args.. | lncli sendtoroute --payment_hash= -
|
|
|
|
notice the '-' at the end, which signals that lncli should read
|
|
the route in from stdin
|
|
`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "payment_hash, pay_hash",
|
|
Usage: "the hash to use within the payment's HTLC",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "routes, r",
|
|
Usage: "a json array string in the format of the response " +
|
|
"of queryroutes that denotes which routes to use",
|
|
},
|
|
},
|
|
Action: sendToRoute,
|
|
}
|
|
|
|
func sendToRoute(ctx *cli.Context) error {
|
|
// Show command help if no arguments provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
cli.ShowCommandHelp(ctx, "sendtoroute")
|
|
return nil
|
|
}
|
|
|
|
args := ctx.Args()
|
|
|
|
var (
|
|
rHash []byte
|
|
err error
|
|
)
|
|
switch {
|
|
case ctx.IsSet("payment_hash"):
|
|
rHash, err = hex.DecodeString(ctx.String("payment_hash"))
|
|
case args.Present():
|
|
rHash, err = hex.DecodeString(args.First())
|
|
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("payment hash argument missing")
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rHash) != 32 {
|
|
return fmt.Errorf("payment hash must be exactly 32 "+
|
|
"bytes, is instead %d", len(rHash))
|
|
}
|
|
|
|
var jsonRoutes string
|
|
switch {
|
|
// The user is specifying the routes explicitly via the key word
|
|
// argument.
|
|
case ctx.IsSet("routes"):
|
|
jsonRoutes = ctx.String("routes")
|
|
|
|
// The user is specifying the routes as a positional argument.
|
|
case args.Present() && args.First() != "-":
|
|
jsonRoutes = args.First()
|
|
|
|
// The user is signalling that we should read stdin in order to parse
|
|
// the set of target routes.
|
|
case args.Present() && args.First() == "-":
|
|
b, err := ioutil.ReadAll(os.Stdin)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(b) == 0 {
|
|
return fmt.Errorf("queryroutes output is empty")
|
|
}
|
|
|
|
jsonRoutes = string(b)
|
|
}
|
|
|
|
routes := &lnrpc.QueryRoutesResponse{}
|
|
err = jsonpb.UnmarshalString(jsonRoutes, routes)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to unmarshal json string "+
|
|
"from incoming array of routes: %v", err)
|
|
}
|
|
|
|
req := &lnrpc.SendToRouteRequest{
|
|
PaymentHash: rHash,
|
|
Routes: routes.Routes,
|
|
}
|
|
|
|
return sendToRouteRequest(ctx, req)
|
|
}
|
|
|
|
func sendToRouteRequest(ctx *cli.Context, req *lnrpc.SendToRouteRequest) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
paymentStream, err := client.SendToRoute(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := paymentStream.Send(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := paymentStream.Recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printJSON(struct {
|
|
E string `json:"payment_error"`
|
|
P string `json:"payment_preimage"`
|
|
R *lnrpc.Route `json:"payment_route"`
|
|
}{
|
|
E: resp.PaymentError,
|
|
P: hex.EncodeToString(resp.PaymentPreimage),
|
|
R: resp.PaymentRoute,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var addInvoiceCommand = cli.Command{
|
|
Name: "addinvoice",
|
|
Category: "Payments",
|
|
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 payee
|
|
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: "receipt",
|
|
Usage: "an optional cryptographic receipt of payment",
|
|
},
|
|
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.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 3600 seconds (1 hour) " +
|
|
"is implied.",
|
|
},
|
|
cli.BoolTFlag{
|
|
Name: "private",
|
|
Usage: "encode routing hints in the invoice with " +
|
|
"private channels in order to assist the " +
|
|
"payer in reaching you",
|
|
},
|
|
},
|
|
Action: actionDecorator(addInvoice),
|
|
}
|
|
|
|
func addInvoice(ctx *cli.Context) error {
|
|
var (
|
|
preimage []byte
|
|
descHash []byte
|
|
receipt []byte
|
|
amt int64
|
|
err error
|
|
)
|
|
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("amt"):
|
|
amt = ctx.Int64("amt")
|
|
case args.Present():
|
|
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
|
args = args.Tail()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode amt argument: %v", 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: %v", err)
|
|
}
|
|
|
|
descHash, err = hex.DecodeString(ctx.String("description_hash"))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse description_hash: %v", err)
|
|
}
|
|
|
|
receipt, err = hex.DecodeString(ctx.String("receipt"))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse receipt: %v", err)
|
|
}
|
|
|
|
invoice := &lnrpc.Invoice{
|
|
Memo: ctx.String("memo"),
|
|
Receipt: receipt,
|
|
RPreimage: preimage,
|
|
Value: amt,
|
|
DescriptionHash: descHash,
|
|
FallbackAddr: ctx.String("fallback_addr"),
|
|
Expiry: ctx.Int64("expiry"),
|
|
Private: ctx.Bool("private"),
|
|
}
|
|
|
|
resp, err := client.AddInvoice(context.Background(), invoice)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printJSON(struct {
|
|
RHash string `json:"r_hash"`
|
|
PayReq string `json:"pay_req"`
|
|
AddIndex uint64 `json:"add_index"`
|
|
}{
|
|
RHash: hex.EncodeToString(resp.RHash),
|
|
PayReq: resp.PaymentRequest,
|
|
AddIndex: resp.AddIndex,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var lookupInvoiceCommand = cli.Command{
|
|
Name: "lookupinvoice",
|
|
Category: "Payments",
|
|
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 {
|
|
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: %v", err)
|
|
}
|
|
|
|
req := &lnrpc.PaymentHash{
|
|
RHash: rHash,
|
|
}
|
|
|
|
invoice, err := client.LookupInvoice(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(invoice)
|
|
|
|
return nil
|
|
}
|
|
|
|
var listInvoicesCommand = cli.Command{
|
|
Name: "listinvoices",
|
|
Category: "Payments",
|
|
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. The reversed flag is set by default in order to
|
|
paginate backwards. If you wish to paginate forwards, you must
|
|
explicitly set the flag to false. 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.BoolTFlag{
|
|
Name: "reversed",
|
|
Usage: "if set, the invoices returned precede the " +
|
|
"given index_offset, allowing backwards " +
|
|
"pagination",
|
|
},
|
|
},
|
|
Action: actionDecorator(listInvoices),
|
|
}
|
|
|
|
func listInvoices(ctx *cli.Context) error {
|
|
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("reversed"),
|
|
}
|
|
|
|
invoices, err := client.ListInvoices(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(invoices)
|
|
|
|
return nil
|
|
}
|
|
|
|
var describeGraphCommand = cli.Command{
|
|
Name: "describegraph",
|
|
Category: "Peers",
|
|
Description: "Prints a human readable version of the known channel " +
|
|
"graph from the PoV of the node",
|
|
Usage: "Describe the network graph.",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "include_unannounced",
|
|
Usage: "If set, unannounced channels will be included in the " +
|
|
"graph. Unannounced channels are both private channels, and " +
|
|
"public channels that are not yet announced to the network.",
|
|
},
|
|
},
|
|
Action: actionDecorator(describeGraph),
|
|
}
|
|
|
|
func describeGraph(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ChannelGraphRequest{
|
|
IncludeUnannounced: ctx.Bool("include_unannounced"),
|
|
}
|
|
|
|
graph, err := client.DescribeGraph(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(graph)
|
|
return nil
|
|
}
|
|
|
|
// normalizeFunc is a factory function which returns a function that normalizes
|
|
// the capacity of edges within the graph. The value of the returned
|
|
// function can be used to either plot the capacities, or to use a weight in a
|
|
// rendering of the graph.
|
|
func normalizeFunc(edges []*lnrpc.ChannelEdge, scaleFactor float64) func(int64) float64 {
|
|
var (
|
|
min float64 = math.MaxInt64
|
|
max float64
|
|
)
|
|
|
|
for _, edge := range edges {
|
|
// In order to obtain saner values, we reduce the capacity of a
|
|
// channel to its base 2 logarithm.
|
|
z := math.Log2(float64(edge.Capacity))
|
|
|
|
if z < min {
|
|
min = z
|
|
}
|
|
if z > max {
|
|
max = z
|
|
}
|
|
}
|
|
|
|
return func(x int64) float64 {
|
|
y := math.Log2(float64(x))
|
|
|
|
// TODO(roasbeef): results in min being zero
|
|
return (y - min) / (max - min) * scaleFactor
|
|
}
|
|
}
|
|
|
|
var listPaymentsCommand = cli.Command{
|
|
Name: "listpayments",
|
|
Category: "Payments",
|
|
Usage: "List all outgoing payments.",
|
|
Action: actionDecorator(listPayments),
|
|
}
|
|
|
|
func listPayments(ctx *cli.Context) error {
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.ListPaymentsRequest{}
|
|
|
|
payments, err := client.ListPayments(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(payments)
|
|
return nil
|
|
}
|
|
|
|
var getChanInfoCommand = cli.Command{
|
|
Name: "getchaninfo",
|
|
Category: "Channels",
|
|
Usage: "Get the state of a channel.",
|
|
Description: "Prints out the latest authenticated state for a " +
|
|
"particular channel",
|
|
ArgsUsage: "chan_id",
|
|
Flags: []cli.Flag{
|
|
cli.Int64Flag{
|
|
Name: "chan_id",
|
|
Usage: "the 8-byte compact channel ID to query for",
|
|
},
|
|
},
|
|
Action: actionDecorator(getChanInfo),
|
|
}
|
|
|
|
func getChanInfo(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var (
|
|
chanID int64
|
|
err error
|
|
)
|
|
|
|
switch {
|
|
case ctx.IsSet("chan_id"):
|
|
chanID = ctx.Int64("chan_id")
|
|
case ctx.Args().Present():
|
|
chanID, err = strconv.ParseInt(ctx.Args().First(), 10, 64)
|
|
default:
|
|
return fmt.Errorf("chan_id argument missing")
|
|
}
|
|
|
|
req := &lnrpc.ChanInfoRequest{
|
|
ChanId: uint64(chanID),
|
|
}
|
|
|
|
chanInfo, err := client.GetChanInfo(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(chanInfo)
|
|
return nil
|
|
}
|
|
|
|
var getNodeInfoCommand = cli.Command{
|
|
Name: "getnodeinfo",
|
|
Category: "Peers",
|
|
Usage: "Get information on a specific node.",
|
|
Description: "Prints out the latest authenticated node state for an " +
|
|
"advertised node",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "pub_key",
|
|
Usage: "the 33-byte hex-encoded compressed public of the target " +
|
|
"node",
|
|
},
|
|
},
|
|
Action: actionDecorator(getNodeInfo),
|
|
}
|
|
|
|
func getNodeInfo(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
args := ctx.Args()
|
|
|
|
var pubKey string
|
|
switch {
|
|
case ctx.IsSet("pub_key"):
|
|
pubKey = ctx.String("pub_key")
|
|
case args.Present():
|
|
pubKey = args.First()
|
|
default:
|
|
return fmt.Errorf("pub_key argument missing")
|
|
}
|
|
|
|
req := &lnrpc.NodeInfoRequest{
|
|
PubKey: pubKey,
|
|
}
|
|
|
|
nodeInfo, err := client.GetNodeInfo(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(nodeInfo)
|
|
return nil
|
|
}
|
|
|
|
var queryRoutesCommand = cli.Command{
|
|
Name: "queryroutes",
|
|
Category: "Payments",
|
|
Usage: "Query a route to a destination.",
|
|
Description: "Queries the channel router for a potential path to the destination that has sufficient flow for the amount including fees",
|
|
ArgsUsage: "dest amt",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "dest",
|
|
Usage: "the 33-byte hex-encoded public key for the payment " +
|
|
"destination",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "amt",
|
|
Usage: "the amount to send expressed in satoshis",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit",
|
|
Usage: "maximum fee allowed in satoshis when sending " +
|
|
"the payment",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "fee_limit_percent",
|
|
Usage: "percentage of the payment's amount used as the " +
|
|
"maximum fee allowed when sending the payment",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "num_max_routes",
|
|
Usage: "the max number of routes to be returned",
|
|
Value: 10,
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "final_cltv_delta",
|
|
Usage: "(optional) number of blocks the last hop has to reveal " +
|
|
"the preimage",
|
|
},
|
|
},
|
|
Action: actionDecorator(queryRoutes),
|
|
}
|
|
|
|
func queryRoutes(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var (
|
|
dest string
|
|
amt int64
|
|
err error
|
|
)
|
|
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("dest"):
|
|
dest = ctx.String("dest")
|
|
case args.Present():
|
|
dest = args.First()
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("dest argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("amt"):
|
|
amt = ctx.Int64("amt")
|
|
case args.Present():
|
|
amt, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode amt argument: %v", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("amt argument missing")
|
|
}
|
|
|
|
feeLimit, err := retrieveFeeLimit(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := &lnrpc.QueryRoutesRequest{
|
|
PubKey: dest,
|
|
Amt: amt,
|
|
FeeLimit: feeLimit,
|
|
NumRoutes: int32(ctx.Int("num_max_routes")),
|
|
FinalCltvDelta: int32(ctx.Int("final_cltv_delta")),
|
|
}
|
|
|
|
route, err := client.QueryRoutes(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(route)
|
|
return nil
|
|
}
|
|
|
|
var getNetworkInfoCommand = cli.Command{
|
|
Name: "getnetworkinfo",
|
|
Category: "Channels",
|
|
Usage: "Get statistical information about the current " +
|
|
"state of the network.",
|
|
Description: "Returns a set of statistics pertaining to the known " +
|
|
"channel graph",
|
|
Action: actionDecorator(getNetworkInfo),
|
|
}
|
|
|
|
func getNetworkInfo(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.NetworkInfoRequest{}
|
|
|
|
netInfo, err := client.GetNetworkInfo(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(netInfo)
|
|
return nil
|
|
}
|
|
|
|
var debugLevelCommand = cli.Command{
|
|
Name: "debuglevel",
|
|
Usage: "Set the debug level.",
|
|
Description: `Logging level for all subsystems {trace, debug, info, warn, error, critical, off}
|
|
You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems
|
|
|
|
Use show to list available subsystems`,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "show",
|
|
Usage: "if true, then the list of available sub-systems will be printed out",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "level",
|
|
Usage: "the level specification to target either a coarse logging level, or granular set of specific sub-systems with logging levels for each",
|
|
},
|
|
},
|
|
Action: actionDecorator(debugLevel),
|
|
}
|
|
|
|
func debugLevel(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
req := &lnrpc.DebugLevelRequest{
|
|
Show: ctx.Bool("show"),
|
|
LevelSpec: ctx.String("level"),
|
|
}
|
|
|
|
resp, err := client.DebugLevel(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var decodePayReqCommand = cli.Command{
|
|
Name: "decodepayreq",
|
|
Category: "Payments",
|
|
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 {
|
|
ctxb := context.Background()
|
|
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(ctxb, &lnrpc.PayReqString{
|
|
PayReq: payreq,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var listChainTxnsCommand = cli.Command{
|
|
Name: "listchaintxns",
|
|
Category: "On-chain",
|
|
Usage: "List transactions from the wallet.",
|
|
Description: "List all transactions an address of the wallet was involved in.",
|
|
Action: actionDecorator(listChainTxns),
|
|
}
|
|
|
|
func listChainTxns(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
resp, err := client.GetTransactions(ctxb, &lnrpc.GetTransactionsRequest{})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var stopCommand = cli.Command{
|
|
Name: "stop",
|
|
Usage: "Stop and shutdown the daemon.",
|
|
Description: `
|
|
Gracefully stop all daemon subsystems before stopping the daemon itself.
|
|
This is equivalent to stopping it using CTRL-C.`,
|
|
Action: actionDecorator(stopDaemon),
|
|
}
|
|
|
|
func stopDaemon(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
_, err := client.StopDaemon(ctxb, &lnrpc.StopRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var signMessageCommand = cli.Command{
|
|
Name: "signmessage",
|
|
Category: "Wallet",
|
|
Usage: "Sign a message with the node's private key.",
|
|
ArgsUsage: "msg",
|
|
Description: `
|
|
Sign msg with the resident node's private key.
|
|
Returns the signature as a zbase32 string.
|
|
|
|
Positional arguments and flags can be used interchangeably but not at the same time!`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "msg",
|
|
Usage: "the message to sign",
|
|
},
|
|
},
|
|
Action: actionDecorator(signMessage),
|
|
}
|
|
|
|
func signMessage(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var msg []byte
|
|
|
|
switch {
|
|
case ctx.IsSet("msg"):
|
|
msg = []byte(ctx.String("msg"))
|
|
case ctx.Args().Present():
|
|
msg = []byte(ctx.Args().First())
|
|
default:
|
|
return fmt.Errorf("msg argument missing")
|
|
}
|
|
|
|
resp, err := client.SignMessage(ctxb, &lnrpc.SignMessageRequest{Msg: msg})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var verifyMessageCommand = cli.Command{
|
|
Name: "verifymessage",
|
|
Category: "Wallet",
|
|
Usage: "Verify a message signed with the signature.",
|
|
ArgsUsage: "msg signature",
|
|
Description: `
|
|
Verify that the message was signed with a properly-formed signature
|
|
The signature must be zbase32 encoded and signed with the private key of
|
|
an active node in the resident node's channel database.
|
|
|
|
Positional arguments and flags can be used interchangeably but not at the same time!`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "msg",
|
|
Usage: "the message to verify",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "sig",
|
|
Usage: "the zbase32 encoded signature of the message",
|
|
},
|
|
},
|
|
Action: actionDecorator(verifyMessage),
|
|
}
|
|
|
|
func verifyMessage(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var (
|
|
msg []byte
|
|
sig string
|
|
)
|
|
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("msg"):
|
|
msg = []byte(ctx.String("msg"))
|
|
case args.Present():
|
|
msg = []byte(ctx.Args().First())
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("msg argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("sig"):
|
|
sig = ctx.String("sig")
|
|
case args.Present():
|
|
sig = args.First()
|
|
default:
|
|
return fmt.Errorf("signature argument missing")
|
|
}
|
|
|
|
req := &lnrpc.VerifyMessageRequest{Msg: msg, Signature: sig}
|
|
resp, err := client.VerifyMessage(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var feeReportCommand = cli.Command{
|
|
Name: "feereport",
|
|
Category: "Channels",
|
|
Usage: "Display the current fee policies of all active channels.",
|
|
Description: `
|
|
Returns the current fee policies of all active channels.
|
|
Fee policies can be updated using the updatechanpolicy command.`,
|
|
Action: actionDecorator(feeReport),
|
|
}
|
|
|
|
func feeReport(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
req := &lnrpc.FeeReportRequest{}
|
|
resp, err := client.FeeReport(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var updateChannelPolicyCommand = cli.Command{
|
|
Name: "updatechanpolicy",
|
|
Category: "Channels",
|
|
Usage: "Update the channel policy for all channels, or a single " +
|
|
"channel.",
|
|
ArgsUsage: "base_fee_msat fee_rate time_lock_delta [channel_point]",
|
|
Description: `
|
|
Updates the channel policy for all channels, or just a particular channel
|
|
identified by its channel point. The update will be committed, and
|
|
broadcast to the rest of the network within the next batch.
|
|
Channel points are encoded as: funding_txid:output_index`,
|
|
Flags: []cli.Flag{
|
|
cli.Int64Flag{
|
|
Name: "base_fee_msat",
|
|
Usage: "the base fee in milli-satoshis that will " +
|
|
"be charged for each forwarded HTLC, regardless " +
|
|
"of payment size",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "fee_rate",
|
|
Usage: "the fee rate that will be charged " +
|
|
"proportionally based on the value of each " +
|
|
"forwarded HTLC, the lowest possible rate is 0.000001",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "time_lock_delta",
|
|
Usage: "the CLTV delta that will be applied to all " +
|
|
"forwarded HTLCs",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "chan_point",
|
|
Usage: "The channel whose fee policy should be " +
|
|
"updated, if nil the policies for all channels " +
|
|
"will be updated. Takes the form of: txid:output_index",
|
|
},
|
|
},
|
|
Action: actionDecorator(updateChannelPolicy),
|
|
}
|
|
|
|
func updateChannelPolicy(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var (
|
|
baseFee int64
|
|
feeRate float64
|
|
timeLockDelta int64
|
|
err error
|
|
)
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("base_fee_msat"):
|
|
baseFee = ctx.Int64("base_fee_msat")
|
|
case args.Present():
|
|
baseFee, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode base_fee_msat: %v", err)
|
|
}
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("base_fee_msat argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("fee_rate"):
|
|
feeRate = ctx.Float64("fee_rate")
|
|
case args.Present():
|
|
feeRate, err = strconv.ParseFloat(args.First(), 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode fee_rate: %v", err)
|
|
}
|
|
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("fee_rate argument missing")
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("time_lock_delta"):
|
|
timeLockDelta = ctx.Int64("time_lock_delta")
|
|
case args.Present():
|
|
timeLockDelta, err = strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode time_lock_delta: %v",
|
|
err)
|
|
}
|
|
|
|
args = args.Tail()
|
|
default:
|
|
return fmt.Errorf("time_lock_delta argument missing")
|
|
}
|
|
|
|
var (
|
|
chanPoint *lnrpc.ChannelPoint
|
|
chanPointStr string
|
|
)
|
|
|
|
switch {
|
|
case ctx.IsSet("chan_point"):
|
|
chanPointStr = ctx.String("chan_point")
|
|
case args.Present():
|
|
chanPointStr = args.First()
|
|
}
|
|
|
|
if chanPointStr != "" {
|
|
split := strings.Split(chanPointStr, ":")
|
|
if len(split) != 2 {
|
|
return fmt.Errorf("expecting chan_point to be in format of: " +
|
|
"txid:index")
|
|
}
|
|
|
|
index, err := strconv.ParseInt(split[1], 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode output index: %v", err)
|
|
}
|
|
|
|
chanPoint = &lnrpc.ChannelPoint{
|
|
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
|
|
FundingTxidStr: split[0],
|
|
},
|
|
OutputIndex: uint32(index),
|
|
}
|
|
}
|
|
|
|
req := &lnrpc.PolicyUpdateRequest{
|
|
BaseFeeMsat: baseFee,
|
|
FeeRate: feeRate,
|
|
TimeLockDelta: uint32(timeLockDelta),
|
|
}
|
|
|
|
if chanPoint != nil {
|
|
req.Scope = &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
ChanPoint: chanPoint,
|
|
}
|
|
} else {
|
|
req.Scope = &lnrpc.PolicyUpdateRequest_Global{
|
|
Global: true,
|
|
}
|
|
}
|
|
|
|
resp, err := client.UpdateChannelPolicy(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var forwardingHistoryCommand = cli.Command{
|
|
Name: "fwdinghistory",
|
|
Category: "Payments",
|
|
Usage: "Query the history of all forwarded HTLCs.",
|
|
ArgsUsage: "start_time [end_time] [index_offset] [max_events]",
|
|
Description: `
|
|
Query the HTLC switch's internal forwarding log for all completed
|
|
payment circuits (HTLCs) over a particular time range (--start_time and
|
|
--end_time). The start and end times are meant to be expressed in
|
|
seconds since the Unix epoch. If a start and end time aren't provided,
|
|
then events over the past 24 hours are queried for.
|
|
|
|
The max number of events returned is 50k. The default number is 100,
|
|
callers can use the --max_events param to modify this value.
|
|
|
|
Finally, callers can skip a series of events using the --index_offset
|
|
parameter. Each response will contain the offset index of the last
|
|
entry. Using this callers can manually paginate within a time slice.
|
|
`,
|
|
Flags: []cli.Flag{
|
|
cli.Int64Flag{
|
|
Name: "start_time",
|
|
Usage: "the starting time for the query, expressed in " +
|
|
"seconds since the unix epoch",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "end_time",
|
|
Usage: "the end time for the query, expressed in " +
|
|
"seconds since the unix epoch",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "index_offset",
|
|
Usage: "the number of events to skip",
|
|
},
|
|
cli.Int64Flag{
|
|
Name: "max_events",
|
|
Usage: "the max number of events to return",
|
|
},
|
|
},
|
|
Action: actionDecorator(forwardingHistory),
|
|
}
|
|
|
|
func forwardingHistory(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client, cleanUp := getClient(ctx)
|
|
defer cleanUp()
|
|
|
|
var (
|
|
startTime, endTime uint64
|
|
indexOffset, maxEvents uint32
|
|
err error
|
|
)
|
|
args := ctx.Args()
|
|
|
|
switch {
|
|
case ctx.IsSet("start_time"):
|
|
startTime = ctx.Uint64("start_time")
|
|
case args.Present():
|
|
startTime, err = strconv.ParseUint(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode start_time %v", err)
|
|
}
|
|
args = args.Tail()
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("end_time"):
|
|
endTime = ctx.Uint64("end_time")
|
|
case args.Present():
|
|
endTime, err = strconv.ParseUint(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode end_time: %v", err)
|
|
}
|
|
args = args.Tail()
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("index_offset"):
|
|
indexOffset = uint32(ctx.Int64("index_offset"))
|
|
case args.Present():
|
|
i, err := strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode index_offset: %v", err)
|
|
}
|
|
indexOffset = uint32(i)
|
|
args = args.Tail()
|
|
}
|
|
|
|
switch {
|
|
case ctx.IsSet("max_events"):
|
|
maxEvents = uint32(ctx.Int64("max_events"))
|
|
case args.Present():
|
|
m, err := strconv.ParseInt(args.First(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode max_events: %v", err)
|
|
}
|
|
maxEvents = uint32(m)
|
|
args = args.Tail()
|
|
}
|
|
|
|
req := &lnrpc.ForwardingHistoryRequest{
|
|
StartTime: startTime,
|
|
EndTime: endTime,
|
|
IndexOffset: indexOffset,
|
|
NumMaxEvents: maxEvents,
|
|
}
|
|
resp, err := client.ForwardingHistory(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|