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= --amt=", 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: @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 ] [--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) }