diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index 5832ce20..384e3d7d 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -306,6 +306,7 @@ func main() { app.Commands = append(app.Commands, routerCommands()...) app.Commands = append(app.Commands, walletCommands()...) app.Commands = append(app.Commands, watchtowerCommands()...) + app.Commands = append(app.Commands, wtclientCommands()...) if err := app.Run(os.Args); err != nil { fatal(err) diff --git a/cmd/lncli/wtclient_active.go b/cmd/lncli/wtclient_active.go new file mode 100644 index 00000000..c58f3808 --- /dev/null +++ b/cmd/lncli/wtclient_active.go @@ -0,0 +1,283 @@ +// +build wtclientrpc + +package main + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" + "github.com/urfave/cli" +) + +// wtclientCommands will return nil for non-wtclientrpc builds. +func wtclientCommands() []cli.Command { + return []cli.Command{ + { + Name: "wtclient", + Usage: "Interact with the watchtower client.", + Category: "Watchtower", + Subcommands: []cli.Command{ + addTowerCommand, + removeTowerCommand, + listTowersCommand, + getTowerCommand, + statsCommand, + policyCommand, + }, + }, + } +} + +// getWtclient initializes a connection to the watchtower client RPC in order to +// interact with it. +func getWtclient(ctx *cli.Context) (wtclientrpc.WatchtowerClientClient, func()) { + conn := getClientConn(ctx, false) + cleanUp := func() { + conn.Close() + } + return wtclientrpc.NewWatchtowerClientClient(conn), cleanUp +} + +var addTowerCommand = cli.Command{ + Name: "add", + Usage: "Register a watchtower to use for future sessions/backups.", + Description: "If the watchtower has already been registered, then " + + "this command serves as a way of updating the watchtower " + + "with new addresses it is reachable over.", + ArgsUsage: "pubkey@address", + Action: actionDecorator(addTower), +} + +func addTower(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() != 1 || ctx.NumFlags() > 0 { + return cli.ShowCommandHelp(ctx, "add") + } + + parts := strings.Split(ctx.Args().First(), "@") + if len(parts) != 2 { + return errors.New("expected tower of format pubkey@address") + } + pubKey, err := hex.DecodeString(parts[0]) + if err != nil { + return fmt.Errorf("invalid public key: %v", err) + } + address := parts[1] + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.AddTowerRequest{ + Pubkey: pubKey, + Address: address, + } + resp, err := client.AddTower(context.Background(), req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + +var removeTowerCommand = cli.Command{ + Name: "remove", + Usage: "Remove a watchtower to prevent its use for future " + + "sessions/backups.", + Description: "An optional address can be provided to remove, " + + "indicating that the watchtower is no longer reachable at " + + "this address. If an address isn't provided, then the " + + "watchtower will no longer be used for future sessions/backups.", + ArgsUsage: "pubkey | pubkey@address", + Action: actionDecorator(removeTower), +} + +func removeTower(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() != 1 || ctx.NumFlags() > 0 { + return cli.ShowCommandHelp(ctx, "remove") + } + + // The command can have only one argument, but it can be interpreted in + // either of the following formats: + // + // pubkey or pubkey@address + // + // The hex-encoded public key of the watchtower is always required, + // while the second is an optional address we'll remove from the + // watchtower's database record. + parts := strings.Split(ctx.Args().First(), "@") + if len(parts) > 2 { + return errors.New("expected tower of format pubkey@address") + } + pubKey, err := hex.DecodeString(parts[0]) + if err != nil { + return fmt.Errorf("invalid public key: %v", err) + } + var address string + if len(parts) == 2 { + address = parts[1] + } + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.RemoveTowerRequest{ + Pubkey: pubKey, + Address: address, + } + resp, err := client.RemoveTower(context.Background(), req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + +var listTowersCommand = cli.Command{ + Name: "towers", + Usage: "Display information about all registered watchtowers.", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "include_sessions", + Usage: "include sessions with the watchtower in the " + + "response", + }, + }, + Action: actionDecorator(listTowers), +} + +func listTowers(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() > 0 || ctx.NumFlags() > 1 { + return cli.ShowCommandHelp(ctx, "towers") + } + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.ListTowersRequest{ + IncludeSessions: ctx.Bool("include_sessions"), + } + resp, err := client.ListTowers(context.Background(), req) + if err != nil { + return err + } + + var listTowersResp = struct { + Towers []*Tower `json:"towers"` + }{ + Towers: make([]*Tower, len(resp.Towers)), + } + for i, tower := range resp.Towers { + listTowersResp.Towers[i] = NewTowerFromProto(tower) + } + + printJSON(listTowersResp) + return nil +} + +var getTowerCommand = cli.Command{ + Name: "tower", + Usage: "Display information about a specific registered watchtower.", + ArgsUsage: "pubkey", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "include_sessions", + Usage: "include sessions with the watchtower in the " + + "response", + }, + }, + Action: actionDecorator(getTower), +} + +func getTower(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() != 1 || ctx.NumFlags() > 1 { + return cli.ShowCommandHelp(ctx, "tower") + } + + // The command only has one argument, which we expect to be the + // hex-encoded public key of the watchtower we'll display information + // about. + pubKey, err := hex.DecodeString(ctx.Args().Get(0)) + if err != nil { + return fmt.Errorf("invalid public key: %v", err) + } + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.GetTowerInfoRequest{ + Pubkey: pubKey, + IncludeSessions: ctx.Bool("include_sessions"), + } + resp, err := client.GetTowerInfo(context.Background(), req) + if err != nil { + return err + } + + printJSON(NewTowerFromProto(resp)) + return nil +} + +var statsCommand = cli.Command{ + Name: "stats", + Usage: "Display the session stats of the watchtower client.", + Action: actionDecorator(stats), +} + +func stats(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() > 0 || ctx.NumFlags() > 0 { + return cli.ShowCommandHelp(ctx, "stats") + } + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.StatsRequest{} + resp, err := client.Stats(context.Background(), req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} + +var policyCommand = cli.Command{ + Name: "policy", + Usage: "Display the active watchtower client policy configuration.", + Action: actionDecorator(policy), +} + +func policy(ctx *cli.Context) error { + // Display the command's help message if the number of arguments/flags + // is not what we expect. + if ctx.NArg() > 0 || ctx.NumFlags() > 0 { + return cli.ShowCommandHelp(ctx, "policy") + } + + client, cleanUp := getWtclient(ctx) + defer cleanUp() + + req := &wtclientrpc.PolicyRequest{} + resp, err := client.Policy(context.Background(), req) + if err != nil { + return err + } + + printRespJSON(resp) + return nil +} diff --git a/cmd/lncli/wtclient_default.go b/cmd/lncli/wtclient_default.go new file mode 100644 index 00000000..d1e3c1dd --- /dev/null +++ b/cmd/lncli/wtclient_default.go @@ -0,0 +1,10 @@ +// +build !wtclientrpc + +package main + +import "github.com/urfave/cli" + +// wtclientCommands will return nil for non-wtclientrpc builds. +func wtclientCommands() []cli.Command { + return nil +} diff --git a/cmd/lncli/wtclient_types.go b/cmd/lncli/wtclient_types.go new file mode 100644 index 00000000..7dcf5bf5 --- /dev/null +++ b/cmd/lncli/wtclient_types.go @@ -0,0 +1,52 @@ +// +build wtclientrpc + +package main + +import ( + "encoding/hex" + + "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" +) + +// TowerSession encompasses information about a tower session. +type TowerSession struct { + NumBackups uint32 `json:"num_backups"` + NumPendingBackups uint32 `json:"num_pending_backups"` + MaxBackups uint32 `json:"max_backups"` + SweepSatPerByte uint32 `json:"sweep_sat_per_byte"` +} + +// NewTowerSessionsFromProto converts a set of tower sessions from their RPC +// type to a CLI-friendly type. +func NewTowerSessionsFromProto(sessions []*wtclientrpc.TowerSession) []*TowerSession { + towerSessions := make([]*TowerSession, 0, len(sessions)) + for _, session := range sessions { + towerSessions = append(towerSessions, &TowerSession{ + NumBackups: session.NumBackups, + NumPendingBackups: session.NumPendingBackups, + MaxBackups: session.MaxBackups, + SweepSatPerByte: session.SweepSatPerByte, + }) + } + return towerSessions +} + +// Tower encompasses information about a registered watchtower. +type Tower struct { + PubKey string `json:"pubkey"` + Addresses []string `json:"addresses"` + ActiveSessionCandidate bool `json:"active_session_candidate"` + NumSessions uint32 `json:"num_sessions"` + Sessions []*TowerSession `json:"sessions"` +} + +// NewTowerFromProto converts a tower from its RPC type to a CLI-friendly type. +func NewTowerFromProto(tower *wtclientrpc.Tower) *Tower { + return &Tower{ + PubKey: hex.EncodeToString(tower.Pubkey), + Addresses: tower.Addresses, + ActiveSessionCandidate: tower.ActiveSessionCandidate, + NumSessions: tower.NumSessions, + Sessions: NewTowerSessionsFromProto(tower.Sessions), + } +}