pay: add partial_msat option to make partial payment.

a.k.a. "Pay with a friend!".

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: JSON-RPC: `pay` has a new parameter `partial_msat` to only pay part of an invoice (someone else presumably will pay the rest at the same time!)
Suggested-by: Calle
This commit is contained in:
Rusty Russell 2024-03-20 09:50:13 +10:30
parent bd5d2d1673
commit 4a9b9b8b29
11 changed files with 380 additions and 303 deletions

View File

@ -1532,6 +1532,7 @@
"Pay.maxfee": 11,
"Pay.maxfeepercent": 4,
"Pay.msatoshi": 2,
"Pay.partial_msat": 15,
"Pay.retry_for": 5,
"Pay.riskfactor": 8
},
@ -5574,6 +5575,10 @@
"added": "pre-v0.10.1",
"deprecated": false
},
"Pay.partial_msat": {
"added": "v23.05",
"deprecated": false
},
"Pay.parts": {
"added": "pre-v0.10.1",
"deprecated": false

Binary file not shown.

BIN
cln-grpc/src/convert.rs generated

Binary file not shown.

BIN
cln-rpc/src/model.rs generated

Binary file not shown.

View File

@ -25182,6 +25182,13 @@
"description": [
"It is only required for bolt11 invoices which do not contain a description themselves, but contain a description hash: in this case *description* is required. *description* is then checked against the hash inside the invoice before it will be paid."
]
},
"partial_msat": {
"type": "msat",
"added": "v23.05",
"description": [
"Explicitly state that you are only paying some part of the invoice. Presumably someone else is paying the rest (otherwise the payment will time out at the recipient). Note that this is currently not supported for self-payment (please file an issue if you need this)"
]
}
}
},

View File

@ -1085,7 +1085,7 @@ class LightningRpc(UnixDomainSocketRpc):
def pay(self, bolt11, amount_msat=None, label=None, riskfactor=None,
maxfeepercent=None, retry_for=None,
maxdelay=None, exemptfee=None, localinvreqid=None, exclude=None,
maxfee=None, description=None):
maxfee=None, description=None, partial_msat=None):
"""
Send payment specified by {bolt11} with {amount_msat}
(ignored if {bolt11} has an amount), optional {label}
@ -1104,6 +1104,7 @@ class LightningRpc(UnixDomainSocketRpc):
"exclude": exclude,
"maxfee": maxfee,
"description": description,
"partial_msat": partial_msat,
}
return self.call("pay", payload)

File diff suppressed because one or more lines are too long

View File

@ -102,6 +102,13 @@
"description": [
"It is only required for bolt11 invoices which do not contain a description themselves, but contain a description hash: in this case *description* is required. *description* is then checked against the hash inside the invoice before it will be paid."
]
},
"partial_msat": {
"type": "msat",
"added": "v23.05",
"description": [
"Explicitly state that you are only paying some part of the invoice. Presumably someone else is paying the rest (otherwise the payment will time out at the recipient). Note that this is currently not supported for self-payment (please file an issue if you need this)"
]
}
}
},

View File

@ -1021,7 +1021,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, *maxfee;
struct amount_msat *exemptfee, *msat, *maxfee, *partial;
const char *label, *description;
unsigned int *retryfor;
u64 *riskfactor_millionths;
@ -1054,6 +1054,7 @@ static struct command_result *json_pay(struct command *cmd,
p_opt("exclude", param_route_exclusion_array, &exclusions),
p_opt("maxfee", param_msat, &maxfee),
p_opt("description", param_escaped_string, &description),
p_opt("partial_msat", param_msat, &partial),
p_opt_dev("dev_use_shadow", param_bool, &dev_use_shadow, true),
NULL))
return command_param_failed();
@ -1199,8 +1200,21 @@ static struct command_result *json_pay(struct command *cmd,
p->final_amount = *msat;
}
/* FIXME: Allow partial payment! */
p->our_amount = p->final_amount;
if (partial) {
if (amount_msat_greater(*partial, p->final_amount)) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"partial_msat must be less or equal to total amount %s",
fmt_amount_msat(tmpctx, p->final_amount));
}
if (node_id_eq(&my_id, p->destination)) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"partial_msat not supported (yet?) for self-pay");
}
p->our_amount = *partial;
} else {
p->our_amount = p->final_amount;
}
/* We replace real final values if we're using a blinded path */
if (p->blindedpath) {

View File

@ -5477,3 +5477,46 @@ def test_pay_routehint_minhtlc(node_factory, bitcoind):
# And you should also be able to getroute (and have it ignore htlc_min/max constraints!)
l1.rpc.getroute(l3.info['id'], amount_msat=0, riskfactor=1)
@pytest.mark.openchannel('v1')
@pytest.mark.openchannel('v2')
def test_pay_partial_msat(node_factory, executor):
l1, l2, l3 = node_factory.line_graph(3)
inv = l3.rpc.invoice(100000000, "inv", "inv")
with pytest.raises(RpcError, match="partial_msat must be less or equal to total amount 10000000"):
l2.rpc.pay(inv['bolt11'], partial_msat=100000001)
# This will fail with an MPP timeout.
with pytest.raises(RpcError, match="failed: WIRE_MPP_TIMEOUT"):
l2.rpc.pay(inv['bolt11'], partial_msat=90000000)
# This will work like normal.
l2.rpc.pay(inv['bolt11'], partial_msat=100000000)
# Make sure l3 can pay to l2 now.
wait_for(lambda: only_one(l3.rpc.listpeerchannels()['channels'])['spendable_msat'] > 1001)
# Now we can combine together to pay l2:
inv = l2.rpc.invoice('any', "inv", "inv")
# If we specify different totals, this *won't work*
l1pay = executor.submit(l1.rpc.pay, inv['bolt11'], amount_msat=10000, partial_msat=9000)
l3pay = executor.submit(l3.rpc.pay, inv['bolt11'], amount_msat=10001, partial_msat=1001)
# BOLT #4:
# - SHOULD fail the entire HTLC set if `total_msat` is not
# the same for all HTLCs in the set.
with pytest.raises(RpcError, match="failed: WIRE_FINAL_INCORRECT_HTLC_AMOUNT"):
l3pay.result(TIMEOUT)
with pytest.raises(RpcError, match="failed: WIRE_FINAL_INCORRECT_HTLC_AMOUNT"):
l1pay.result(TIMEOUT)
# But same amount, will combine forces!
l1pay = executor.submit(l1.rpc.pay, inv['bolt11'], amount_msat=10000, partial_msat=9000)
l3pay = executor.submit(l3.rpc.pay, inv['bolt11'], amount_msat=10000, partial_msat=1000)
l1pay.result(TIMEOUT)
l3pay.result(TIMEOUT)

View File

@ -427,7 +427,7 @@ def test_pay_plugin(node_factory):
# Make sure usage messages are present.
msg = 'pay bolt11 [amount_msat] [label] [riskfactor] [maxfeepercent] '\
'[retry_for] [maxdelay] [exemptfee] [localinvreqid] [exclude] '\
'[maxfee] [description]'
'[maxfee] [description] [partial_msat]'
# We run with --developer:
msg += ' [dev_use_shadow]'
assert only_one(l1.rpc.help('pay')['help'])['command'] == msg