diff --git a/CHANGELOG.md b/CHANGELOG.md index 939d91cf8..3ff137b72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- JSON API: `invoice` expiry defaults to 7 days. +- JSON API: `invoice` expiry defaults to 7 days, and can have s/m/h/d/w suffixes. ### Deprecated diff --git a/doc/lightning-invoice.7 b/doc/lightning-invoice.7 index 02f0beb31..14e2fb9b0 100644 --- a/doc/lightning-invoice.7 +++ b/doc/lightning-invoice.7 @@ -42,7 +42,7 @@ The \fIlabel\fR must be a unique string or number (which is treated as a string, .sp The \fIdescription\fR is a short description of purpose of payment, e\&.g\&. \fI1 cup of coffee\fR\&. This value is encoded into the BOLT11 invoice and is viewable by any node you send this invoice to\&. It must be UTF\-8, and cannot use \fI\eu\fR JSON escape codes\&. .sp -The \fIexpiry\fR is optionally the number of seconds the invoice is valid for\&. If no value is provided the default of 604800 (1 week) is used\&. +The \fIexpiry\fR is optionally the time the invoice is valid for; without a suffix it is interpreted as seconds, otherwise suffixes \fIs\fR, \fIm\fR, \fIh\fR, \fId\fR, \fIw\fR indicate seconds, minutes, hours, days and weeks respectively\&. If no value is provided the default of 604800 (1w) is used\&. .sp The \fIfallbacks\fR array is one or more fallback addresses to include in the invoice (in order from most\-preferred to least): note that these arrays are not currently tracked to fulfill the invoice\&. .sp diff --git a/doc/lightning-invoice.7.txt b/doc/lightning-invoice.7.txt index 0ea947d38..6cf1696c5 100644 --- a/doc/lightning-invoice.7.txt +++ b/doc/lightning-invoice.7.txt @@ -34,8 +34,11 @@ e.g. '1 cup of coffee'. This value is encoded into the BOLT11 invoice and is viewable by any node you send this invoice to. It must be UTF-8, and cannot use '\u' JSON escape codes. -The 'expiry' is optionally the number of seconds the invoice is valid for. -If no value is provided the default of 604800 (1 week) is used. +The 'expiry' is optionally the time the invoice is valid for; without +a suffix it is interpreted as seconds, otherwise suffixes 's', 'm', +'h', 'd', 'w' indicate seconds, minutes, hours, days and weeks +respectively. If no value is provided the default of 604800 (1w) +is used. The 'fallbacks' array is one or more fallback addresses to include in the invoice (in order from most-preferred to least): note that these diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 80e294a29..345ebcf66 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -389,6 +391,56 @@ static struct command_result *param_msat_or_any(struct command *cmd, buffer + tok->start); } +/* Parse time with optional suffix, return seconds */ +static struct command_result *param_time(struct command *cmd, const char *name, + const char *buffer, + const jsmntok_t *tok, + uint64_t **secs) +{ + /* We need to manipulate this, so make copy */ + jsmntok_t timetok = *tok; + u64 mul; + char s; + struct { + char suffix; + u64 mul; + } suffixes[] = { + { 's', 1 }, + { 'm', 60 }, + { 'h', 60*60 }, + { 'd', 24*60*60 }, + { 'w', 7*24*60*60 } }; + + mul = 1; + if (timetok.end == timetok.start) + s = '\0'; + else + s = buffer[timetok.end - 1]; + for (size_t i = 0; i < ARRAY_SIZE(suffixes); i++) { + if (s == suffixes[i].suffix) { + mul = suffixes[i].mul; + timetok.end--; + break; + } + } + + *secs = tal(cmd, uint64_t); + if (json_to_u64(buffer, &timetok, *secs)) { + if (mul_overflows_u64(**secs, mul)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "'%s' string '%.*s' is too large", + name, tok->end - tok->start, + buffer + tok->start); + } + **secs *= mul; + return NULL; + } + + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "'%s' should be a number with optional {s,m,h,d,w} suffix, not '%.*s'", + name, tok->end - tok->start, buffer + tok->start); +} + static struct command_result *json_invoice(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -415,7 +467,7 @@ static struct command_result *json_invoice(struct command *cmd, p_req("msatoshi", param_msat_or_any, &msatoshi_val), p_req("label", param_label, &info->label), p_req("description", param_escaped_string, &desc_val), - p_opt_def("expiry", param_u64, &expiry, 3600*24*7), + p_opt_def("expiry", param_time, &expiry, 3600*24*7), p_opt("fallbacks", param_array, &fallbacks), p_opt("preimage", param_tok, &preimagetok), p_opt("exposeprivatechannels", param_bool, &exposeprivate), diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 7e9904356..666c11334 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -297,6 +297,37 @@ def test_invoice_expiry(node_factory, executor): # all invoices are expired and should be deleted assert len(l2.rpc.listinvoices()['invoices']) == 0 + # Test expiry suffixes. + start = int(time.time()) + inv = l2.rpc.invoice(msatoshi=123000, label='inv_s', description='description', expiry='1s')['bolt11'] + end = int(time.time()) + expiry = only_one(l2.rpc.listinvoices('inv_s')['invoices'])['expires_at'] + assert expiry >= start + 1 and expiry <= end + 1 + + start = int(time.time()) + inv = l2.rpc.invoice(msatoshi=123000, label='inv_m', description='description', expiry='1m')['bolt11'] + end = int(time.time()) + expiry = only_one(l2.rpc.listinvoices('inv_m')['invoices'])['expires_at'] + assert expiry >= start + 60 and expiry <= end + 60 + + start = int(time.time()) + inv = l2.rpc.invoice(msatoshi=123000, label='inv_h', description='description', expiry='1h')['bolt11'] + end = int(time.time()) + expiry = only_one(l2.rpc.listinvoices('inv_h')['invoices'])['expires_at'] + assert expiry >= start + 3600 and expiry <= end + 3600 + + start = int(time.time()) + inv = l2.rpc.invoice(msatoshi=123000, label='inv_d', description='description', expiry='1d')['bolt11'] + end = int(time.time()) + expiry = only_one(l2.rpc.listinvoices('inv_d')['invoices'])['expires_at'] + assert expiry >= start + 24 * 3600 and expiry <= end + 24 * 3600 + + start = int(time.time()) + inv = l2.rpc.invoice(msatoshi=123000, label='inv_w', description='description', expiry='1w')['bolt11'] + end = int(time.time()) + expiry = only_one(l2.rpc.listinvoices('inv_w')['invoices'])['expires_at'] + assert expiry >= start + 7 * 24 * 3600 and expiry <= end + 7 * 24 * 3600 + @unittest.skipIf(not DEVELOPER, "Too slow without --dev-bitcoind-poll") def test_waitinvoice(node_factory, executor):