Skip to content

Commit 2c12f67

Browse files
committed
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: #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 <[email protected]>
1 parent 737b2c6 commit 2c12f67

File tree

2 files changed

+113
-12
lines changed

2 files changed

+113
-12
lines changed

plugins/xpay/xpay.c

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,14 +1758,99 @@ static const struct plugin_notification notifications[] = {
17581758
},
17591759
};
17601760

1761+
/* xpay doesn't have maxfeepercent or exemptfee, so we convert them to
1762+
* an absolute restriction here. If we can't, fail and let pay handle
1763+
* it. */
1764+
static bool calc_maxfee(struct command *cmd,
1765+
const char **maxfeestr,
1766+
const char *buf,
1767+
const jsmntok_t *invstringtok,
1768+
const jsmntok_t *amount_msattok,
1769+
const jsmntok_t *exemptfeetok,
1770+
const jsmntok_t *maxfeepercenttok)
1771+
{
1772+
u64 maxfeepercent_ppm;
1773+
struct amount_msat amount, maxfee, exemptfee;
1774+
1775+
if (!exemptfeetok && !maxfeepercenttok)
1776+
return true;
1777+
1778+
/* Can't have both */
1779+
if (*maxfeestr)
1780+
return false;
1781+
1782+
/* If they specify amount easy, otherwise take from invoice */
1783+
if (amount_msattok) {
1784+
if (!parse_amount_msat(&amount, buf + amount_msattok->start,
1785+
amount_msattok->end - amount_msattok->start))
1786+
return false;
1787+
} else {
1788+
const struct bolt11 *b11;
1789+
char *fail;
1790+
const char *invstr;
1791+
1792+
/* We need to know total amount to calc fee */
1793+
if (!invstringtok)
1794+
return false;
1795+
1796+
invstr = json_strdup(tmpctx, buf, invstringtok);
1797+
b11 = bolt11_decode(tmpctx, invstr, NULL, NULL, NULL, &fail);
1798+
if (b11 != NULL) {
1799+
if (b11->msat == NULL)
1800+
return false;
1801+
amount = *b11->msat;
1802+
} else {
1803+
const struct tlv_invoice *b12;
1804+
b12 = invoice_decode(tmpctx, invstr, strlen(invstr),
1805+
NULL, NULL, &fail);
1806+
if (b12 == NULL || b12->invoice_amount == NULL)
1807+
return false;
1808+
amount = amount_msat(*b12->invoice_amount);
1809+
}
1810+
}
1811+
1812+
if (maxfeepercenttok) {
1813+
if (!json_to_millionths(buf,
1814+
maxfeepercenttok,
1815+
&maxfeepercent_ppm))
1816+
return false;
1817+
} else
1818+
maxfeepercent_ppm = 500000;
1819+
1820+
if (!amount_msat_fee(&maxfee, amount, 0, maxfeepercent_ppm / 100))
1821+
return false;
1822+
1823+
if (exemptfeetok) {
1824+
if (!parse_amount_msat(&exemptfee, buf + exemptfeetok->start,
1825+
exemptfeetok->end - exemptfeetok->start))
1826+
return false;
1827+
} else
1828+
exemptfee = AMOUNT_MSAT(5000);
1829+
1830+
if (amount_msat_less(maxfee, exemptfee))
1831+
maxfee = exemptfee;
1832+
1833+
*maxfeestr = fmt_amount_msat(cmd, maxfee);
1834+
plugin_log(cmd->plugin, LOG_DBG,
1835+
"Converted maxfeepercent=%.*s, exemptfee=%.*s to maxfee %s",
1836+
maxfeepercenttok ? json_tok_full_len(maxfeepercenttok) : 5,
1837+
maxfeepercenttok ? json_tok_full(buf, maxfeepercenttok) : "UNSET",
1838+
exemptfeetok ? json_tok_full_len(exemptfeetok) : 5,
1839+
exemptfeetok ? json_tok_full(buf, exemptfeetok) : "UNSET",
1840+
*maxfeestr);
1841+
1842+
return true;
1843+
}
1844+
17611845
static struct command_result *handle_rpc_command(struct command *cmd,
17621846
const char *buf,
17631847
const jsmntok_t *params)
17641848
{
17651849
struct xpay *xpay = xpay_of(cmd->plugin);
17661850
const jsmntok_t *rpc_tok, *method_tok, *params_tok, *id_tok,
1767-
*bolt11 = NULL, *amount_msat = NULL, *maxfee = NULL,
1851+
*bolt11 = NULL, *amount_msat = NULL,
17681852
*partial_msat = NULL, *retry_for = NULL;
1853+
const char *maxfee = NULL;
17691854
struct json_stream *response;
17701855

17711856
if (!xpay->take_over_pay)
@@ -1795,7 +1880,7 @@ static struct command_result *handle_rpc_command(struct command *cmd,
17951880
if (params_tok->size == 2)
17961881
amount_msat = json_next(bolt11);
17971882
} else if (params_tok->type == JSMN_OBJECT) {
1798-
const jsmntok_t *t;
1883+
const jsmntok_t *t, *maxfeepercent = NULL, *exemptfee = NULL;
17991884
size_t i;
18001885

18011886
json_for_each_obj(i, t, params_tok) {
@@ -1806,9 +1891,13 @@ static struct command_result *handle_rpc_command(struct command *cmd,
18061891
else if (json_tok_streq(buf, t, "retry_for"))
18071892
retry_for = t + 1;
18081893
else if (json_tok_streq(buf, t, "maxfee"))
1809-
maxfee = t + 1;
1894+
maxfee = json_strdup(cmd, buf, t + 1);
18101895
else if (json_tok_streq(buf, t, "partial_msat"))
18111896
partial_msat = t + 1;
1897+
else if (json_tok_streq(buf, t, "maxfeepercent"))
1898+
maxfeepercent = t + 1;
1899+
else if (json_tok_streq(buf, t, "exemptfee"))
1900+
exemptfee = t + 1;
18121901
else {
18131902
plugin_log(cmd->plugin, LOG_INFORM,
18141903
"Not redirecting pay (unknown arg %.*s)",
@@ -1822,6 +1911,14 @@ static struct command_result *handle_rpc_command(struct command *cmd,
18221911
"Not redirecting pay (missing bolt11 parameter)");
18231912
goto dont_redirect;
18241913
}
1914+
/* If this returns NULL, we let pay handle the weird case */
1915+
if (!calc_maxfee(cmd, &maxfee, buf,
1916+
bolt11, amount_msat,
1917+
exemptfee, maxfeepercent)) {
1918+
plugin_log(cmd->plugin, LOG_INFORM,
1919+
"Not redirecting pay (weird maxfee params)");
1920+
goto dont_redirect;
1921+
}
18251922
} else {
18261923
plugin_log(cmd->plugin, LOG_INFORM,
18271924
"Not redirecting pay (unexpected params type)");
@@ -1840,8 +1937,10 @@ static struct command_result *handle_rpc_command(struct command *cmd,
18401937
json_add_tok(response, "amount_msat", amount_msat, buf);
18411938
if (retry_for)
18421939
json_add_tok(response, "retry_for", retry_for, buf);
1940+
/* Even if this was a number token, handing it as a string is
1941+
* allowed by parse_msat */
18431942
if (maxfee)
1844-
json_add_tok(response, "maxfee", maxfee, buf);
1943+
json_add_string(response, "maxfee", maxfee);
18451944
if (partial_msat)
18461945
json_add_tok(response, "partial_msat", partial_msat, buf);
18471946
json_object_end(response);

tests/test_xpay.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -436,18 +436,10 @@ def test_xpay_takeover(node_factory, executor):
436436
l1.rpc.pay(inv['bolt11'], amount_msat=10000, riskfactor=1)
437437
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"riskfactor\\"\)')
438438

439-
inv = l3.rpc.invoice('any', "test_xpay_takeover9", "test_xpay_takeover9")
440-
l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxfeepercent=1)
441-
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxfeepercent\\"\)')
442-
443439
inv = l3.rpc.invoice('any', "test_xpay_takeover10", "test_xpay_takeover10")
444440
l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxdelay=200)
445441
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxdelay\\"\)')
446442

447-
inv = l3.rpc.invoice('any', "test_xpay_takeover11", "test_xpay_takeover11")
448-
l1.rpc.pay(inv['bolt11'], amount_msat=10000, exemptfee=1)
449-
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"exemptfee\\"\)')
450-
451443
# Test that it's really dynamic.
452444
l1.rpc.setconfig('xpay-handle-pay', False)
453445

@@ -473,6 +465,16 @@ def test_xpay_takeover(node_factory, executor):
473465
assert ret['amount_msat'] == 100000
474466
assert ret['amount_sent_msat'] > 100000
475467

468+
# Test maxfeepercent.
469+
inv = l3.rpc.invoice(100000, "test_xpay_takeover14", "test_xpay_takeover14")['bolt11']
470+
with pytest.raises(RpcError, match=r"Could not find route without excessive cost"):
471+
l1.rpc.pay(bolt11=inv, maxfeepercent=0.001, exemptfee=0)
472+
l1.daemon.wait_for_log('plugin-cln-xpay: Converted maxfeepercent=0.001, exemptfee=0 to maxfee 1msat')
473+
474+
# Exemptfee default more than covers it.
475+
l1.rpc.pay(bolt11=inv, maxfeepercent=0.25)
476+
l1.daemon.wait_for_log('Converted maxfeepercent=0.25, exemptfee=UNSET to maxfee 5000msat')
477+
476478

477479
def test_xpay_preapprove(node_factory):
478480
l1, l2 = node_factory.line_graph(2, opts={'dev-hsmd-fail-preapprove': None})

0 commit comments

Comments
 (0)