Skip to content

Commit

Permalink
pay: add partial_msat option to make partial payment.
Browse files Browse the repository at this point in the history
a.k.a. "Pay with a friend!".

Signed-off-by: Rusty Russell <[email protected]>
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
  • Loading branch information
rustyrussell committed Mar 18, 2024
1 parent 3ad6647 commit fbc9fbc
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 302 deletions.
5 changes: 5 additions & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@
"Pay.maxfee": 11,
"Pay.maxfeepercent": 4,
"Pay.msatoshi": 2,
"Pay.partial_msat": 15,
"Pay.retry_for": 5,
"Pay.riskfactor": 8
},
Expand Down Expand Up @@ -5542,6 +5543,10 @@
"added": "pre-v0.10.1",
"deprecated": false
},
"Pay.partial_msat": {
"added": "v24.05",
"deprecated": false
},
"Pay.parts": {
"added": "pre-v0.10.1",
"deprecated": false
Expand Down
1 change: 1 addition & 0 deletions cln-grpc/proto/node.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cln-grpc/src/convert.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cln-rpc/src/model.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13931,6 +13931,10 @@
},
"description": {
"type": "string"
},
"partial_msat": {
"added": "v24.05",
"type": "msat"
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,7 @@ def newaddr(self, addresstype=None):
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}
Expand All @@ -1104,6 +1104,7 @@ def pay(self, bolt11, amount_msat=None, label=None, riskfactor=None,
"exclude": exclude,
"maxfee": maxfee,
"description": description,
"partial_msat": partial_msat,
}
return self.call("pay", payload)

Expand Down
592 changes: 296 additions & 296 deletions contrib/pyln-grpc-proto/pyln/grpc/node_pb2.py

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion doc/lightning-pay.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SYNOPSIS

**pay** *bolt11* [*amount\_msat*] [*label*] [*riskfactor*]
[*maxfeepercent*] [*retry\_for*] [*maxdelay*] [*exemptfee*]
[*localinvreqid*] [*exclude*] [*maxfee*] [*description*]
[*localinvreqid*] [*exclude*] [*maxfee*] [*description*] [*part\_msat*]

DESCRIPTION
-----------
Expand Down Expand Up @@ -49,6 +49,12 @@ in this case *description* is required.
*description* is then checked against the hash inside the invoice
before it will be paid.

*part\_msat* allows you to 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).

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.
Expand Down
4 changes: 4 additions & 0 deletions doc/schemas/pay.request.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
},
"description": {
"type": "string"
},
"partial_msat": {
"added": "v24.05",
"type": "msat"
}
}
}
20 changes: 17 additions & 3 deletions plugins/pay.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down
43 changes: 43 additions & 0 deletions tests/test_pay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fbc9fbc

Please sign in to comment.