pay: add absolute "maxfee" parameter.

This is what LND does, and it's better for upper layers than trying to
twist our maxfeepercent / exemptfee heuristics to suit.

(I don't remember who complained about this, sorry!)

I'm doing this now because I want to add YA parameter next!

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: JSON-RPC: `pay` has new parameter `maxfee` for setting absolute fee (instead of using `maxfeepercent` and/or `exemptfee`)
This commit is contained in:
Rusty Russell 2022-04-02 12:55:04 +10:30
parent 713cd0540d
commit 6c54e0e7e7
6 changed files with 55 additions and 19 deletions

View File

@ -997,7 +997,8 @@ class LightningRpc(UnixDomainSocketRpc):
def pay(self, bolt11, msatoshi=None, label=None, riskfactor=None,
maxfeepercent=None, retry_for=None,
maxdelay=None, exemptfee=None, localofferid=None, exclude=None):
maxdelay=None, exemptfee=None, localofferid=None, exclude=None,
maxfee=None):
"""
Send payment specified by {bolt11} with {msatoshi}
(ignored if {bolt11} has an amount), optional {label}
@ -1014,6 +1015,7 @@ class LightningRpc(UnixDomainSocketRpc):
"exemptfee": exemptfee,
"localofferid": localofferid,
"exclude": exclude,
"maxfee": maxfee,
}
return self.call("pay", payload)

View File

@ -5,8 +5,8 @@ SYNOPSIS
--------
**pay** *bolt11* [*msatoshi*] [*label*] [*riskfactor*]
[*maxfeepercent*] [*retry\_for*] [*maxdelay*] [*exemptfee*]
[*localofferid*] [*exclude*]
[*maxfeepercent*] [*retry_for*] [*maxdelay*] [*exemptfee*]
[*localofferid*] [*exclude*] [*maxfee*]
DESCRIPTION
-----------
@ -37,6 +37,12 @@ leveraged by forwarding nodes. Setting `exemptfee` allows the
that we only make a single payment for an offer, and that the offer is
marked `used` once paid.
*maxfee* overrides both *maxfeepercent* and *exemptfee* defaults (and
if you specify *maxfee* you cannot specify either of those), and
creates an absolute limit on what fee we will pay. This allows you to
implement your own heuristics rather than the primitive ones used
here.
The response will occur when the payment fails or succeeds. Once a
payment has succeeded, calls to **pay** with the same *bolt11* will
succeed immediately.

View File

@ -45,6 +45,9 @@
}
]
}
},
"maxfee": {
"type": "msat"
}
}
}

View File

@ -925,7 +925,7 @@ static struct command_result *json_pay(struct command *cmd,
char *b11_fail, *b12_fail;
u64 *maxfee_pct_millionths;
u32 *maxdelay;
struct amount_msat *exemptfee, *msat;
struct amount_msat *exemptfee, *msat, *maxfee;
const char *label;
unsigned int *retryfor;
u64 *riskfactor_millionths;
@ -950,14 +950,15 @@ static struct command_result *json_pay(struct command *cmd,
p_opt("label", param_string, &label),
p_opt_def("riskfactor", param_millionths,
&riskfactor_millionths, 10000000),
p_opt_def("maxfeepercent", param_millionths,
&maxfee_pct_millionths, 500000),
p_opt("maxfeepercent", param_millionths,
&maxfee_pct_millionths),
p_opt_def("retry_for", param_number, &retryfor, 60),
p_opt_def("maxdelay", param_number, &maxdelay,
maxdelay_default),
p_opt_def("exemptfee", param_msat, &exemptfee, AMOUNT_MSAT(5000)),
p_opt("exemptfee", param_msat, &exemptfee),
p_opt("localofferid", param_sha256, &local_offer_id),
p_opt("exclude", param_route_exclusion_array, &exclusions),
p_opt("maxfee", param_msat, &maxfee),
#if DEVELOPER
p_opt_def("use_shadow", param_bool, &use_shadow, true),
#endif
@ -1103,17 +1104,35 @@ static struct command_result *json_pay(struct command *cmd,
p->getroute->riskfactorppm = *riskfactor_millionths;
tal_free(riskfactor_millionths);
/* We free unneeded params as we use them, to keep memleak happy. */
if (!amount_msat_fee(&p->constraints.fee_budget, p->amount, 0,
*maxfee_pct_millionths / 100)) {
return command_fail(
cmd, JSONRPC2_INVALID_PARAMS,
"Overflow when computing fee budget, fee rate too high.");
}
tal_free(maxfee_pct_millionths);
if (maxfee) {
if (maxfee_pct_millionths || exemptfee) {
return command_fail(
cmd, JSONRPC2_INVALID_PARAMS,
"If you specify maxfee, cannot specify maxfeepercent or exemptfee.");
}
p->constraints.fee_budget = *maxfee;
payment_mod_exemptfee_get_data(p)->amount = AMOUNT_MSAT(0);
} else {
u64 maxppm;
if (maxfee_pct_millionths)
maxppm = *maxfee_pct_millionths / 100;
else
maxppm = 500000 / 100;
if (!amount_msat_fee(&p->constraints.fee_budget, p->amount, 0,
maxppm)) {
return command_fail(
cmd, JSONRPC2_INVALID_PARAMS,
"Overflow when computing fee budget, fee rate too high.");
}
payment_mod_exemptfee_get_data(p)->amount
= exemptfee ? *exemptfee : AMOUNT_MSAT(5000);
/* We free unneeded params now to keep memleak happy. */
tal_free(maxfee_pct_millionths);
tal_free(exemptfee);
}
payment_mod_exemptfee_get_data(p)->amount = *exemptfee;
tal_free(exemptfee);
shadow_route = payment_mod_shadowroute_get_data(p);
payment_mod_presplit_get_data(p)->disable = disablempp;
payment_mod_adaptive_splitter_get_data(p)->disable = disablempp;

View File

@ -128,10 +128,15 @@ def test_pay_limits(node_factory):
assert(len(status) == 2)
assert(status[0]['failure']['code'] == 205)
# This fails!
err = r'Fee exceeds our fee budget: 2msat > 1msat, discarding route'
with pytest.raises(RpcError, match=err) as err:
l1.rpc.pay(bolt11=inv['bolt11'], msatoshi=100000, maxfee=1)
# This works, because fee is less than exemptfee.
l1.dev_pay(inv['bolt11'], msatoshi=100000, maxfeepercent=0.0001,
exemptfee=2000, use_shadow=False)
status = l1.rpc.call('paystatus', {'bolt11': inv['bolt11']})['pay'][2]['attempts']
status = l1.rpc.call('paystatus', {'bolt11': inv['bolt11']})['pay'][3]['attempts']
assert len(status) == 1
assert status[0]['strategy'] == "Initial attempt"

View File

@ -397,7 +397,8 @@ def test_pay_plugin(node_factory):
# Make sure usage messages are present.
msg = 'pay bolt11 [msatoshi] [label] [riskfactor] [maxfeepercent] '\
'[retry_for] [maxdelay] [exemptfee] [localofferid] [exclude]'
'[retry_for] [maxdelay] [exemptfee] [localofferid] [exclude] '\
'[maxfee]'
if DEVELOPER:
msg += ' [use_shadow]'
assert only_one(l1.rpc.help('pay')['help'])['command'] == msg