Skip to content

Commit 1dcb4fa

Browse files
committed
plugins: cancelrecurringinvoice command.
`fetchinvoice` variant, for setting invreq_recurrence_cancel instead. Signed-off-by: Rusty Russell <[email protected]> Changelog-EXPERIMENTAL: `cancelrecurringinvoice` command to send new "don't expect any more invoice requests" msg to recurring bolt12 invoices.
1 parent 0463e3b commit 1dcb4fa

File tree

7 files changed

+342
-1
lines changed

7 files changed

+342
-1
lines changed

contrib/msggen/msggen/schema.json

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4828,6 +4828,87 @@
48284828
}
48294829
]
48304830
},
4831+
"cancelrecurringinvoice.json": {
4832+
"$schema": "../rpc-schema-draft.json",
4833+
"type": "object",
4834+
"added": "v25.09",
4835+
"rpc": "cancelrecurringinvoice",
4836+
"title": "Command for sending a cancel message for a recurring offer",
4837+
"description": [
4838+
"NOTE: Recurring offers are experimental, and may be changed in backwards-incompable ways.",
4839+
"",
4840+
"The **cancelrecurringinvoice** RPC command sends a cancellation message in place of an invoice_request. The BOLT 12 specification suggests sending this as a courtesy in place of the next invoice_request (as would be sent by fetchinvoice)."
4841+
],
4842+
"request": {
4843+
"required": [
4844+
"offer",
4845+
"recurrence_counter",
4846+
"recurrence_label"
4847+
],
4848+
"additionalProperties": false,
4849+
"properties": {
4850+
"offer": {
4851+
"type": "string",
4852+
"description": [
4853+
"Offer string (must be recurring) which we have been paying."
4854+
]
4855+
},
4856+
"recurrence_counter": {
4857+
"type": "u64",
4858+
"description": [
4859+
"One later than the last-specified recurrence_counter for the last invoice."
4860+
]
4861+
},
4862+
"recurrence_label": {
4863+
"type": "string",
4864+
"description": [
4865+
"This must be the same as prior fetchinvoice calls for the same recurrence, as it is used to link them together."
4866+
]
4867+
},
4868+
"recurrence_start": {
4869+
"type": "number",
4870+
"description": [
4871+
"Indicates what period number to start at (usually 0). This will be the same as previous fetchinvoice calls."
4872+
]
4873+
},
4874+
"payer_note": {
4875+
"type": "string",
4876+
"description": [
4877+
"To tell the issuer the reason for the cancellation."
4878+
]
4879+
},
4880+
"bip353": {
4881+
"type": "string",
4882+
"description": [
4883+
"BIP353 string (optionally with \u20bf) indicating where we fetched the offer from"
4884+
]
4885+
}
4886+
}
4887+
},
4888+
"response": {
4889+
"required": [
4890+
"bolt12"
4891+
],
4892+
"additionalProperties": false,
4893+
"properties": {
4894+
"bolt12": {
4895+
"type": "string",
4896+
"description": [
4897+
"The invoice_request we sent to the issuer."
4898+
]
4899+
}
4900+
}
4901+
},
4902+
"author": [
4903+
"Rusty Russell <<[email protected]>> is mainly responsible."
4904+
],
4905+
"see_also": [
4906+
"lightning-fetchinvoice(7)"
4907+
],
4908+
"resources": [
4909+
"Main web site: <https://github.com/ElementsProject/lightning>"
4910+
]
4911+
},
48314912
"check.json": {
48324913
"$schema": "../rpc-schema-draft.json",
48334914
"type": "object",

doc/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ MARKDOWNPAGES := doc/addgossip.7 \
3030
doc/bkpr-listbalances.7 \
3131
doc/bkpr-listincome.7 \
3232
doc/blacklistrune.7 \
33+
doc/cancelrecurringinvoice.7 \
3334
doc/check.7 \
3435
doc/checkmessage.7 \
3536
doc/checkrune.7 \

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Core Lightning Documentation
3838
bkpr-listbalances <bkpr-listbalances.7.md>
3939
bkpr-listincome <bkpr-listincome.7.md>
4040
blacklistrune <blacklistrune.7.md>
41+
cancelrecurringinvoice <cancelrecurringinvoice.7.md>
4142
check <check.7.md>
4243
checkmessage <checkmessage.7.md>
4344
checkrune <checkrune.7.md>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"$schema": "../rpc-schema-draft.json",
3+
"type": "object",
4+
"added": "v25.09",
5+
"rpc": "cancelrecurringinvoice",
6+
"title": "Command for sending a cancel message for a recurring offer",
7+
"description": [
8+
"NOTE: Recurring offers are experimental, and may be changed in backwards-incompable ways.",
9+
"",
10+
"The **cancelrecurringinvoice** RPC command sends a cancellation message in place of an invoice_request. The BOLT 12 specification suggests sending this as a courtesy in place of the next invoice_request (as would be sent by fetchinvoice)."
11+
],
12+
"request": {
13+
"required": [
14+
"offer",
15+
"recurrence_counter",
16+
"recurrence_label"
17+
],
18+
"additionalProperties": false,
19+
"properties": {
20+
"offer": {
21+
"type": "string",
22+
"description": [
23+
"Offer string (must be recurring) which we have been paying."
24+
]
25+
},
26+
"recurrence_counter": {
27+
"type": "u64",
28+
"description": [
29+
"One later than the last-specified recurrence_counter for the last invoice."
30+
]
31+
},
32+
"recurrence_label": {
33+
"type": "string",
34+
"description": [
35+
"This must be the same as prior fetchinvoice calls for the same recurrence, as it is used to link them together."
36+
]
37+
},
38+
"recurrence_start": {
39+
"type": "number",
40+
"description": [
41+
"Indicates what period number to start at (usually 0). This will be the same as previous fetchinvoice calls."
42+
]
43+
},
44+
"payer_note": {
45+
"type": "string",
46+
"description": [
47+
"To tell the issuer the reason for the cancellation."
48+
]
49+
},
50+
"bip353": {
51+
"type": "string",
52+
"description": [
53+
"BIP353 string (optionally with ₿) indicating where we fetched the offer from"
54+
]
55+
}
56+
}
57+
},
58+
"response": {
59+
"required": [
60+
"bolt12"
61+
],
62+
"additionalProperties": false,
63+
"properties": {
64+
"bolt12": {
65+
"type": "string",
66+
"description": [
67+
"The invoice_request we sent to the issuer."
68+
]
69+
}
70+
}
71+
},
72+
"author": [
73+
"Rusty Russell <<[email protected]>> is mainly responsible."
74+
],
75+
"see_also": [
76+
"lightning-fetchinvoice(7)"
77+
],
78+
"resources": [
79+
"Main web site: <https://github.com/ElementsProject/lightning>"
80+
]
81+
}

