diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index e7d636ebc109..93e53b3a416e 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -1758,14 +1758,99 @@ static const struct plugin_notification notifications[] = { }, }; +/* xpay doesn't have maxfeepercent or exemptfee, so we convert them to + * an absolute restriction here. If we can't, fail and let pay handle + * it. */ +static bool calc_maxfee(struct command *cmd, + const char **maxfeestr, + const char *buf, + const jsmntok_t *invstringtok, + const jsmntok_t *amount_msattok, + const jsmntok_t *exemptfeetok, + const jsmntok_t *maxfeepercenttok) +{ + u64 maxfeepercent_ppm; + struct amount_msat amount, maxfee, exemptfee; + + if (!exemptfeetok && !maxfeepercenttok) + return true; + + /* Can't have both */ + if (*maxfeestr) + return false; + + /* If they specify amount easy, otherwise take from invoice */ + if (amount_msattok) { + if (!parse_amount_msat(&amount, buf + amount_msattok->start, + amount_msattok->end - amount_msattok->start)) + return false; + } else { + const struct bolt11 *b11; + char *fail; + const char *invstr; + + /* We need to know total amount to calc fee */ + if (!invstringtok) + return false; + + invstr = json_strdup(tmpctx, buf, invstringtok); + b11 = bolt11_decode(tmpctx, invstr, NULL, NULL, NULL, &fail); + if (b11 != NULL) { + if (b11->msat == NULL) + return false; + amount = *b11->msat; + } else { + const struct tlv_invoice *b12; + b12 = invoice_decode(tmpctx, invstr, strlen(invstr), + NULL, NULL, &fail); + if (b12 == NULL || b12->invoice_amount == NULL) + return false; + amount = amount_msat(*b12->invoice_amount); + } + } + + if (maxfeepercenttok) { + if (!json_to_millionths(buf, + maxfeepercenttok, + &maxfeepercent_ppm)) + return false; + } else + maxfeepercent_ppm = 500000; + + if (!amount_msat_fee(&maxfee, amount, 0, maxfeepercent_ppm / 100)) + return false; + + if (exemptfeetok) { + if (!parse_amount_msat(&exemptfee, buf + exemptfeetok->start, + exemptfeetok->end - exemptfeetok->start)) + return false; + } else + exemptfee = AMOUNT_MSAT(5000); + + if (amount_msat_less(maxfee, exemptfee)) + maxfee = exemptfee; + + *maxfeestr = fmt_amount_msat(cmd, maxfee); + plugin_log(cmd->plugin, LOG_DBG, + "Converted maxfeepercent=%.*s, exemptfee=%.*s to maxfee %s", + maxfeepercenttok ? json_tok_full_len(maxfeepercenttok) : 5, + maxfeepercenttok ? json_tok_full(buf, maxfeepercenttok) : "UNSET", + exemptfeetok ? json_tok_full_len(exemptfeetok) : 5, + exemptfeetok ? json_tok_full(buf, exemptfeetok) : "UNSET", + *maxfeestr); + + return true; +} + static struct command_result *handle_rpc_command(struct command *cmd, const char *buf, const jsmntok_t *params) { struct xpay *xpay = xpay_of(cmd->plugin); const jsmntok_t *rpc_tok, *method_tok, *params_tok, *id_tok, - *bolt11 = NULL, *amount_msat = NULL, *maxfee = NULL, + *bolt11 = NULL, *amount_msat = NULL, *partial_msat = NULL, *retry_for = NULL; + const char *maxfee = NULL; struct json_stream *response; if (!xpay->take_over_pay) @@ -1795,7 +1880,7 @@ static struct command_result *handle_rpc_command(struct command *cmd, if (params_tok->size == 2) amount_msat = json_next(bolt11); } else if (params_tok->type == JSMN_OBJECT) { - const jsmntok_t *t; + const jsmntok_t *t, *maxfeepercent = NULL, *exemptfee = NULL; size_t i; json_for_each_obj(i, t, params_tok) { @@ -1806,9 +1891,13 @@ static struct command_result *handle_rpc_command(struct command *cmd, else if (json_tok_streq(buf, t, "retry_for")) retry_for = t + 1; else if (json_tok_streq(buf, t, "maxfee")) - maxfee = t + 1; + maxfee = json_strdup(cmd, buf, t + 1); else if (json_tok_streq(buf, t, "partial_msat")) partial_msat = t + 1; + else if (json_tok_streq(buf, t, "maxfeepercent")) + maxfeepercent = t + 1; + else if (json_tok_streq(buf, t, "exemptfee")) + exemptfee = t + 1; else { plugin_log(cmd->plugin, LOG_INFORM, "Not redirecting pay (unknown arg %.*s)", @@ -1822,6 +1911,14 @@ static struct command_result *handle_rpc_command(struct command *cmd, "Not redirecting pay (missing bolt11 parameter)"); goto dont_redirect; } + /* If this returns NULL, we let pay handle the weird case */ + if (!calc_maxfee(cmd, &maxfee, buf, + bolt11, amount_msat, + exemptfee, maxfeepercent)) { + plugin_log(cmd->plugin, LOG_INFORM, + "Not redirecting pay (weird maxfee params)"); + goto dont_redirect; + } } else { plugin_log(cmd->plugin, LOG_INFORM, "Not redirecting pay (unexpected params type)"); @@ -1840,8 +1937,10 @@ static struct command_result *handle_rpc_command(struct command *cmd, json_add_tok(response, "amount_msat", amount_msat, buf); if (retry_for) json_add_tok(response, "retry_for", retry_for, buf); + /* Even if this was a number token, handing it as a string is + * allowed by parse_msat */ if (maxfee) - json_add_tok(response, "maxfee", maxfee, buf); + json_add_string(response, "maxfee", maxfee); if (partial_msat) json_add_tok(response, "partial_msat", partial_msat, buf); json_object_end(response); diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 8b25d498bb9a..ec56798fcee6 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -436,18 +436,10 @@ def test_xpay_takeover(node_factory, executor): l1.rpc.pay(inv['bolt11'], amount_msat=10000, riskfactor=1) l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"riskfactor\\"\)') - inv = l3.rpc.invoice('any', "test_xpay_takeover9", "test_xpay_takeover9") - l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxfeepercent=1) - l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxfeepercent\\"\)') - inv = l3.rpc.invoice('any', "test_xpay_takeover10", "test_xpay_takeover10") l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxdelay=200) l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxdelay\\"\)') - inv = l3.rpc.invoice('any', "test_xpay_takeover11", "test_xpay_takeover11") - l1.rpc.pay(inv['bolt11'], amount_msat=10000, exemptfee=1) - l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"exemptfee\\"\)') - # Test that it's really dynamic. l1.rpc.setconfig('xpay-handle-pay', False) @@ -473,6 +465,16 @@ def test_xpay_takeover(node_factory, executor): assert ret['amount_msat'] == 100000 assert ret['amount_sent_msat'] > 100000 + # Test maxfeepercent. + inv = l3.rpc.invoice(100000, "test_xpay_takeover14", "test_xpay_takeover14")['bolt11'] + with pytest.raises(RpcError, match=r"Could not find route without excessive cost"): + l1.rpc.pay(bolt11=inv, maxfeepercent=0.001, exemptfee=0) + l1.daemon.wait_for_log('plugin-cln-xpay: Converted maxfeepercent=0.001, exemptfee=0 to maxfee 1msat') + + # Exemptfee default more than covers it. + l1.rpc.pay(bolt11=inv, maxfeepercent=0.25) + l1.daemon.wait_for_log('Converted maxfeepercent=0.25, exemptfee=UNSET to maxfee 5000msat') + def test_xpay_preapprove(node_factory): l1, l2 = node_factory.line_graph(2, opts={'dev-hsmd-fail-preapprove': None})