diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 5b5d93298..4996c81fb 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -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) diff --git a/doc/lightning-pay.7.md b/doc/lightning-pay.7.md index 2b52c6e54..dd1ff3ad2 100644 --- a/doc/lightning-pay.7.md +++ b/doc/lightning-pay.7.md @@ -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. diff --git a/doc/schemas/pay.request.json b/doc/schemas/pay.request.json index d7bcb5f85..b6dc27a1a 100644 --- a/doc/schemas/pay.request.json +++ b/doc/schemas/pay.request.json @@ -45,6 +45,9 @@ } ] } + }, + "maxfee": { + "type": "msat" } } } diff --git a/plugins/pay.c b/plugins/pay.c index 23a9170c5..7bc0eb92c 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -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; diff --git a/tests/test_pay.py b/tests/test_pay.py index ac68c7b40..e0914887e 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -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" diff --git a/tests/test_plugin.py b/tests/test_plugin.py index fecc7ac6e..01b90631d 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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