lncli: live table-based payment updates

This commit is contained in:
Joost Jager 2020-04-15 15:59:40 +02:00
parent ce0a61abeb
commit 8d7e07d96b
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
3 changed files with 265 additions and 30 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
@ -8,9 +9,13 @@ import (
"fmt"
"io/ioutil"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/jedib0t/go-pretty/table"
"github.com/jedib0t/go-pretty/text"
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
@ -47,10 +52,10 @@ var (
"Custom record ids start from 65536.",
}
showInflightFlag = cli.BoolFlag{
Name: "show_inflight",
inflightUpdatesFlag = cli.BoolFlag{
Name: "inflight_updates",
Usage: "if set, intermediate payment state updates will be " +
"displayed",
"displayed. Only valid in combination with --json.",
}
maxShardsFlag = cli.UintFlag{
@ -59,6 +64,13 @@ var (
"used",
Value: 1,
}
jsonFlag = cli.BoolFlag{
Name: "json",
Usage: "if set, payment updates are printed as json " +
"messages. Set by default on Windows because table " +
"formatting is unsupported.",
}
)
// paymentFlags returns common flags for sendpayment and payinvoice.
@ -95,7 +107,7 @@ func paymentFlags() []cli.Flag {
Name: "allow_self_payment",
Usage: "allow sending a circular payment to self",
},
dataFlag, showInflightFlag, maxShardsFlag,
dataFlag, inflightUpdatesFlag, maxShardsFlag, jsonFlag,
}
}
@ -397,32 +409,30 @@ func sendPaymentRequest(ctx *cli.Context,
req.FeeLimitSat = feeLimit
req.NoInflightUpdates = !ctx.Bool(showInflightFlag.Name)
// Always print in-flight updates for the table output.
printJSON := ctx.Bool(jsonFlag.Name)
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
stream, err := routerClient.SendPaymentV2(context.Background(), req)
if err != nil {
return err
}
for {
status, err := stream.Recv()
if err != nil {
return err
}
printRespJSON(status)
if status.Status != lnrpc.Payment_IN_FLIGHT {
// If we get a payment error back, we pass an error up
// to main which eventually calls fatal() and returns
// with a non-zero exit code.
if status.Status != lnrpc.Payment_SUCCEEDED {
return errors.New(status.Status.String())
}
return nil
}
finalState, err := printLivePayment(
stream, client, printJSON,
)
if err != nil {
return err
}
// If we get a payment error back, we pass an error up
// to main which eventually calls fatal() and returns
// with a non-zero exit code.
if finalState.Status != lnrpc.Payment_SUCCEEDED {
return errors.New(finalState.Status.String())
}
return nil
}
var trackPaymentCommand = cli.Command{
@ -443,7 +453,7 @@ func trackPayment(ctx *cli.Context) error {
conn := getClientConn(ctx, false)
defer conn.Close()
client := routerrpc.NewRouterClient(conn)
routerClient := routerrpc.NewRouterClient(conn)
if !args.Present() {
return fmt.Errorf("hash argument missing")
@ -458,25 +468,206 @@ func trackPayment(ctx *cli.Context) error {
PaymentHash: hash,
}
stream, err := client.TrackPaymentV2(context.Background(), req)
stream, err := routerClient.TrackPaymentV2(context.Background(), req)
if err != nil {
return err
}
client := lnrpc.NewLightningClient(conn)
_, err = printLivePayment(stream, client, ctx.Bool(jsonFlag.Name))
return err
}
// printLivePayment receives payment updates from the given stream and either
// outputs them as json or as a more user-friendly formatted table. The table
// option uses terminal control codes to rewrite the output. This call
// terminates when the payment reaches a final state.
func printLivePayment(stream routerrpc.Router_TrackPaymentV2Client,
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
// Terminal escape codes aren't supported on Windows, fall back to json.
if !json && runtime.GOOS == "windows" {
json = true
}
aliases := newAliasCache(client)
first := true
var lastLineCount int
for {
status, err := stream.Recv()
payment, err := stream.Recv()
if err != nil {
return err
return nil, err
}
printRespJSON(status)
if json {
// Delimit json messages by newlines (inspired by
// grpc over rest chunking).
if first {
first = false
} else {
fmt.Println()
}
if status.Status != lnrpc.Payment_IN_FLIGHT {
return nil
// Write raw json to stdout.
printRespJSON(payment)
} else {
table := formatPayment(payment, aliases)
// Clear all previously written lines and print the
// updated table.
clearLines(lastLineCount)
fmt.Print(table)
// Store the number of lines written for the next update
// pass.
lastLineCount = 0
for _, b := range table {
if b == '\n' {
lastLineCount++
}
}
}
// Terminate loop if payments state is final.
if payment.Status != lnrpc.Payment_IN_FLIGHT {
return payment, nil
}
}
}
// aliasCache allows cached retrieval of node aliases.
type aliasCache struct {
cache map[string]string
client lnrpc.LightningClient
}
func newAliasCache(client lnrpc.LightningClient) *aliasCache {
return &aliasCache{
client: client,
cache: make(map[string]string),
}
}
// get returns a node alias either from cache or freshly requested from lnd.
func (a *aliasCache) get(pubkey string) string {
alias, ok := a.cache[pubkey]
if ok {
return alias
}
// Request node info.
resp, err := a.client.GetNodeInfo(
context.Background(),
&lnrpc.NodeInfoRequest{
PubKey: pubkey,
},
)
if err != nil {
// If no info is available, use the
// pubkey as identifier.
alias = pubkey[:6]
} else {
alias = resp.Node.Alias
}
a.cache[pubkey] = alias
return alias
}
// formatMsat formats msat amounts as fractional sats.
func formatMsat(amt int64) string {
return strconv.FormatFloat(float64(amt)/1000.0, 'f', -1, 64)
}
// formatPayment formats the payment state as an ascii table.
func formatPayment(payment *lnrpc.Payment, aliases *aliasCache) string {
t := table.NewWriter()
// Build table header.
t.AppendHeader(table.Row{
"HTLC_STATE", "ATTEMPT_TIME", "RESOLVE_TIME", "RECEIVER_AMT",
"FEE", "TIMELOCK", "CHAN_OUT", "ROUTE",
})
t.SetColumnConfigs([]table.ColumnConfig{
{Name: "ATTEMPT_TIME", Align: text.AlignRight},
{Name: "RESOLVE_TIME", Align: text.AlignRight},
{Name: "CHAN_OUT", Align: text.AlignLeft,
AlignHeader: text.AlignLeft},
})
// Add all htlcs as rows.
createTime := time.Unix(0, payment.CreationTimeNs)
var totalPaid, totalFees int64
for _, htlc := range payment.Htlcs {
formatTime := func(timeNs int64) string {
if timeNs == 0 {
return "-"
}
resolveTime := time.Unix(0, timeNs)
resolveTimeMs := resolveTime.Sub(createTime).
Milliseconds()
return fmt.Sprintf(
"%.3f", float64(resolveTimeMs)/1000.0,
)
}
attemptTime := formatTime(htlc.AttemptTimeNs)
resolveTime := formatTime(htlc.ResolveTimeNs)
route := htlc.Route
lastHop := route.Hops[len(route.Hops)-1]
hops := []string{}
for _, h := range route.Hops {
alias := aliases.get(h.PubKey)
hops = append(hops, alias)
}
state := htlc.Status.String()
if htlc.Failure != nil {
state = fmt.Sprintf(
"%v @ %v",
htlc.Failure.Code,
htlc.Failure.FailureSourceIndex,
)
}
t.AppendRow([]interface{}{
state, attemptTime, resolveTime,
formatMsat(lastHop.AmtToForwardMsat),
formatMsat(route.TotalFeesMsat),
route.TotalTimeLock, route.Hops[0].ChanId,
strings.Join(hops, "->")},
)
if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
totalPaid += lastHop.AmtToForwardMsat
totalFees += route.TotalFeesMsat
}
}
// Render table.
b := &bytes.Buffer{}
t.SetOutputMirror(b)
t.Render()
// Add additional payment-level data.
fmt.Fprintf(b, "Amount + fee: %v + %v sat\n",
formatMsat(totalPaid), formatMsat(totalFees))
fmt.Fprintf(b, "Payment hash: %v\n", payment.PaymentHash)
fmt.Fprintf(b, "Payment status: %v", payment.Status)
switch payment.Status {
case lnrpc.Payment_SUCCEEDED:
fmt.Fprintf(b, ", preimage: %v", payment.PaymentPreimage)
case lnrpc.Payment_FAILED:
fmt.Fprintf(b, ", reason: %v", payment.FailureReason)
}
fmt.Fprintf(b, "\n")
return b.String()
}
var payInvoiceCommand = cli.Command{
Name: "payinvoice",
Category: "Payments",
@ -672,3 +863,15 @@ func sendToRouteRequest(ctx *cli.Context, req *lnrpc.SendToRouteRequest) error {
return nil
}
// ESC is the ASCII code for escape character
const ESC = 27
// clearCode defines a terminal escape code to clear the currently line and move
// the cursor up.
var clearCode = fmt.Sprintf("%c[%dA%c[2K", ESC, 1, ESC)
// clearLines erases the last count lines in the terminal window.
func clearLines(count int) {
_, _ = fmt.Print(strings.Repeat(clearCode, count))
}

4
go.mod
View File

@ -17,12 +17,14 @@ require (
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941
github.com/davecgh/go-spew v1.1.1
github.com/go-errors/errors v1.0.1
github.com/go-openapi/strfmt v0.19.5 // indirect
github.com/golang/protobuf v1.3.1
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.8.6
github.com/jackpal/gateway v1.0.5
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad
github.com/jedib0t/go-pretty v4.3.0+incompatible
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/logrotate v1.0.0
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c // indirect
@ -40,6 +42,7 @@ require (
github.com/lightningnetwork/lnd/queue v1.0.3
github.com/lightningnetwork/lnd/ticker v1.0.0
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8
github.com/prometheus/client_golang v0.9.3
github.com/rogpeppe/fastuuid v1.2.0 // indirect
@ -47,6 +50,7 @@ require (
github.com/urfave/cli v1.18.0
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922
google.golang.org/grpc v1.19.0

28
go.sum
View File

@ -15,6 +15,8 @@ github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -66,6 +68,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
@ -79,6 +82,11 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM=
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@ -90,6 +98,10 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42 h1:q3pnF5JFBNRz8sRD+IRj7Y6DMyYGTNqnZ9axTbSfoNI=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
@ -102,6 +114,8 @@ github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@ -145,10 +159,14 @@ github.com/lightningnetwork/lightning-onion v1.0.1/go.mod h1:rigfi6Af/KqsF7Za0hO
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -178,15 +196,23 @@ github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/urfave/cli v1.18.0 h1:m9MfmZWX7bwr9kUcs/Asr95j0IVXzGNNc+/5ku2m26Q=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -214,6 +240,8 @@ golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7v
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd h1:DBH9mDw0zluJT/R+nGuV3jWFWLFaHyYZWD4tOT+cjn0=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=