1010 lines
24 KiB
Go
1010 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/BitfuryLightning/tools/prefix_tree"
|
|
"github.com/BitfuryLightning/tools/rt"
|
|
"github.com/BitfuryLightning/tools/rt/graph"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/roasbeef/btcd/wire"
|
|
"github.com/urfave/cli"
|
|
"golang.org/x/net/context"
|
|
|
|
"github.com/BitfuryLightning/tools/rt/visualizer"
|
|
)
|
|
|
|
// TODO(roasbeef): cli logic for supporting both positional and unix style
|
|
// arguments.
|
|
|
|
func printRespJson(resp interface{}) {
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
fatal(err)
|
|
}
|
|
|
|
// TODO(roasbeef): disable 'omitempty' like behavior
|
|
|
|
var out bytes.Buffer
|
|
json.Indent(&out, b, "", "\t")
|
|
out.WriteTo(os.Stdout)
|
|
}
|
|
|
|
var ShellCommand = cli.Command{
|
|
Name: "shell",
|
|
Usage: "enter interactive shell",
|
|
Action: func(c *cli.Context) {
|
|
println("not implemented yet")
|
|
},
|
|
}
|
|
|
|
var NewAddressCommand = cli.Command{
|
|
Name: "newaddress",
|
|
Usage: "generates a new address. Three address types are supported: p2wkh, np2wkh, p2pkh",
|
|
Action: newAddress,
|
|
}
|
|
|
|
func newAddress(ctx *cli.Context) error {
|
|
client := getClient(ctx)
|
|
|
|
stringAddrType := ctx.Args().Get(0)
|
|
|
|
// 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.NewAddressRequest_AddressType
|
|
switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
|
|
case "p2wkh":
|
|
addrType = lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH
|
|
case "np2wkh":
|
|
addrType = lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH
|
|
case "p2pkh":
|
|
addrType = lnrpc.NewAddressRequest_PUBKEY_HASH
|
|
default:
|
|
return fmt.Errorf("invalid address type %v, support address type "+
|
|
"are: p2wkh, np2wkh, p2pkh", 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",
|
|
Description: "send a specified amount of bitcoin to the passed address",
|
|
Usage: "sendcoins --addr=<bitcoin addresss> --amt=<num coins in satoshis>",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "addr",
|
|
Usage: "the bitcoin address to send coins to on-chain",
|
|
},
|
|
// TODO(roasbeef): switch to BTC on command line? int may not be sufficient
|
|
cli.IntFlag{
|
|
Name: "amt",
|
|
Usage: "the number of bitcoin denominated in satoshis to send",
|
|
},
|
|
},
|
|
Action: sendCoins,
|
|
}
|
|
|
|
func sendCoins(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
req := &lnrpc.SendCoinsRequest{
|
|
Addr: ctx.String("addr"),
|
|
Amount: int64(ctx.Int("amt")),
|
|
}
|
|
txid, err := client.SendCoins(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(txid)
|
|
return nil
|
|
}
|
|
|
|
var SendManyCommand = cli.Command{
|
|
Name: "sendmany",
|
|
Description: "create and broadcast a transaction paying the specified " +
|
|
"amount(s) to the passed address(es)",
|
|
Usage: `sendmany '{"ExampleAddr": NumCoinsInSatoshis, "SecondAddr": NumCoins}'`,
|
|
Action: sendMany,
|
|
}
|
|
|
|
func sendMany(ctx *cli.Context) error {
|
|
var amountToAddr map[string]int64
|
|
|
|
jsonMap := ctx.Args().Get(0)
|
|
if err := json.Unmarshal([]byte(jsonMap), &amountToAddr); err != nil {
|
|
return err
|
|
}
|
|
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
txid, err := client.SendMany(ctxb, &lnrpc.SendManyRequest{amountToAddr})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(txid)
|
|
return nil
|
|
}
|
|
|
|
var ConnectCommand = cli.Command{
|
|
Name: "connect",
|
|
Usage: "connect to a remote lnd peer: <lnid>@host",
|
|
Action: connectPeer,
|
|
}
|
|
|
|
func connectPeer(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
targetAddress := ctx.Args().Get(0)
|
|
splitAddr := strings.Split(targetAddress, "@")
|
|
if len(splitAddr) != 2 {
|
|
return fmt.Errorf("target address expected in format: lnid@host:port")
|
|
}
|
|
|
|
addr := &lnrpc.LightningAddress{
|
|
PubKeyHash: splitAddr[0],
|
|
Host: splitAddr[1],
|
|
}
|
|
req := &lnrpc.ConnectPeerRequest{addr}
|
|
|
|
lnid, err := client.ConnectPeer(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(lnid)
|
|
return nil
|
|
}
|
|
|
|
// TODO(roasbeef): default number of confirmations
|
|
var OpenChannelCommand = cli.Command{
|
|
Name: "openchannel",
|
|
Description: "Attempt to open a new channel to an existing peer, " +
|
|
"optionally blocking until the channel is 'open'. Once the " +
|
|
"channel is open, a channelPoint (txid:vout) of the funding " +
|
|
"output is returned. NOTE: peer_id and lightning_id are " +
|
|
"mutually exclusive, only one should be used, not both.",
|
|
Usage: "openchannel --peer_id=X --local_amt=N --remote_amt=N --num_confs=N",
|
|
Flags: []cli.Flag{
|
|
cli.IntFlag{
|
|
Name: "peer_id",
|
|
Usage: "the relative id of the peer to open a channel with",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "lightning_id",
|
|
Usage: "the lightning id of the target peer",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "local_amt",
|
|
Usage: "the number of satoshis the wallet should commit to the channel",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "remote_amt",
|
|
Usage: "the number of satoshis the remote peer should commit to the channel",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "num_confs",
|
|
Usage: "the number of confirmations required before the " +
|
|
"channel is considered 'open'",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "block",
|
|
Usage: "block and wait until the channel is fully open",
|
|
},
|
|
},
|
|
Action: openChannel,
|
|
}
|
|
|
|
func openChannel(ctx *cli.Context) error {
|
|
// TODO(roasbeef): add deadline to context
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
if ctx.Int("peer_id") != 0 && ctx.String("lightning_id") != "" {
|
|
return fmt.Errorf("both peer_id and lightning_id cannot be set " +
|
|
"at the same time, only one can be specified")
|
|
}
|
|
|
|
req := &lnrpc.OpenChannelRequest{
|
|
LocalFundingAmount: int64(ctx.Int("local_amt")),
|
|
RemoteFundingAmount: int64(ctx.Int("remote_amt")),
|
|
NumConfs: uint32(ctx.Int("num_confs")),
|
|
}
|
|
|
|
if ctx.Int("peer_id") != 0 {
|
|
req.TargetPeerId = int32(ctx.Int("peer_id"))
|
|
} else {
|
|
lnID, err := hex.DecodeString(ctx.String("lightning_id"))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode lightning id: %v", err)
|
|
}
|
|
req.TargetNode = lnID
|
|
}
|
|
|
|
stream, err := client.OpenChannel(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ctx.Bool("block") {
|
|
return nil
|
|
}
|
|
|
|
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_ChanOpen:
|
|
channelPoint := update.ChanOpen.ChannelPoint
|
|
txid, err := wire.NewShaHash(channelPoint.FundingTxid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
index := channelPoint.OutputIndex
|
|
printRespJson(struct {
|
|
ChannelPoint string `json:"channel_point"`
|
|
}{
|
|
ChannelPoint: fmt.Sprintf("%v:%v", txid, index),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TODO(roasbeef): also allow short relative channel ID.
|
|
var CloseChannelCommand = cli.Command{
|
|
Name: "closechannel",
|
|
Description: "Close an existing channel. The channel can be closed either " +
|
|
"cooperatively, or uncooperatively (forced).",
|
|
Usage: "closechannel funding_txid output_index time_limit allow_force",
|
|
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 " +
|
|
"abandonded",
|
|
},
|
|
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",
|
|
},
|
|
},
|
|
Action: closeChannel,
|
|
}
|
|
|
|
func closeChannel(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
txid, err := wire.NewShaHashFromStr(ctx.String("funding_txid"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(roasbeef): implement time deadline within server
|
|
req := &lnrpc.CloseChannelRequest{
|
|
ChannelPoint: &lnrpc.ChannelPoint{
|
|
FundingTxid: txid[:],
|
|
OutputIndex: uint32(ctx.Int("output_index")),
|
|
},
|
|
Force: ctx.Bool("force"),
|
|
}
|
|
|
|
stream, err := client.CloseChannel(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ctx.Bool("block") {
|
|
return nil
|
|
}
|
|
|
|
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_ChanClose:
|
|
closingHash := update.ChanClose.ClosingTxid
|
|
txid, err := wire.NewShaHash(closingHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(struct {
|
|
ClosingTXID string `json:"closing_txid"`
|
|
}{
|
|
ClosingTXID: txid.String(),
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var ListPeersCommand = cli.Command{
|
|
Name: "listpeers",
|
|
Description: "List all active, currently connected peers.",
|
|
Action: listPeers,
|
|
}
|
|
|
|
func listPeers(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
req := &lnrpc.ListPeersRequest{}
|
|
resp, err := client.ListPeers(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(resp)
|
|
return nil
|
|
}
|
|
|
|
var WalletBalanceCommand = cli.Command{
|
|
Name: "walletbalance",
|
|
Description: "compute and display the wallet's current balance",
|
|
Usage: "walletbalance --witness_only=[true|false]",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "witness_only",
|
|
Usage: "if only witness outputs should be considered when " +
|
|
"calculating the wallet's balance",
|
|
},
|
|
},
|
|
Action: walletBalance,
|
|
}
|
|
|
|
func walletBalance(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
req := &lnrpc.WalletBalanceRequest{
|
|
WitnessOnly: ctx.Bool("witness_only"),
|
|
}
|
|
resp, err := client.WalletBalance(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(resp)
|
|
return nil
|
|
}
|
|
|
|
var ChannelBalanceCommand = cli.Command{
|
|
Name: "channelbalance",
|
|
Description: "returns the sum of the total available channel balance across all open channels",
|
|
Action: channelBalance,
|
|
}
|
|
|
|
func channelBalance(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
req := &lnrpc.ChannelBalanceRequest{}
|
|
resp, err := client.ChannelBalance(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(resp)
|
|
return nil
|
|
}
|
|
|
|
var GetInfoCommand = cli.Command{
|
|
Name: "getinfo",
|
|
Description: "returns basic information related to the active daemon",
|
|
Action: getInfo,
|
|
}
|
|
|
|
func getInfo(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
req := &lnrpc.GetInfoRequest{}
|
|
resp, err := client.GetInfo(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(resp)
|
|
return nil
|
|
}
|
|
|
|
var PendingChannelsCommand = cli.Command{
|
|
Name: "pendingchannels",
|
|
Description: "display information pertaining to pending channels",
|
|
Usage: "pendingchannels --status=[all|opening|closing]",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "open, o",
|
|
Usage: "display the status of new pending channels",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "close, c",
|
|
Usage: "display the status of channels being closed",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "all, a",
|
|
Usage: "display the status of channels in the " +
|
|
"process of being opened or closed",
|
|
},
|
|
},
|
|
Action: pendingChannels,
|
|
}
|
|
|
|
func pendingChannels(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
var channelStatus lnrpc.ChannelStatus
|
|
switch {
|
|
case ctx.Bool("all"):
|
|
channelStatus = lnrpc.ChannelStatus_ALL
|
|
case ctx.Bool("open"):
|
|
channelStatus = lnrpc.ChannelStatus_OPENING
|
|
case ctx.Bool("close"):
|
|
channelStatus = lnrpc.ChannelStatus_CLOSING
|
|
default:
|
|
channelStatus = lnrpc.ChannelStatus_ALL
|
|
}
|
|
|
|
req := &lnrpc.PendingChannelRequest{channelStatus}
|
|
resp, err := client.PendingChannels(ctxb, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var SendPaymentCommand = cli.Command{
|
|
Name: "sendpayment",
|
|
Description: "send a payment over lightning",
|
|
Usage: "sendpayment --dest=[node_id] --amt=[in_satoshis]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "dest, d",
|
|
Usage: "lightning address of the payment recipient",
|
|
},
|
|
cli.IntFlag{ // TODO(roasbeef): float64?
|
|
Name: "amt, a",
|
|
Usage: "number of satoshis to send",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "payment_hash, r",
|
|
Usage: "the hash to use within the payment's HTLC",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "fast, f",
|
|
Usage: "skip the HTLC trickle logic, immediately creating a " +
|
|
"new commitment",
|
|
},
|
|
},
|
|
Action: sendPaymentCommand,
|
|
}
|
|
|
|
func sendPaymentCommand(ctx *cli.Context) error {
|
|
client := getClient(ctx)
|
|
|
|
destAddr, err := hex.DecodeString(ctx.String("dest"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rHash, err := hex.DecodeString(ctx.String("payment_hash"))
|
|
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 := &lnrpc.SendRequest{
|
|
Dest: destAddr,
|
|
Amt: int64(ctx.Int("amt")),
|
|
PaymentHash: rHash[:],
|
|
FastSend: ctx.Bool("fast"),
|
|
}
|
|
|
|
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()
|
|
|
|
printRespJson(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var AddInvoiceCommand = cli.Command{
|
|
Name: "addinvoice",
|
|
Description: "add a new invoice, expressing intent for a future payment",
|
|
Usage: "addinvoice --memo=[note] --receipt=[sig+contract hash] --value=[in_satoshis] --preimage=[32_byte_hash]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "memo",
|
|
Usage: "an optional memo to attach along with the invoice",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "receipt",
|
|
Usage: "an optional cryptographic receipt of payment",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "preimage",
|
|
Usage: "the hex-encoded preimage which will allow settling an incoming HTLC payable to this preimage",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "value",
|
|
Usage: "the value of this invoice in satoshis",
|
|
},
|
|
},
|
|
Action: addInvoice,
|
|
}
|
|
|
|
func addInvoice(ctx *cli.Context) error {
|
|
client := getClient(ctx)
|
|
|
|
preimage, err := hex.DecodeString(ctx.String("preimage"))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse preimage: %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: int64(ctx.Int("value")),
|
|
}
|
|
|
|
resp, err := client.AddInvoice(context.Background(), invoice)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(struct {
|
|
RHash string `json:"r_hash"`
|
|
}{
|
|
RHash: hex.EncodeToString(resp.RHash),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
var LookupInvoiceCommand = cli.Command{
|
|
Name: "lookupinvoice",
|
|
Description: "lookup an existing invoice by its payment hash",
|
|
Usage: "lookupinvoice --rhash=[32_byte_hash]",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "rhash",
|
|
Usage: "the payment hash of the invoice to query for, the hash " +
|
|
"should be a hex-encoded string",
|
|
},
|
|
},
|
|
Action: lookupInvoice,
|
|
}
|
|
|
|
func lookupInvoice(ctx *cli.Context) error {
|
|
client := getClient(ctx)
|
|
|
|
rHash, err := hex.DecodeString(ctx.String("rhash"))
|
|
if err != nil {
|
|
return 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",
|
|
Usage: "listinvoice --pending_only=[true|false]",
|
|
Description: "list all invoices currently stored",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "pending_only",
|
|
Usage: "toggles if all invoices should be returned, or only " +
|
|
"those that are currently unsettled",
|
|
},
|
|
},
|
|
Action: listInvoices,
|
|
}
|
|
|
|
func listInvoices(ctx *cli.Context) error {
|
|
client := getClient(ctx)
|
|
|
|
pendingOnly := true
|
|
if !ctx.Bool("pending_only") {
|
|
pendingOnly = false
|
|
}
|
|
|
|
req := &lnrpc.ListInvoiceRequest{
|
|
PendingOnly: pendingOnly,
|
|
}
|
|
|
|
invoices, err := client.ListInvoices(context.Background(), req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJson(invoices)
|
|
|
|
return nil
|
|
}
|
|
|
|
var ShowRoutingTableCommand = cli.Command{
|
|
Name: "showroutingtable",
|
|
Description: "shows routing table for a node",
|
|
Usage: "showroutingtable text|image",
|
|
Subcommands: []cli.Command{
|
|
{
|
|
Name: "text",
|
|
Usage: "[--table|--human]",
|
|
Description: "Show routing table in textual format. By default in JSON",
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "table",
|
|
Usage: "Print channels in routing table in table format.",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "human",
|
|
Usage: "Print channels in routing table in table format. Output lightning_id partially - only a few first symbols which uniquelly identifies it.",
|
|
},
|
|
},
|
|
Action: showRoutingTableAsText,
|
|
},
|
|
{
|
|
Name: "image",
|
|
Usage: "[--type <IMAGE_TYPE>] [--dest OUTPUT_FILE] [--open]",
|
|
Description: "Create image with graphical representation of routing table",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "type",
|
|
Usage: "Type of image file. Use one of: http://www.graphviz.org/content/output-formats. Usage of this option supresses textual output",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "dest",
|
|
Usage: "Specifies where to save the generated file. If don't specified use os.TempDir Usage of this option supresses textual output",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "open",
|
|
Usage: "Open generated file automatically. Uses command line \"open\" command",
|
|
},
|
|
},
|
|
Action: showRoutingTableAsImage,
|
|
},
|
|
},
|
|
}
|
|
|
|
func getRoutingTable(ctxb context.Context, client lnrpc.LightningClient) (*rt.RoutingTable, error) {
|
|
req := &lnrpc.ShowRoutingTableRequest{}
|
|
resp, err := client.ShowRoutingTable(ctxb, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := rt.NewRoutingTable()
|
|
for _, channel := range resp.Channels {
|
|
r.AddChannel(
|
|
graph.NewID(channel.Id1),
|
|
graph.NewID(channel.Id2),
|
|
graph.NewEdgeID(channel.Outpoint),
|
|
&rt.ChannelInfo{channel.Capacity, channel.Weight},
|
|
)
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func showRoutingTableAsText(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
r, err := getRoutingTable(ctxb, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ctx.Bool("table") && ctx.Bool("human") {
|
|
return fmt.Errorf("--table and --human cannot be used at the same time")
|
|
}
|
|
|
|
if ctx.Bool("table") {
|
|
printRTAsTable(r, false)
|
|
} else if ctx.Bool("human") {
|
|
printRTAsTable(r, true)
|
|
} else {
|
|
printRTAsJSON(r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func showRoutingTableAsImage(ctx *cli.Context) error {
|
|
ctxb := context.Background()
|
|
client := getClient(ctx)
|
|
|
|
r, err := getRoutingTable(ctxb, client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqGetInfo := &lnrpc.GetInfoRequest{}
|
|
respGetInfo, err := client.GetInfo(ctxb, reqGetInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selfLightningId, err := hex.DecodeString(respGetInfo.LightningId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
imgType := ctx.String("type")
|
|
imgDest := ctx.String("dest")
|
|
if imgType == "" && imgDest == "" {
|
|
return fmt.Errorf("One or both of --type or --dest should be specified")
|
|
}
|
|
|
|
tempFile, err := ioutil.TempFile("", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var imageFile *os.File
|
|
// if the type is not specified explicitly parse the filename
|
|
if imgType == "" {
|
|
imgType = filepath.Ext(imgDest)[1:]
|
|
}
|
|
// if the filename is not specified explicitly use tempfile
|
|
if imgDest == "" {
|
|
imageFile, err = TempFileWithSuffix("", "rt_", "."+imgType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
imageFile, err = os.Create(imgDest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if _, ok := visualizer.SupportedFormatsAsMap()[imgType]; !ok {
|
|
fmt.Printf("Format: '%v' not recognized. Use one of: %v\n", imgType, visualizer.SupportedFormats())
|
|
return nil
|
|
}
|
|
// generate description graph by dot language
|
|
err = writeToTempFile(r, tempFile, selfLightningId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = writeToImageFile(tempFile, imageFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ctx.Bool("open") {
|
|
if err := visualizer.Open(imageFile); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeToTempFile(r *rt.RoutingTable, file *os.File, self []byte) error {
|
|
slc := []graph.ID{graph.NewID(string(self))}
|
|
viz := visualizer.New(r.G, slc, nil, nil)
|
|
viz.ApplyToNode = func(s string) string { return hex.EncodeToString([]byte(s)) }
|
|
viz.ApplyToEdge = func(info interface{}) string {
|
|
if info, ok := info.(*rt.ChannelInfo); ok {
|
|
return fmt.Sprintf(`"%v"`, info.Capacity())
|
|
}
|
|
return "nil"
|
|
}
|
|
// need to call method if plan to use shortcut, autocomplete, etc
|
|
viz.BuildPrefixTree()
|
|
viz.EnableShortcut(true)
|
|
dot := viz.Draw()
|
|
_, err := file.Write([]byte(dot))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = file.Sync()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeToImageFile(TempFile, ImageFile *os.File) error {
|
|
err := visualizer.Run("neato", TempFile, ImageFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = TempFile.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Remove(TempFile.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ImageFile.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// get around a bug in the standard library, add suffix param
|
|
func TempFileWithSuffix(dir, prefix, suffix string) (*os.File, error) {
|
|
f, err := ioutil.TempFile(dir, prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.Remove(f.Name())
|
|
f, err = os.Create(f.Name() + suffix)
|
|
return f, err
|
|
}
|
|
|
|
// Prints routing table in human readable table format
|
|
func printRTAsTable(r *rt.RoutingTable, humanForm bool) {
|
|
// Minimum length of data part to which name can be shortened
|
|
var minLen int
|
|
var tmpl string
|
|
var lightningIdTree, edgeIdTree prefix_tree.PrefixTree
|
|
if humanForm {
|
|
tmpl = "%-10v %-10v %-10v %-10v %-10v\n"
|
|
minLen = 6
|
|
} else {
|
|
tmpl = "%-64v %-64v %-66v %-10v %-10v\n"
|
|
minLen = 100
|
|
}
|
|
fmt.Printf(tmpl, "ID1", "ID2", "Outpoint", "Capacity", "Weight")
|
|
channels := r.AllChannels()
|
|
if humanForm {
|
|
// Generate prefix tree for shortcuts
|
|
lightningIdTree = prefix_tree.NewPrefixTree()
|
|
for _, node := range r.Nodes() {
|
|
lightningIdTree.Add(hex.EncodeToString([]byte(node.String())))
|
|
}
|
|
edgeIdTree = prefix_tree.NewPrefixTree()
|
|
for _, channel := range channels {
|
|
edgeIdTree.Add(channel.EdgeID.String())
|
|
}
|
|
}
|
|
for _, channel := range channels {
|
|
var source, target, edgeId string
|
|
sourceHex := hex.EncodeToString([]byte(channel.Id1.String()))
|
|
targetHex := hex.EncodeToString([]byte(channel.Id2.String()))
|
|
edgeIdRaw := channel.EdgeID.String()
|
|
if humanForm {
|
|
source = getShortcut(lightningIdTree, sourceHex, minLen)
|
|
target = getShortcut(lightningIdTree, targetHex, minLen)
|
|
edgeId = getShortcut(edgeIdTree, edgeIdRaw, minLen)
|
|
} else {
|
|
source = sourceHex
|
|
target = targetHex
|
|
edgeId = edgeIdRaw
|
|
}
|
|
fmt.Printf(tmpl, source, target, edgeId, channel.Info.Cpt, channel.Info.Wgt)
|
|
}
|
|
}
|
|
|
|
func getShortcut(tree prefix_tree.PrefixTree, s string, minLen int) string {
|
|
s1, err := tree.Shortcut(s)
|
|
if err != nil || s == s1 {
|
|
return s
|
|
}
|
|
if len(s1) < minLen && minLen < len(s) {
|
|
s1 = s[:minLen]
|
|
}
|
|
shortcut := fmt.Sprintf("%v...", s1)
|
|
if len(shortcut) >= len(s) {
|
|
shortcut = s
|
|
}
|
|
return shortcut
|
|
}
|
|
|
|
func printRTAsJSON(r *rt.RoutingTable) {
|
|
type ChannelDesc struct {
|
|
ID1 string `json:"lightning_id1"`
|
|
ID2 string `json:"lightning_id2"`
|
|
EdgeId string `json:"outpoint"`
|
|
Capacity int64 `json:"capacity"`
|
|
Weight float64 `json:"weight"`
|
|
}
|
|
var channels struct {
|
|
Channels []ChannelDesc `json:"channels"`
|
|
}
|
|
channelsRaw := r.AllChannels()
|
|
channels.Channels = make([]ChannelDesc, 0, len(channelsRaw))
|
|
for _, channelRaw := range channelsRaw {
|
|
sourceHex := hex.EncodeToString([]byte(channelRaw.Id1.String()))
|
|
targetHex := hex.EncodeToString([]byte(channelRaw.Id2.String()))
|
|
channels.Channels = append(channels.Channels,
|
|
ChannelDesc{
|
|
ID1: sourceHex,
|
|
ID2: targetHex,
|
|
EdgeId: channelRaw.EdgeID.String(),
|
|
Weight: channelRaw.Info.Weight(),
|
|
Capacity: channelRaw.Info.Capacity(),
|
|
},
|
|
)
|
|
}
|
|
printRespJson(channels)
|
|
}
|