diff --git a/common/bolt12.c b/common/bolt12.c index a0c8c9f08..291b4ccb5 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -594,7 +594,7 @@ static bool bolt12_has_request_prefix(const char *str) return strstarts(str, "lnr1") || strstarts(str, "LNR1"); } -static bool bolt12_has_offer_prefix(const char *str) +bool bolt12_has_offer_prefix(const char *str) { return strstarts(str, "lno1") || strstarts(str, "LNO1"); } diff --git a/common/bolt12.h b/common/bolt12.h index 0ad354649..29effd552 100644 --- a/common/bolt12.h +++ b/common/bolt12.h @@ -125,6 +125,7 @@ void offer_period_paywindow(const struct recurrence *recurrence, /** * Preliminary prefix check to see if the string might be a bolt12 string. */ +bool bolt12_has_offer_prefix(const char *str); bool bolt12_has_prefix(const char *str); /** diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 47140c4f0..6844c033a 100644 --- a/contrib/msggen/msggen/schema.json +++ b/contrib/msggen/msggen/schema.json @@ -35529,7 +35529,7 @@ "invstring": { "type": "string", "description": [ - "bolt11 or bolt12 invoice" + "bolt11 or bolt12 invoice, or a bolt12 (non-recursive) offer. If it's an offer, the invoice is fetch using `fetchinvoice` automatically." ] }, "amount_msat": { diff --git a/doc/schemas/xpay.json b/doc/schemas/xpay.json index b962ade29..414e9a2c7 100644 --- a/doc/schemas/xpay.json +++ b/doc/schemas/xpay.json @@ -17,7 +17,7 @@ "invstring": { "type": "string", "description": [ - "bolt11 or bolt12 invoice" + "bolt11 or bolt12 invoice, or a bolt12 (non-recursive) offer. If it's an offer, the invoice is fetch using `fetchinvoice` automatically." ] }, "amount_msat": { diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index ec9116522..589229bcc 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -77,9 +77,9 @@ struct payment { /* Maximum fee we're prepare to pay */ struct amount_msat maxfee; /* Maximum delay on the route we're ok with */ - u32 *maxdelay; + u32 maxdelay; /* Maximum number of payment routes that can be pending. */ - u32 *maxparts; + u32 maxparts; /* Do we have to do it all in a single part? */ bool disable_mpp; /* BOLT11 payment secret (NULL for BOLT12, it uses blinded paths) */ @@ -163,6 +163,18 @@ struct attempt { const struct preimage *preimage; }; +/* Recursion */ +static struct command_result *xpay_core(struct command *cmd, + const char *invstring TAKES, + const struct amount_msat *msat, + const struct amount_msat *maxfee, + const char **layers, + u32 retryfor, + const struct amount_msat *partial, + u32 maxdelay, + u32 dev_maxparts, + bool as_pay); + /* Wrapper for pending commands (ignores return) */ static void was_pending(const struct command_result *res) { @@ -1367,10 +1379,10 @@ static struct command_result *getroutes_for(struct command *aux_cmd, json_array_end(req->js); json_add_amount_msat(req->js, "maxfee_msat", maxfee); json_add_u32(req->js, "final_cltv", payment->final_cltv); - json_add_u32(req->js, "maxdelay", *payment->maxdelay); + json_add_u32(req->js, "maxdelay", payment->maxdelay); count_pending = count_current_attempts(payment); - assert(*payment->maxparts > count_pending); - json_add_u32(req->js, "maxparts", *payment->maxparts - count_pending); + assert(payment->maxparts > count_pending); + json_add_u32(req->js, "maxparts", payment->maxparts - count_pending); return send_payment_req(aux_cmd, payment, req); } @@ -1640,34 +1652,132 @@ preapproveinvoice_succeed(struct command *cmd, return populate_private_layer(cmd, payment); } -static struct command_result *json_xpay_core(struct command *cmd, - const char *buffer, - const jsmntok_t *params, - bool as_pay) +struct xpay_params { + struct amount_msat *maxfee, *partial; + const char **layers; + unsigned int retryfor; + u32 maxdelay, dev_maxparts; +}; + +static struct command_result * +invoice_fetched(struct command *cmd, + const char *method, + const char *buf, + const jsmntok_t *result, + struct xpay_params *params) +{ + char *inv; + + inv = json_strdup(NULL, buf, json_get_member(buf, result, "invoice")); + return xpay_core(cmd, take(to_canonical_invstr(NULL, take(inv))), + NULL, params->maxfee, params->layers, + params->retryfor, params->partial, params->maxdelay, + params->dev_maxparts, false); +} + +static struct command_result *json_xpay_params(struct command *cmd, + const char *buffer, + const jsmntok_t *params, + bool as_pay) { - struct xpay *xpay = xpay_of(cmd->plugin); struct amount_msat *msat, *maxfee, *partial; - struct payment *payment = tal(cmd, struct payment); + const char *invstring; + const char **layers; + u32 *maxdelay, *maxparts; unsigned int *retryfor; struct out_req *req; - u64 now, invexpiry; char *err; if (!param_check(cmd, buffer, params, - p_req("invstring", param_invstring, &payment->invstring), + p_req("invstring", param_invstring, &invstring), p_opt("amount_msat", param_msat, &msat), p_opt("maxfee", param_msat, &maxfee), - p_opt("layers", param_string_array, &payment->layers), + p_opt("layers", param_string_array, &layers), p_opt_def("retry_for", param_number, &retryfor, 60), p_opt("partial_msat", param_msat, &partial), - p_opt_def("maxdelay", param_u32, &payment->maxdelay, 2016), - p_opt_dev("dev_maxparts", param_u32, &payment->maxparts, 100), + p_opt_def("maxdelay", param_u32, &maxdelay, 2016), + p_opt_dev("dev_maxparts", param_u32, &maxparts, 100), NULL)) return command_param_failed(); - if (*payment->maxparts == 0) + + if (*maxparts == 0) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "maxparts cannot be zero"); + /* 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"); + + if (command_check_only(cmd)) + return command_check_done(cmd); + + xparams = tal(cmd, struct xpay_params); + xparams->maxfee = maxfee; + xparams->partial = partial; + xparams->layers = layers; + xparams->retryfor = *retryfor; + xparams->maxdelay = *maxdelay; + xparams->dev_maxparts = *maxparts; + + req = jsonrpc_request_start(cmd, "fetchinvoice", + invoice_fetched, + forward_error, xparams); + json_add_string(req->js, "offer", invstring); + if (msat) + json_add_amount_msat(req->js, "amount_msat", *msat); + return send_outreq(req); + } + + return xpay_core(cmd, invstring, + msat, maxfee, layers, *retryfor, partial, *maxdelay, *maxparts, + as_pay); +} + +static struct command_result *xpay_core(struct command *cmd, + const char *invstring TAKES, + const struct amount_msat *msat, + const struct amount_msat *maxfee, + const char **layers, + u32 retryfor, + const struct amount_msat *partial, + u32 maxdelay, + u32 dev_maxparts, + bool as_pay) +{ + struct payment *payment = tal(cmd, struct payment); + struct xpay *xpay = xpay_of(cmd->plugin); + u64 now, invexpiry; + struct out_req *req; + char *err; + list_head_init(&payment->current_attempts); list_head_init(&payment->past_attempts); payment->plugin = cmd->plugin; @@ -1677,9 +1787,16 @@ static struct command_result *json_xpay_core(struct command *cmd, payment->total_num_attempts = payment->num_failures = 0; payment->requests = tal_arr(payment, struct out_req *, 0); payment->prior_results = tal_strdup(payment, ""); - payment->deadline = timemono_add(time_mono(), time_from_sec(*retryfor)); + payment->deadline = timemono_add(time_mono(), time_from_sec(retryfor)); payment->start_time = time_now(); payment->pay_compat = as_pay; + payment->invstring = tal_strdup(payment, invstring); + if (layers) + payment->layers = tal_dup_talarr(payment, const char *, layers); + else + payment->layers = NULL; + payment->maxdelay = maxdelay; + payment->maxparts = dev_maxparts; if (bolt12_has_prefix(payment->invstring)) { struct gossmap *gossmap = get_gossmap(xpay); @@ -1827,14 +1944,14 @@ static struct command_result *json_xpay(struct command *cmd, const char *buffer, const jsmntok_t *params) { - return json_xpay_core(cmd, buffer, params, false); + return json_xpay_params(cmd, buffer, params, false); } static struct command_result *json_xpay_as_pay(struct command *cmd, const char *buffer, const jsmntok_t *params) { - return json_xpay_core(cmd, buffer, params, true); + return json_xpay_params(cmd, buffer, params, true); } static struct command_result *getchaininfo_done(struct command *aux_cmd, diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 8539fdd20..822a26ff1 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -978,3 +978,21 @@ def test_attempt_notifications(node_factory): 'error_code': 4103, 'error_message': 'temporary_channel_failure'}} assert data == expected + + +def test_xpay_offer(node_factory): + l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) + + offer1 = l3.rpc.offer('any')['bolt12'] + offer2 = l3.rpc.offer('5sat', "5sat donation")['bolt12'] + + with pytest.raises(RpcError, match=r"Must specify amount for this offer"): + l1.rpc.xpay(offer1) + + l1.rpc.xpay(offer1, 100) + + with pytest.raises(RpcError, match=r"Offer amount is 5000msat, you tried to pay 1000msat"): + l1.rpc.xpay(offer2, 1000) + + l1.rpc.xpay(offer2) + l1.rpc.xpay(offer2, 5000)