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 <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
  • Loading branch information
rustyrussell committed Mar 19, 2024
1 parent f495b23 commit c1b2c56
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 303 deletions.
5 changes: 5 additions & 0 deletions .msggen.json
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,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 @@ -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
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.

7 changes: 7 additions & 0 deletions contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
]
}
}
},
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
596 changes: 298 additions & 298 deletions contrib/pyln-grpc-proto/pyln/grpc/node_pb2.py

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions doc/schemas/lightning-pay.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
]
}
}
},
Expand Down
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 c1b2c56

Please sign in to comment.