mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-20 13:54:36 +01:00
decode: support decoding runes.
This is a bit weird since it lives in the offers plugin, but it works well. This should make runes much more approachable for people! Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
468dff1723
commit
8c48eda8c7
5 changed files with 354 additions and 9 deletions
|
@ -120,6 +120,56 @@ time in seconds:
|
|||
"unique_id": "3"
|
||||
}
|
||||
|
||||
You can also use lightning-decode(7) to examine runes you have been given:
|
||||
|
||||
$ .lightning-cli decode tU-RLjMiDpY2U0o3W1oFowar36RFGpWloPbW9-RuZdo9MyZpZD0wMjRiOWExZmE4ZTAwNmYxZTM5MzdmNjVmNjZjNDA4ZTZkYThlMWNhNzI4ZWE0MzIyMmE3MzgxZGYxY2M0NDk2MDUmbWV0aG9kPWxpc3RwZWVycyZwbnVtPTEmcG5hbWVpZF4wMjRiOWExZmE4ZTAwNmYxZTM5M3xwYXJyMF4wMjRiOWExZmE4ZTAwNmYxZTM5MyZ0aW1lPDE2NTY5MjA1MzgmcmF0ZT0y
|
||||
{
|
||||
"type": "rune",
|
||||
"unique_id": "3",
|
||||
"string": "b54f912e33220e9636534a375b5a05a306abdfa4451a95a5a0f6d6f7e46e65da:=3&id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605&method=listpeers&pnum=1&pnameid^024b9a1fa8e006f1e393|parr0^024b9a1fa8e006f1e393&time<1656920538&rate=2",
|
||||
"restrictions": [
|
||||
{
|
||||
"alternatives": [
|
||||
"id=024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605"
|
||||
],
|
||||
"summary": "id (of commanding peer) equal to '024b9a1fa8e006f1e3937f65f66c408e6da8e1ca728ea43222a7381df1cc449605'"
|
||||
},
|
||||
{
|
||||
"alternatives": [
|
||||
"method=listpeers"
|
||||
],
|
||||
"summary": "method (of command) equal to 'listpeers'"
|
||||
},
|
||||
{
|
||||
"alternatives": [
|
||||
"pnum=1"
|
||||
],
|
||||
"summary": "pnum (number of command parameters) equal to 1"
|
||||
},
|
||||
{
|
||||
"alternatives": [
|
||||
"pnameid^024b9a1fa8e006f1e393",
|
||||
"parr0^024b9a1fa8e006f1e393"
|
||||
],
|
||||
"summary": "pnameid (object parameter 'id') starts with '024b9a1fa8e006f1e393' OR parr0 (array parameter #0) starts with '024b9a1fa8e006f1e393'"
|
||||
},
|
||||
{
|
||||
"alternatives": [
|
||||
"time<1656920538"
|
||||
],
|
||||
"summary": "time (in seconds since 1970) less than 1656920538 (approximately 19 hours 18 minutes from now)"
|
||||
},
|
||||
{
|
||||
"alternatives": [
|
||||
"rate=2"
|
||||
],
|
||||
"summary": "rate (max per minute) equal to 2"
|
||||
}
|
||||
],
|
||||
"valid": true
|
||||
}
|
||||
|
||||
|
||||
SHARING RUNES
|
||||
-------------
|
||||
|
||||
|
@ -157,7 +207,7 @@ excuses his previous adoption of the name "Eltoo".
|
|||
SEE ALSO
|
||||
--------
|
||||
|
||||
lightning-commando(7)
|
||||
lightning-commando(7), lightning-decode(7)
|
||||
|
||||
RESOURCES
|
||||
---------
|
||||
|
|
|
@ -9,17 +9,21 @@ SYNOPSIS
|
|||
DESCRIPTION
|
||||
-----------
|
||||
|
||||
The **decode** RPC command checks and parses a *bolt11* or *bolt12*
|
||||
string (optionally prefixed by `lightning:` or `LIGHTNING:`) as
|
||||
specified by the BOLT 11 and BOLT 12 specifications. It may decode
|
||||
other formats in future.
|
||||
The **decode** RPC command checks and parses:
|
||||
|
||||
- a *bolt11* or *bolt12* string (optionally prefixed by `lightning:`
|
||||
or `LIGHTNING:`) as specified by the BOLT 11 and BOLT 12
|
||||
specifications.
|
||||
- a *rune* as created by lightning-commando-rune(7).
|
||||
|
||||
It may decode other formats in future.
|
||||
|
||||
RETURN VALUE
|
||||
------------
|
||||
|
||||
[comment]: # (GENERATE-FROM-SCHEMA-START)
|
||||
On success, an object is returned, containing:
|
||||
- **type** (string): what kind of object it decoded to (one of "bolt12 offer", "bolt12 invoice", "bolt12 invoice_request", "bolt11 invoice")
|
||||
- **type** (string): what kind of object it decoded to (one of "bolt12 offer", "bolt12 invoice", "bolt12 invoice_request", "bolt11 invoice", "rune")
|
||||
- **valid** (boolean): if this is false, you *MUST* not use the result except for diagnostics!
|
||||
|
||||
If **type** is "bolt12 offer", and **valid** is *true*:
|
||||
|
@ -159,6 +163,16 @@ If **type** is "bolt11 invoice", and **valid** is *true*:
|
|||
- **tag** (string): The bech32 letter which identifies this field (always 1 characters)
|
||||
- **data** (string): The bech32 data for this field
|
||||
|
||||
If **type** is "rune":
|
||||
- **string** (string): the string encoding of the rune
|
||||
- **restrictions** (array of objects): restrictions built into the rune: all must pass:
|
||||
- **alternatives** (array of strings): each way restriction can be met: any can pass:
|
||||
- the alternative of form fieldname condition fieldname
|
||||
- **summary** (string): human-readable summary of this restriction
|
||||
- **unique_id** (string, optional): unique id (always a numeric id on runes we create)
|
||||
- **version** (string, optional): rune version, not currently set on runes we create
|
||||
- **valid** (boolean, optional) (always *true*)
|
||||
|
||||
[comment]: # (GENERATE-FROM-SCHEMA-END)
|
||||
|
||||
AUTHOR
|
||||
|
@ -169,7 +183,7 @@ Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible.
|
|||
SEE ALSO
|
||||
--------
|
||||
|
||||
lightning-pay(7), lightning-offer(7), lightning-offerout(7), lightning-fetchinvoice(7), lightning-sendinvoice(7)
|
||||
lightning-pay(7), lightning-offer(7), lightning-offerout(7), lightning-fetchinvoice(7), lightning-sendinvoice(7), lightning-commando-rune(7)
|
||||
|
||||
[BOLT \#11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md).
|
||||
|
||||
|
@ -181,4 +195,4 @@ RESOURCES
|
|||
|
||||
Main web site: <https://github.com/ElementsProject/lightning>
|
||||
|
||||
[comment]: # ( SHA256STAMP:bc3778965137591623ce08ff51adf411bc42e6d1a4200692961b69962da39be7)
|
||||
[comment]: # ( SHA256STAMP:d1e1f044c2e67ec169728dbc551903c97f9a9daa1f42e9d2f1686fc692d25be8)
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
"bolt12 offer",
|
||||
"bolt12 invoice",
|
||||
"bolt12 invoice_request",
|
||||
"bolt11 invoice"
|
||||
"bolt11 invoice",
|
||||
"rune"
|
||||
],
|
||||
"description": "what kind of object it decoded to"
|
||||
},
|
||||
|
@ -909,6 +910,72 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"rune"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"string",
|
||||
"restrictions"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"unique_id": {
|
||||
"type": "string",
|
||||
"description": "unique id (always a numeric id on runes we create)"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "rune version, not currently set on runes we create"
|
||||
},
|
||||
"valid": {
|
||||
"type": "boolean",
|
||||
"enum": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"type": {},
|
||||
"string": {
|
||||
"type": "string",
|
||||
"description": "the string encoding of the rune"
|
||||
},
|
||||
"restrictions": {
|
||||
"type": "array",
|
||||
"description": "restrictions built into the rune: all must pass",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"alternatives",
|
||||
"summary"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"alternatives": {
|
||||
"type": "array",
|
||||
"description": "each way restriction can be met: any can pass",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "the alternative of form fieldname condition fieldname"
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "human-readable summary of this restriction"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
161
plugins/offers.c
161
plugins/offers.c
|
@ -3,6 +3,7 @@
|
|||
#include <bitcoin/chainparams.h>
|
||||
#include <ccan/array_size/array_size.h>
|
||||
#include <ccan/cast/cast.h>
|
||||
#include <ccan/rune/rune.h>
|
||||
#include <ccan/tal/str/str.h>
|
||||
#include <common/bech32.h>
|
||||
#include <common/bolt11.h>
|
||||
|
@ -204,6 +205,7 @@ struct decodable {
|
|||
struct tlv_offer *offer;
|
||||
struct tlv_invoice *invoice;
|
||||
struct tlv_invoice_request *invreq;
|
||||
struct rune *rune;
|
||||
};
|
||||
|
||||
static struct command_result *param_decodable(struct command *cmd,
|
||||
|
@ -271,6 +273,13 @@ static struct command_result *param_decodable(struct command *cmd,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
decodable->rune = rune_from_base64n(decodable, buffer + tok.start,
|
||||
tok.end - tok.start);
|
||||
if (decodable->rune) {
|
||||
decodable->type = "rune";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Return failure message from most likely parsing candidate */
|
||||
return command_fail_badparam(cmd, name, buffer, &tok, likely_fail);
|
||||
}
|
||||
|
@ -808,6 +817,156 @@ static void json_add_invoice_request(struct json_stream *js,
|
|||
json_add_bool(js, "valid", valid);
|
||||
}
|
||||
|
||||
static void json_add_rune(struct command *cmd, struct json_stream *js, const struct rune *rune)
|
||||
{
|
||||
if (rune->unique_id)
|
||||
json_add_string(js, "unique_id", rune->unique_id);
|
||||
if (rune->version)
|
||||
json_add_string(js, "version", rune->version);
|
||||
json_add_string(js, "string", take(rune_to_string(NULL, rune)));
|
||||
|
||||
json_array_start(js, "restrictions");
|
||||
for (size_t i = rune->unique_id ? 1 : 0; i < tal_count(rune->restrs); i++) {
|
||||
const struct rune_restr *restr = rune->restrs[i];
|
||||
char *summary = tal_strdup(tmpctx, "");
|
||||
const char *sep = "";
|
||||
|
||||
json_object_start(js, NULL);
|
||||
json_array_start(js, "alternatives");
|
||||
for (size_t j = 0; j < tal_count(restr->alterns); j++) {
|
||||
const struct rune_altern *alt = restr->alterns[j];
|
||||
const char *annotation, *value;
|
||||
bool int_val = false, time_val = false;
|
||||
|
||||
if (streq(alt->fieldname, "time")) {
|
||||
annotation = "in seconds since 1970";
|
||||
time_val = true;
|
||||
} else if (streq(alt->fieldname, "id"))
|
||||
annotation = "of commanding peer";
|
||||
else if (streq(alt->fieldname, "method"))
|
||||
annotation = "of command";
|
||||
else if (streq(alt->fieldname, "pnum")) {
|
||||
annotation = "number of command parameters";
|
||||
int_val = true;
|
||||
} else if (streq(alt->fieldname, "rate")) {
|
||||
annotation = "max per minute";
|
||||
int_val = true;
|
||||
} else if (strstarts(alt->fieldname, "parr")) {
|
||||
annotation = tal_fmt(tmpctx, "array parameter #%s", alt->fieldname+4);
|
||||
} else if (strstarts(alt->fieldname, "pname"))
|
||||
annotation = tal_fmt(tmpctx, "object parameter '%s'", alt->fieldname+5);
|
||||
else
|
||||
annotation = "unknown condition?";
|
||||
|
||||
tal_append_fmt(&summary, "%s", sep);
|
||||
|
||||
/* Where it's ambiguous, quote if it's not treated as an int */
|
||||
if (int_val)
|
||||
value = alt->value;
|
||||
else if (time_val) {
|
||||
u64 t = atol(alt->value);
|
||||
|
||||
if (t) {
|
||||
u64 diff, now = time_now().ts.tv_sec;
|
||||
/* Need a non-const during construction */
|
||||
char *v;
|
||||
|
||||
if (now > t)
|
||||
diff = now - t;
|
||||
else
|
||||
diff = t - now;
|
||||
if (diff < 60)
|
||||
v = tal_fmt(tmpctx, "%"PRIu64" seconds", diff);
|
||||
else if (diff < 60 * 60)
|
||||
v = tal_fmt(tmpctx, "%"PRIu64" minutes %"PRIu64" seconds",
|
||||
diff / 60, diff % 60);
|
||||
else {
|
||||
v = tal_strdup(tmpctx, "approximately ");
|
||||
/* diff is in minutes */
|
||||
diff /= 60;
|
||||
if (diff < 48 * 60)
|
||||
tal_append_fmt(&v, "%"PRIu64" hours %"PRIu64" minutes",
|
||||
diff / 60, diff % 60);
|
||||
else {
|
||||
/* hours */
|
||||
diff /= 60;
|
||||
if (diff < 60 * 24)
|
||||
tal_append_fmt(&v, "%"PRIu64" days %"PRIu64" hours",
|
||||
diff / 24, diff % 24);
|
||||
else {
|
||||
/* days */
|
||||
diff /= 24;
|
||||
if (diff < 365 * 2)
|
||||
tal_append_fmt(&v, "%"PRIu64" months %"PRIu64" days",
|
||||
diff / 30, diff % 30);
|
||||
else {
|
||||
/* months */
|
||||
diff /= 30;
|
||||
tal_append_fmt(&v, "%"PRIu64" years %"PRIu64" months",
|
||||
diff / 12, diff % 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (now > t)
|
||||
tal_append_fmt(&v, " ago");
|
||||
else
|
||||
tal_append_fmt(&v, " from now");
|
||||
value = tal_fmt(tmpctx, "%s (%s)", alt->value, v);
|
||||
} else
|
||||
value = alt->value;
|
||||
} else
|
||||
value = tal_fmt(tmpctx, "'%s'", alt->value);
|
||||
|
||||
switch (alt->condition) {
|
||||
case RUNE_COND_IF_MISSING:
|
||||
tal_append_fmt(&summary, "%s (%s) is missing", alt->fieldname, annotation);
|
||||
break;
|
||||
case RUNE_COND_EQUAL:
|
||||
tal_append_fmt(&summary, "%s (%s) equal to %s", alt->fieldname, annotation, value);
|
||||
break;
|
||||
case RUNE_COND_NOT_EQUAL:
|
||||
tal_append_fmt(&summary, "%s (%s) unequal to %s", alt->fieldname, annotation, value);
|
||||
break;
|
||||
case RUNE_COND_BEGINS:
|
||||
tal_append_fmt(&summary, "%s (%s) starts with '%s'", alt->fieldname, annotation, alt->value);
|
||||
break;
|
||||
case RUNE_COND_ENDS:
|
||||
tal_append_fmt(&summary, "%s (%s) ends with '%s'", alt->fieldname, annotation, alt->value);
|
||||
break;
|
||||
case RUNE_COND_CONTAINS:
|
||||
tal_append_fmt(&summary, "%s (%s) contains '%s'", alt->fieldname, annotation, alt->value);
|
||||
break;
|
||||
case RUNE_COND_INT_LESS:
|
||||
tal_append_fmt(&summary, "%s (%s) less than %s", alt->fieldname, annotation,
|
||||
time_val ? value : alt->value);
|
||||
break;
|
||||
case RUNE_COND_INT_GREATER:
|
||||
tal_append_fmt(&summary, "%s (%s) greater than %s", alt->fieldname, annotation,
|
||||
time_val ? value : alt->value);
|
||||
break;
|
||||
case RUNE_COND_LEXO_BEFORE:
|
||||
tal_append_fmt(&summary, "%s (%s) sorts before '%s'", alt->fieldname, annotation, alt->value);
|
||||
break;
|
||||
case RUNE_COND_LEXO_AFTER:
|
||||
tal_append_fmt(&summary, "%s (%s) sorts after '%s'", alt->fieldname, annotation, alt->value);
|
||||
break;
|
||||
case RUNE_COND_COMMENT:
|
||||
tal_append_fmt(&summary, "[comment: %s%s]", alt->fieldname, alt->value);
|
||||
break;
|
||||
}
|
||||
sep = " OR ";
|
||||
json_add_str_fmt(js, NULL, "%s%c%s", alt->fieldname, alt->condition, alt->value);
|
||||
}
|
||||
json_array_end(js);
|
||||
json_add_string(js, "summary", summary);
|
||||
json_object_end(js);
|
||||
}
|
||||
json_array_end(js);
|
||||
/* FIXME: do some sanity checks? */
|
||||
json_add_bool(js, "valid", true);
|
||||
}
|
||||
|
||||
static struct command_result *json_decode(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params)
|
||||
|
@ -833,6 +992,8 @@ static struct command_result *json_decode(struct command *cmd,
|
|||
json_add_bolt11(response, decodable->b11);
|
||||
json_add_bool(response, "valid", true);
|
||||
}
|
||||
if (decodable->rune)
|
||||
json_add_rune(cmd, response, decodable->rune);
|
||||
return command_finished(cmd, response);
|
||||
}
|
||||
|
||||
|
|
|
@ -2673,6 +2673,59 @@ def test_commando_rune(node_factory):
|
|||
assert rune9['rune'] == 'O8Zr-ULTBKO3_pKYz0QKE9xYl1vQ4Xx9PtlHuist9Rk9NCZwbnVtPTAmcmF0ZT0zJnJhdGU9MQ=='
|
||||
assert rune9['unique_id'] == '4'
|
||||
|
||||
runedecodes = ((rune1, []),
|
||||
(rune2, [{'alternatives': ['method^list', 'method^get', 'method=summary'],
|
||||
'summary': "method (of command) starts with 'list' OR method (of command) starts with 'get' OR method (of command) equal to 'summary'"},
|
||||
{'alternatives': ['method/listdatastore'],
|
||||
'summary': "method (of command) unequal to 'listdatastore'"}]),
|
||||
(rune4, [{'alternatives': ['id^022d223620a359a47ff7'],
|
||||
'summary': "id (of commanding peer) starts with '022d223620a359a47ff7'"},
|
||||
{'alternatives': ['method=listpeers'],
|
||||
'summary': "method (of command) equal to 'listpeers'"}]),
|
||||
(rune5, [{'alternatives': ['id^022d223620a359a47ff7'],
|
||||
'summary': "id (of commanding peer) starts with '022d223620a359a47ff7'"},
|
||||
{'alternatives': ['method=listpeers'],
|
||||
'summary': "method (of command) equal to 'listpeers'"},
|
||||
{'alternatives': ['pnamelevel!', 'pnamelevel/io'],
|
||||
'summary': "pnamelevel (object parameter 'level') is missing OR pnamelevel (object parameter 'level') unequal to 'io'"}]),
|
||||
(rune6, [{'alternatives': ['id^022d223620a359a47ff7'],
|
||||
'summary': "id (of commanding peer) starts with '022d223620a359a47ff7'"},
|
||||
{'alternatives': ['method=listpeers'],
|
||||
'summary': "method (of command) equal to 'listpeers'"},
|
||||
{'alternatives': ['pnamelevel!', 'pnamelevel/io'],
|
||||
'summary': "pnamelevel (object parameter 'level') is missing OR pnamelevel (object parameter 'level') unequal to 'io'"},
|
||||
{'alternatives': ['parr1!', 'parr1/io'],
|
||||
'summary': "parr1 (array parameter #1) is missing OR parr1 (array parameter #1) unequal to 'io'"}]),
|
||||
(rune7, [{'alternatives': ['pnum=0'],
|
||||
'summary': "pnum (number of command parameters) equal to 0"}]),
|
||||
(rune8, [{'alternatives': ['pnum=0'],
|
||||
'summary': "pnum (number of command parameters) equal to 0"},
|
||||
{'alternatives': ['rate=3'],
|
||||
'summary': "rate (max per minute) equal to 3"}]),
|
||||
(rune9, [{'alternatives': ['pnum=0'],
|
||||
'summary': "pnum (number of command parameters) equal to 0"},
|
||||
{'alternatives': ['rate=3'],
|
||||
'summary': "rate (max per minute) equal to 3"},
|
||||
{'alternatives': ['rate=1'],
|
||||
'summary': "rate (max per minute) equal to 1"}]))
|
||||
for decode in runedecodes:
|
||||
rune = decode[0]
|
||||
restrictions = decode[1]
|
||||
decoded = l1.rpc.decode(rune['rune'])
|
||||
assert decoded['type'] == 'rune'
|
||||
assert decoded['unique_id'] == rune['unique_id']
|
||||
assert decoded['valid'] is True
|
||||
assert decoded['restrictions'] == restrictions
|
||||
|
||||
# Time handling is a bit special, since we annotate the timestamp with how far away it is.
|
||||
decoded = l1.rpc.decode(rune3['rune'])
|
||||
assert decoded['type'] == 'rune'
|
||||
assert decoded['unique_id'] == rune3['unique_id']
|
||||
assert decoded['valid'] is True
|
||||
assert len(decoded['restrictions']) == 1
|
||||
assert decoded['restrictions'][0]['alternatives'] == ['time>1656675211']
|
||||
assert decoded['restrictions'][0]['summary'].startswith("time (in seconds since 1970) greater than 1656675211 (")
|
||||
|
||||
# Replace rune3 with a more useful timestamp!
|
||||
expiry = int(time.time()) + 15
|
||||
rune3 = l1.rpc.commando_rune(restrictions="time<{}".format(expiry))
|
||||
|
|
Loading…
Add table
Reference in a new issue