plugins/fetchinvoice.c

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,23 @@ static struct command_result *timeout_sent_invreq(struct command *timer_cmd,
410410
return timer_complete(timer_cmd);
411411
}
412412

413+
static struct command_result *cancelrecurringinvoice_done(struct command *cmd,
414+
struct sent *sent)
415+
{
416+
return command_success(cmd,
417+
json_out_obj(cmd, "bolt12",
418+
invrequest_encode(tmpctx, sent->invreq)));
419+
}
420+
413421
static struct command_result *sendonionmsg_done(struct command *cmd,
414422
const char *method UNUSED,
415423
const char *buf UNUSED,
416424
const jsmntok_t *result UNUSED,
417425
struct sent *sent)
418426
{
427+
if (sent->invreq && sent->invreq->invreq_recurrence_cancel)
428+
return cancelrecurringinvoice_done(cmd, sent);
429+
419430
command_timer(cmd,
420431
time_from_sec(sent->wait_timeout),
421432
timeout_sent_invreq, sent);
@@ -768,7 +779,10 @@ static struct command_result *invreq_done(struct command *cmd,
768779
payload->invoice_request = tal_arr(payload, u8, 0);
769780
towire_tlv_invoice_request(&payload->invoice_request, sent->invreq);
770781

771-
return send_message(cmd, sent, true, payload, sendonionmsg_done);
782+
/* Don't expect a reply message for cancel */
783+
return send_message(cmd, sent,
784+
sent->invreq->invreq_recurrence_cancel ? false : true,
785+
payload, sendonionmsg_done);
772786
}
773787

774788
static struct command_result *param_dev_scidd(struct command *cmd, const char *name,
@@ -1137,6 +1151,161 @@ struct command_result *json_fetchinvoice(struct command *cmd,
11371151
return send_outreq(req);
11381152
}
11391153

1154+
struct command_result *json_cancelrecurringinvoice(struct command *cmd,
1155+
const char *buffer,
1156+
const jsmntok_t *params)
1157+
{
1158+
const char *rec_label, *payer_note;
1159+
struct out_req *req;
1160+
struct tlv_invoice_request *invreq;
1161+
struct sent *sent = tal(cmd, struct sent);
1162+
struct bip_353_name *bip353;
1163+
u32 *recurrence_counter, *recurrence_start;
1164+
1165+
if (!param_check(cmd, buffer, params,
1166+
p_req("offer", param_offer, &sent->offer),
1167+
p_req("recurrence_counter", param_number, &recurrence_counter),
1168+
p_req("recurrence_label", param_string, &rec_label),
1169+
p_opt("recurrence_start", param_number, &recurrence_start),
1170+
p_opt("payer_note", param_string, &payer_note),
1171+
p_opt("bip353", param_bip353, &bip353),
1172+
NULL))
1173+
return command_param_failed();
1174+
1175+
sent->their_paths = sent->offer->offer_paths;
1176+
if (sent->their_paths)
1177+
sent->direct_dest = NULL;
1178+
else
1179+
sent->direct_dest = sent->offer->offer_issuer_id;
1180+
/* This is NULL if offer_issuer_id is missing, and set by try_establish */
1181+
sent->issuer_key = sent->offer->offer_issuer_id;
1182+
1183+
/* BOLT #12:
1184+
* The writer:
1185+
* - if it is responding to an offer:
1186+
* - MUST copy all fields from the offer (including unknown fields).
1187+
*/
1188+
invreq = invoice_request_for_offer(sent, sent->offer);
1189+
invreq->invreq_recurrence_counter = tal_steal(invreq, recurrence_counter);
1190+
invreq->invreq_recurrence_start = tal_steal(invreq, recurrence_start);
1191+
invreq->invreq_bip_353_name = tal_steal(invreq, bip353);
1192+
invreq->invreq_recurrence_cancel = talz(invreq, struct tlv_invoice_request_invreq_recurrence_cancel);
1193+
1194+
/* BOLT-recurrence #12:
1195+
* - if it sets `invreq_recurrence_cancel`:
1196+
*...
1197+
* - MAY omit `invreq_amount` and `invreq_quantity`.
1198+
*/
1199+
/* And we do */
1200+
if (!invreq_recurrence(invreq))
1201+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1202+
"Not a recurring offer");
1203+
1204+
/* BOLT-recurrence #12:
1205+
* - if `offer_recurrence_optional` or `offer_recurrence_compulsory` are present:
1206+
* - for the initial request:
1207+
*...
1208+
* - MUST set `invreq_recurrence_counter` `counter` to 0.
1209+
* - MUST NOT set `invreq_recurrence_cancel`.
1210+
*/
1211+
if (*invreq->invreq_recurrence_counter == 0)
1212+
return command_fail_badparam(cmd, "recurrence_counter", buffer, params,
1213+
"Must be non-zero");
1214+
1215+
/* BOLT-recurrence #12:
1216+
* - if `offer_recurrence_base` is present:
1217+
* - MUST include `invreq_recurrence_start`
1218+
*...
1219+
* - otherwise:
1220+
* - MUST NOT include `invreq_recurrence_start`
1221+
*/
1222+
if (invreq->offer_recurrence_base) {
1223+
if (!invreq->invreq_recurrence_start)
1224+
invreq->invreq_recurrence_start = talz(invreq, u32);
1225+
} else {
1226+
if (invreq->invreq_recurrence_start)
1227+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1228+
"unnecessary recurrence_start");
1229+
}
1230+
1231+
invreq->invreq_metadata
1232+
= recurrence_invreq_metadata(invreq, invreq, rec_label);
1233+
1234+
/* We derive transient payer_id from invreq_metadata */
1235+
invreq->invreq_payer_id = tal(invreq, struct pubkey);
1236+
if (!payer_key(invreq->invreq_metadata,
1237+
tal_bytelen(invreq->invreq_metadata),
1238+
invreq->invreq_payer_id)) {
1239+
/* Doesn't happen! */
1240+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1241+
"Invalid tweak for payer_id");
1242+
}
1243+
1244+
/* BOLT-recurrence #12:
1245+
* - if `offer_recurrence_base` is present:
1246+
* - MUST include `invreq_recurrence_start`
1247+
*...
1248+
* - otherwise:
1249+
* - MUST NOT include `invreq_recurrence_start`
1250+
*/
1251+
if (invreq->offer_recurrence_base) {
1252+
if (!invreq->invreq_recurrence_start)
1253+
invreq->invreq_recurrence_start = talz(invreq, u32);
1254+
} else {
1255+
if (invreq->invreq_recurrence_start)
1256+
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
1257+
"unnecessary recurrence_start");
1258+
}
1259+
1260+
/* If only checking, we're done now */
1261+
if (command_check_only(cmd))
1262+
return command_check_done(cmd);
1263+
1264+
/* BOLT #12:
1265+
*
1266+
* - if `offer_chains` is set:
1267+
* - MUST set `invreq_chain` to one of `offer_chains` unless that
1268+
* chain is bitcoin, in which case it SHOULD omit `invreq_chain`.
1269+
* - otherwise:
1270+
* - if it sets `invreq_chain` it MUST set it to bitcoin.
1271+
*/
1272+
/* We already checked that we're compatible chain, in param_offer */
1273+
if (!streq(chainparams->network_name, "bitcoin")) {
1274+
invreq->invreq_chain = tal_dup(invreq, struct bitcoin_blkid,
1275+
&chainparams->genesis_blockhash);
1276+
}
1277+
1278+
/* BOLT #12:
1279+
* - if it supports bolt12 invoice request features:
1280+
* - MUST set `invreq_features`.`features` to the bitmap of features.
1281+
*/
1282+
invreq->invreq_features
1283+
= plugin_feature_set(cmd->plugin)->bits[BOLT12_OFFER_FEATURE];
1284+
1285+
/* invreq->invreq_payer_note is not a nul-terminated string! */
1286+
if (payer_note)
1287+
invreq->invreq_payer_note = tal_dup_arr(invreq, utf8,
1288+
payer_note,
1289+
strlen(payer_note),
1290+
0);
1291+
1292+
/* If only checking, we're done now */
1293+
if (command_check_only(cmd))
1294+
return command_check_done(cmd);
1295+
1296+
/* Make the invoice request (fills in payer_key and payer_info) */
1297+
req = jsonrpc_request_start(cmd, "createinvoicerequest",
1298+
&invreq_done,
1299+
&forward_error,
1300+
sent);
1301+
1302+
/* We don't want this is the database: that's only for ones we publish */
1303+
json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq));
1304+
json_add_bool(req->js, "savetodb", false);
1305+
json_add_string(req->js, "recurrence_label", rec_label);
1306+
return send_outreq(req);
1307+
}
1308+
11401309
/* FIXME: Using a hook here is not ideal: technically it doesn't mean
11411310
* it's actually hit the db! But using waitinvoice is also suboptimal
11421311
* because we don't have libplugin infra to cancel a pending req (and I

plugins/fetchinvoice.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ struct command_result *json_fetchinvoice(struct command *cmd,
99
const char *buffer,
1010
const jsmntok_t *params);
1111

12+
struct command_result *json_cancelrecurringinvoice(struct command *cmd,
13+
const char *buffer,
14+
const jsmntok_t *params);
15+
1216
struct command_result *json_sendinvoice(struct command *cmd,
1317
const char *buffer,
1418
const jsmntok_t *params);

0 commit comments

Comments
 (0)