From 8d7e07d96be5c38ccf1e158935902366cb3f8cc3 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 15 Apr 2020 15:59:40 +0200 Subject: [PATCH] lncli: live table-based payment updates --- cmd/lncli/cmd_pay.go | 263 ++++++++++++++++++++++++++++++++++++++----- go.mod | 4 + go.sum | 28 +++++ 3 files changed, 265 insertions(+), 30 deletions(-) diff --git a/cmd/lncli/cmd_pay.go b/cmd/lncli/cmd_pay.go index 11abacbf..3388a4b9 100644 --- a/cmd/lncli/cmd_pay.go +++ b/cmd/lncli/cmd_pay.go @@ -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)) +} diff --git a/go.mod b/go.mod index a4943762..e5f42da9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c58f8025..85a5e155 100644 --- a/go.sum +++ b/go.sum @@ -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=