diff --git a/cmd/lncli/arg_parse.go b/cmd/lncli/arg_parse.go new file mode 100644 index 00000000..3f621a9a --- /dev/null +++ b/cmd/lncli/arg_parse.go @@ -0,0 +1,40 @@ +package main + +import ( + "regexp" + "strconv" + "time" +) + +// reTimeRange matches systemd.time-like short negative timeranges, e.g. "-200s". +var reTimeRange = regexp.MustCompile(`^-\d{1,18}[s|m|h|d|w|M|y]$`) + +// secondsPer allows translating s(seconds), m(minutes), h(ours), d(ays), +// w(eeks), M(onths) and y(ears) into corresponding seconds. +var secondsPer = map[string]int64{ + "s": 1, + "m": 60, + "h": 3600, + "d": 86400, + "w": 604800, + "M": 2630016, // 30.44 days + "y": 31557600, // 365.25 days +} + +// parseTime parses UNIX timestamps or short timeranges inspired by sytemd (when starting with "-"), +// e.g. "-1M" for one month (30.44 days) ago. +func parseTime(s string, base time.Time) (uint64, error) { + if reTimeRange.MatchString(s) { + last := len(s) - 1 + + d, err := strconv.ParseInt(s[1:last], 10, 64) + if err != nil { + return uint64(0), err + } + + mul := secondsPer[string(s[last])] + return uint64(base.Unix() - d*mul), nil + } + + return strconv.ParseUint(s, 10, 64) +} diff --git a/cmd/lncli/arg_parse_test.go b/cmd/lncli/arg_parse_test.go new file mode 100644 index 00000000..9de6f895 --- /dev/null +++ b/cmd/lncli/arg_parse_test.go @@ -0,0 +1,88 @@ +package main + +import ( + "testing" + "time" +) + +var now = time.Date(2017, 11, 10, 7, 8, 9, 1234, time.UTC) + +var partTimeTests = []struct { + in string + expected uint64 + errExpected bool +}{ + { + "12345", + uint64(12345), + false, + }, + { + "-0s", + uint64(now.Unix()), + false, + }, + { + "-1s", + uint64(time.Date(2017, 11, 10, 7, 8, 8, 1234, time.UTC).Unix()), + false, + }, + { + "-2h", + uint64(time.Date(2017, 11, 10, 5, 8, 9, 1234, time.UTC).Unix()), + false, + }, + { + "-3d", + uint64(time.Date(2017, 11, 7, 7, 8, 9, 1234, time.UTC).Unix()), + false, + }, + { + "-4w", + uint64(time.Date(2017, 10, 13, 7, 8, 9, 1234, time.UTC).Unix()), + false, + }, + { + "-5M", + uint64(now.Unix() - 30.44*5*24*60*60), + false, + }, + { + "-6y", + uint64(now.Unix() - 365.25*6*24*60*60), + false, + }, + { + "-999999999999999999s", + uint64(now.Unix() - 999999999999999999), + false, + }, + { + "-9999999999999999991s", + 0, + true, + }, + { + "-7z", + 0, + true, + }, +} + +// Test that parsing absolute and relative times works. +func TestParseTime(t *testing.T) { + for _, test := range partTimeTests { + actual, err := parseTime(test.in, now) + if test.errExpected == (err == nil) { + t.Fatalf("unexpected error for %s:\n%v\n", test.in, err) + } + if actual != test.expected { + t.Fatalf( + "for %s actual and expected do not match:\n%d\n%d\n", + test.in, + actual, + test.expected, + ) + } + } +} diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 2dfda4b1..4559a132 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -2708,9 +2708,12 @@ var forwardingHistoryCommand = cli.Command{ Query the HTLC switch's internal forwarding log for all completed payment circuits (HTLCs) over a particular time range (--start_time and --end_time). The start and end times are meant to be expressed in - seconds since the Unix epoch. If --start_time isn't provided, - then 24 hours ago is used. If --end_time isn't provided, - then the current time is used. + seconds since the Unix epoch. + Alternatively negative time ranges can be used, e.g. "-3d". Supports + s(seconds), m(minutes), h(ours), d(ays), w(eeks), M(onths), y(ears). + Month equals 30.44 days, year equals 365.25 days. + If --start_time isn't provided, then 24 hours ago is used. If + --end_time isn't provided, then the current time is used. The max number of events returned is 50k. The default number is 100, callers can use the --max_events param to modify this value. @@ -2720,15 +2723,15 @@ var forwardingHistoryCommand = cli.Command{ entry. Using this callers can manually paginate within a time slice. `, Flags: []cli.Flag{ - cli.Int64Flag{ + cli.StringFlag{ Name: "start_time", - Usage: "the starting time for the query, expressed in " + - "seconds since the unix epoch", + Usage: "the starting time for the query " + + `as unix timestamp or relative e.g. "-1w"`, }, - cli.Int64Flag{ + cli.StringFlag{ Name: "end_time", - Usage: "the end time for the query, expressed in " + - "seconds since the unix epoch", + Usage: "the end time for the query " + + `as unix timestamp or relative e.g. "-1w"`, }, cli.Int64Flag{ Name: "index_offset", @@ -2753,30 +2756,33 @@ func forwardingHistory(ctx *cli.Context) error { err error ) args := ctx.Args() + now := time.Now() switch { case ctx.IsSet("start_time"): - startTime = ctx.Uint64("start_time") + startTime, err = parseTime(ctx.String("start_time"), now) case args.Present(): - startTime, err = strconv.ParseUint(args.First(), 10, 64) - if err != nil { - return fmt.Errorf("unable to decode start_time %v", err) - } + startTime, err = parseTime(args.First(), now) args = args.Tail() default: now := time.Now() startTime = uint64(now.Add(-time.Hour * 24).Unix()) } + if err != nil { + return fmt.Errorf("unable to decode start_time: %v", err) + } switch { case ctx.IsSet("end_time"): - endTime = ctx.Uint64("end_time") + endTime, err = parseTime(ctx.String("end_time"), now) case args.Present(): - endTime, err = strconv.ParseUint(args.First(), 10, 64) - if err != nil { - return fmt.Errorf("unable to decode end_time: %v", err) - } + endTime, err = parseTime(args.First(), now) args = args.Tail() + default: + endTime = uint64(now.Unix()) + } + if err != nil { + return fmt.Errorf("unable to decode end_time: %v", err) } switch {