xpay: emulate maxfeepercent and exemptfee when xpay-handle-pay used

maxfeepercent is use by Zeus, so let's make that work.

maxfee is more precise, so it's the only xpay option (maxfee was added
to pay later).

[ Fix to ppm logic by Lagrang3, thanks! --RR ]

Fixes: https://github.com/ElementsProject/lightning/issues/7926
Changelog-Changed: JSON-RPC: With `xpay-handle-pay` set, xpay will now be used even if `pay` uses maxfeeprecent or exemptfee parameters (e.g. Zeus)
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2024-12-16 13:03:14 +10:30
parent cf22762c8f
commit 428c76068c
2 changed files with 113 additions and 12 deletions

View file

@ -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);

View file

@ -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})