xpay: add option to pay bip353.
Changelog-Added: JSON-RPC: `xpay` can now directly pay a BIP353 address, like `₿rusty@rustcorp.com.au`. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
@@ -35529,7 +35529,7 @@
|
||||
"invstring": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"bolt11 or bolt12 invoice, or a bolt12 (non-recursive) offer. If it's an offer, the invoice is fetch using `fetchinvoice` automatically."
|
||||
"bolt11 or bolt12 invoice, a bolt12 (non-recursive) offer or a BIP353 name. If it's a bip353 name, an offer is fetched with `fetchbip353` if available. If it's an offer, the invoice is fetched using `fetchinvoice` automatically."
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"invstring": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"bolt11 or bolt12 invoice, or a bolt12 (non-recursive) offer. If it's an offer, the invoice is fetch using `fetchinvoice` automatically."
|
||||
"bolt11 or bolt12 invoice, a bolt12 (non-recursive) offer or a BIP353 name. If it's a bip353 name, an offer is fetched with `fetchbip353` if available. If it's an offer, the invoice is fetched using `fetchinvoice` automatically."
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
|
||||
@@ -1652,11 +1652,49 @@ preapproveinvoice_succeed(struct command *cmd,
|
||||
return populate_private_layer(cmd, payment);
|
||||
}
|
||||
|
||||
static struct command_result *check_offer_payable(struct command *cmd,
|
||||
const char *offerstr,
|
||||
const struct amount_msat *msat)
|
||||
{
|
||||
char *err;
|
||||
struct tlv_offer *b12offer = offer_decode(tmpctx,
|
||||
offerstr,
|
||||
strlen(offerstr),
|
||||
plugin_feature_set(cmd->plugin),
|
||||
chainparams, &err);
|
||||
if (!b12offer)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Invalid bolt12 offer: %s", err);
|
||||
/* We will only one-shot if we know amount! (FIXME: Convert!) */
|
||||
if (b12offer->offer_currency)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Cannot pay offer in different currency %s",
|
||||
b12offer->offer_currency);
|
||||
if (b12offer->offer_amount) {
|
||||
if (msat && !amount_msat_eq(amount_msat(*b12offer->offer_amount), *msat)) {
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Offer amount is %s, you tried to pay %s",
|
||||
fmt_amount_msat(tmpctx, amount_msat(*b12offer->offer_amount)),
|
||||
fmt_amount_msat(tmpctx, *msat));
|
||||
}
|
||||
} else {
|
||||
if (!msat)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Must specify amount for this offer");
|
||||
}
|
||||
if (b12offer->offer_recurrence)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Cannot xpay recurring offers");
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct xpay_params {
|
||||
struct amount_msat *maxfee, *partial;
|
||||
struct amount_msat *msat, *maxfee, *partial;
|
||||
const char **layers;
|
||||
unsigned int retryfor;
|
||||
u32 maxdelay, dev_maxparts;
|
||||
const char *bip353;
|
||||
};
|
||||
|
||||
static struct command_result *
|
||||
@@ -1675,6 +1713,56 @@ invoice_fetched(struct command *cmd,
|
||||
params->dev_maxparts, false);
|
||||
}
|
||||
|
||||
static struct command_result *
|
||||
do_fetchinvoice(struct command *cmd, const char *offerstr, struct xpay_params *xparams)
|
||||
{
|
||||
struct out_req *req;
|
||||
|
||||
req = jsonrpc_request_start(cmd, "fetchinvoice",
|
||||
invoice_fetched,
|
||||
forward_error,
|
||||
xparams);
|
||||
json_add_string(req->js, "offer", offerstr);
|
||||
if (xparams->msat)
|
||||
json_add_amount_msat(req->js, "amount_msat", *xparams->msat);
|
||||
if (xparams->bip353)
|
||||
json_add_string(req->js, "bip353", xparams->bip353);
|
||||
return send_outreq(req);
|
||||
}
|
||||
|
||||
static struct command_result *
|
||||
bip353_fetched(struct command *cmd,
|
||||
const char *method,
|
||||
const char *buf,
|
||||
const jsmntok_t *result,
|
||||
struct xpay_params *xparams)
|
||||
{
|
||||
const jsmntok_t *instructions, *t, *offertok;
|
||||
const char *offerstr;
|
||||
struct command_result *ret;
|
||||
size_t i;
|
||||
|
||||
instructions = json_get_member(buf, result, "instructions");
|
||||
json_for_each_arr(i, t, instructions) {
|
||||
offertok = json_get_member(buf, t, "offer");
|
||||
if (offertok)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!offertok)
|
||||
return command_fail(cmd, PAY_UNSPECIFIED_ERROR,
|
||||
"BIP353 response did not contain an offer (%.*s)",
|
||||
json_tok_full_len(result),
|
||||
json_tok_full(buf, result));
|
||||
offerstr = json_strdup(tmpctx, buf, offertok);
|
||||
|
||||
ret = check_offer_payable(cmd, offerstr, xparams->msat);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
return do_fetchinvoice(cmd, offerstr, xparams);
|
||||
}
|
||||
|
||||
static struct command_result *json_xpay_params(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params,
|
||||
@@ -1686,7 +1774,7 @@ static struct command_result *json_xpay_params(struct command *cmd,
|
||||
u32 *maxdelay, *maxparts;
|
||||
unsigned int *retryfor;
|
||||
struct out_req *req;
|
||||
char *err;
|
||||
struct xpay_params *xparams;
|
||||
|
||||
if (!param_check(cmd, buffer, params,
|
||||
p_req("invstring", param_invstring, &invstring),
|
||||
@@ -1706,53 +1794,44 @@ static struct command_result *json_xpay_params(struct command *cmd,
|
||||
|
||||
/* Is this a one-shot vibe payment? Kids these days! */
|
||||
if (!as_pay && bolt12_has_offer_prefix(invstring)) {
|
||||
struct xpay_params *xparams;
|
||||
struct tlv_offer *b12offer = offer_decode(tmpctx,
|
||||
invstring,
|
||||
strlen(invstring),
|
||||
plugin_feature_set(cmd->plugin),
|
||||
chainparams, &err);
|
||||
if (!b12offer)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Invalid bolt12 offer: %s", err);
|
||||
/* We will only one-shot if we know amount! (FIXME: Convert!) */
|
||||
if (b12offer->offer_currency)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Cannot pay offer in different currency %s",
|
||||
b12offer->offer_currency);
|
||||
if (b12offer->offer_amount) {
|
||||
if (msat && !amount_msat_eq(amount_msat(*b12offer->offer_amount), *msat)) {
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Offer amount is %s, you tried to pay %s",
|
||||
fmt_amount_msat(tmpctx, amount_msat(*b12offer->offer_amount)),
|
||||
fmt_amount_msat(tmpctx, *msat));
|
||||
}
|
||||
} else {
|
||||
if (!msat)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Must specify amount for this offer");
|
||||
}
|
||||
if (b12offer->offer_recurrence)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Cannot xpay recurring offers");
|
||||
struct command_result *ret;
|
||||
|
||||
ret = check_offer_payable(cmd, invstring, msat);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
if (command_check_only(cmd))
|
||||
return command_check_done(cmd);
|
||||
|
||||
xparams = tal(cmd, struct xpay_params);
|
||||
xparams->msat = msat;
|
||||
xparams->maxfee = maxfee;
|
||||
xparams->partial = partial;
|
||||
xparams->layers = layers;
|
||||
xparams->retryfor = *retryfor;
|
||||
xparams->maxdelay = *maxdelay;
|
||||
xparams->dev_maxparts = *maxparts;
|
||||
xparams->bip353 = NULL;
|
||||
|
||||
req = jsonrpc_request_start(cmd, "fetchinvoice",
|
||||
invoice_fetched,
|
||||
return do_fetchinvoice(cmd, invstring, xparams);
|
||||
}
|
||||
|
||||
/* BIP353? */
|
||||
if (!as_pay && strchr(invstring, '@')) {
|
||||
xparams = tal(cmd, struct xpay_params);
|
||||
xparams->msat = msat;
|
||||
xparams->maxfee = maxfee;
|
||||
xparams->partial = partial;
|
||||
xparams->layers = layers;
|
||||
xparams->retryfor = *retryfor;
|
||||
xparams->maxdelay = *maxdelay;
|
||||
xparams->dev_maxparts = *maxparts;
|
||||
xparams->bip353 = invstring;
|
||||
|
||||
req = jsonrpc_request_start(cmd, "fetchbip353",
|
||||
bip353_fetched,
|
||||
forward_error, xparams);
|
||||
json_add_string(req->js, "offer", invstring);
|
||||
if (msat)
|
||||
json_add_amount_msat(req->js, "amount_msat", *msat);
|
||||
json_add_string(req->js, "address", invstring);
|
||||
return send_outreq(req);
|
||||
}
|
||||
|
||||
|
||||
21
tests/plugins/fakebip353.py
Executable file
21
tests/plugins/fakebip353.py
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
This imitates fetchbip353 for testing.
|
||||
"""
|
||||
from pyln.client import Plugin
|
||||
|
||||
plugin = Plugin()
|
||||
|
||||
|
||||
@plugin.method("fetchbip353")
|
||||
def fetchbip353(plugin, address, **kwargs):
|
||||
return {'instructions': [{'offer': plugin.options['bip353offer']['value']}]}
|
||||
|
||||
|
||||
plugin.add_option(
|
||||
'bip353offer',
|
||||
None,
|
||||
"Fake offer to return"
|
||||
)
|
||||
|
||||
plugin.run()
|
||||
@@ -996,3 +996,17 @@ def test_xpay_offer(node_factory):
|
||||
|
||||
l1.rpc.xpay(offer2)
|
||||
l1.rpc.xpay(offer2, 5000)
|
||||
|
||||
|
||||
def test_xpay_bip353(node_factory):
|
||||
fakebip353_plugin = Path(__file__).parent / "plugins" / "fakebip353.py"
|
||||
|
||||
l1 = node_factory.get_node()
|
||||
offer = l1.rpc.offer('any')['bolt12']
|
||||
|
||||
l2 = node_factory.get_node(options={'disable-plugin': 'cln-bip353',
|
||||
'plugin': fakebip353_plugin,
|
||||
'bip353offer': offer})
|
||||
|
||||
node_factory.join_nodes([l2, l1])
|
||||
l2.rpc.xpay('fake@fake.com', 100)
|
||||
|
||||
Reference in New Issue
Block a user