mirror of
https://github.com/ElementsProject/lightning.git
synced 2024-11-19 01:43:36 +01:00
xpay: new plugin which uses askrene, injectpaymentonion.
Changelog-Added: Plugins: cln-xpay, with associated `xpay` command for payments. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
8c051c555e
commit
7c2407ef48
@ -36882,6 +36882,129 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"lightning-xpay.json": {
|
||||
"$schema": "../rpc-schema-draft.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"rpc": "xpay",
|
||||
"title": "Command for sending a payment for an invoice",
|
||||
"description": [
|
||||
"The **xpay** RPC command attempts to find routes to the given destination, and send the funds it asks for.",
|
||||
"",
|
||||
"This plugin is simpler and more sophisticated than the older 'pay' plugin, but does not have all the same features."
|
||||
],
|
||||
"request": {
|
||||
"required": [
|
||||
"invstring"
|
||||
],
|
||||
"properties": {
|
||||
"invstring": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"bolt11 or bolt12 invoice"
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Only possible for a bolt11 invoice which does not have an amount (in which case, it's compulsory). *amount_msat* is in millisatoshi precision; it can be a whole number, or a whole number with suffix *msat* or *sat*, or a three decimal point number with suffix *sat*, or an 1 to 11 decimal point number suffixed by *btc*."
|
||||
]
|
||||
},
|
||||
"maxfee": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"*maxfee* creates an absolute limit on what fee we will pay."
|
||||
],
|
||||
"default": "5000msat, or 1% (whatever is greater)"
|
||||
},
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"description": [
|
||||
"These are askrene layers to apply in addition to xpay's own: these can alter the topology or provide additional information on the lightning network. See askrene-create-layer."
|
||||
],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"name of an existing layer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"retry_for": {
|
||||
"type": "u32",
|
||||
"description": [
|
||||
"Until *retry_for* seconds passes, the command will keep finding routes and retrying the payment."
|
||||
],
|
||||
"default": "60 seconds"
|
||||
},
|
||||
"partial_msat": {
|
||||
"type": "msat",
|
||||
"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)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"required": [
|
||||
"payment_preimage",
|
||||
"failed_parts",
|
||||
"successful_parts",
|
||||
"amount_msat",
|
||||
"amount_sent_msat"
|
||||
],
|
||||
"properties": {
|
||||
"payment_preimage": {
|
||||
"type": "secret",
|
||||
"description": [
|
||||
"The proof of payment: SHA256 of this **payment_hash**."
|
||||
]
|
||||
},
|
||||
"failed_parts": {
|
||||
"type": "u64",
|
||||
"description": [
|
||||
"How many separate payment parts failed."
|
||||
]
|
||||
},
|
||||
"successful_parts": {
|
||||
"type": "u64",
|
||||
"description": [
|
||||
"How many separate payment parts succeeded (or are anticipated to succeed). This will be at least one."
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Amount the recipient received."
|
||||
]
|
||||
},
|
||||
"amount_sent_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Total amount we sent (including fees)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
"The following error codes may occur:",
|
||||
"",
|
||||
"- -1: Catchall nonspecific error.",
|
||||
"- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)",
|
||||
"- 205: Couldn't find, or find a way to, the destination.",
|
||||
"- 219: Invoice has already been paid.",
|
||||
"- 209: Other payment error."
|
||||
],
|
||||
"author": [
|
||||
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
|
||||
],
|
||||
"see_also": [
|
||||
"lightning-pay(7)",
|
||||
"lightning-decodepay(7)"
|
||||
],
|
||||
"resources": [
|
||||
"Main web site: <https://github.com/ElementsProject/lightning>"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -142,7 +142,8 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \
|
||||
doc/lightning-waitinvoice.7 \
|
||||
doc/lightning-wait.7 \
|
||||
doc/lightning-waitsendpay.7 \
|
||||
doc/lightning-withdraw.7
|
||||
doc/lightning-withdraw.7 \
|
||||
doc/lightning-xpay.7
|
||||
|
||||
ifeq ($(HAVE_SQLITE3),1)
|
||||
GENERATE_MARKDOWN += doc/lightning-listsqlschemas.7 \
|
||||
|
@ -155,6 +155,7 @@ Core Lightning Documentation
|
||||
lightning-waitinvoice <lightning-waitinvoice.7.md>
|
||||
lightning-waitsendpay <lightning-waitsendpay.7.md>
|
||||
lightning-withdraw <lightning-withdraw.7.md>
|
||||
lightning-xpay <lightning-xpay.7.md>
|
||||
lightningd-config <lightningd-config.5.md>
|
||||
lightningd-rpc <lightningd-rpc.7.md>
|
||||
lightningd <lightningd.8.md>
|
||||
|
123
doc/schemas/lightning-xpay.json
Normal file
123
doc/schemas/lightning-xpay.json
Normal file
@ -0,0 +1,123 @@
|
||||
{
|
||||
"$schema": "../rpc-schema-draft.json",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"rpc": "xpay",
|
||||
"title": "Command for sending a payment for an invoice",
|
||||
"description": [
|
||||
"The **xpay** RPC command attempts to find routes to the given destination, and send the funds it asks for.",
|
||||
"",
|
||||
"This plugin is simpler and more sophisticated than the older 'pay' plugin, but does not have all the same features."
|
||||
],
|
||||
"request": {
|
||||
"required": [
|
||||
"invstring"
|
||||
],
|
||||
"properties": {
|
||||
"invstring": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"bolt11 or bolt12 invoice"
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Only possible for a bolt11 invoice which does not have an amount (in which case, it's compulsory). *amount_msat* is in millisatoshi precision; it can be a whole number, or a whole number with suffix *msat* or *sat*, or a three decimal point number with suffix *sat*, or an 1 to 11 decimal point number suffixed by *btc*."
|
||||
]
|
||||
},
|
||||
"maxfee": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"*maxfee* creates an absolute limit on what fee we will pay."
|
||||
],
|
||||
"default": "5000msat, or 1% (whatever is greater)"
|
||||
},
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"description": [
|
||||
"These are askrene layers to apply in addition to xpay's own: these can alter the topology or provide additional information on the lightning network. See askrene-create-layer."
|
||||
],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": [
|
||||
"name of an existing layer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"retry_for": {
|
||||
"type": "u32",
|
||||
"description": [
|
||||
"Until *retry_for* seconds passes, the command will keep finding routes and retrying the payment."
|
||||
],
|
||||
"default": "60 seconds"
|
||||
},
|
||||
"partial_msat": {
|
||||
"type": "msat",
|
||||
"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)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"required": [
|
||||
"payment_preimage",
|
||||
"failed_parts",
|
||||
"successful_parts",
|
||||
"amount_msat",
|
||||
"amount_sent_msat"
|
||||
],
|
||||
"properties": {
|
||||
"payment_preimage": {
|
||||
"type": "secret",
|
||||
"description": [
|
||||
"The proof of payment: SHA256 of this **payment_hash**."
|
||||
]
|
||||
},
|
||||
"failed_parts": {
|
||||
"type": "u64",
|
||||
"description": [
|
||||
"How many separate payment parts failed."
|
||||
]
|
||||
},
|
||||
"successful_parts": {
|
||||
"type": "u64",
|
||||
"description": [
|
||||
"How many separate payment parts succeeded (or are anticipated to succeed). This will be at least one."
|
||||
]
|
||||
},
|
||||
"amount_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Amount the recipient received."
|
||||
]
|
||||
},
|
||||
"amount_sent_msat": {
|
||||
"type": "msat",
|
||||
"description": [
|
||||
"Total amount we sent (including fees)."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
"The following error codes may occur:",
|
||||
"",
|
||||
"- -1: Catchall nonspecific error.",
|
||||
"- 203: Permanent failure from destination (e.g. it said it didn't recognize invoice)",
|
||||
"- 205: Couldn't find, or find a way to, the destination.",
|
||||
"- 219: Invoice has already been paid.",
|
||||
"- 209: Other payment error."
|
||||
],
|
||||
"author": [
|
||||
"Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible."
|
||||
],
|
||||
"see_also": [
|
||||
"lightning-pay(7)",
|
||||
"lightning-decodepay(7)"
|
||||
],
|
||||
"resources": [
|
||||
"Main web site: <https://github.com/ElementsProject/lightning>"
|
||||
]
|
||||
}
|
@ -119,6 +119,7 @@ C_PLUGINS := \
|
||||
plugins/recover \
|
||||
plugins/txprepare \
|
||||
plugins/cln-renepay \
|
||||
plugins/cln-xpay \
|
||||
plugins/spenderp \
|
||||
plugins/cln-askrene
|
||||
|
||||
@ -202,6 +203,7 @@ PLUGIN_COMMON_OBJS := \
|
||||
include plugins/askrene/Makefile
|
||||
include plugins/bkpr/Makefile
|
||||
include plugins/renepay/Makefile
|
||||
include plugins/xpay/Makefile
|
||||
|
||||
# Make sure these depend on everything.
|
||||
ALL_C_SOURCES += $(PLUGIN_ALL_SRC)
|
||||
|
15
plugins/xpay/Makefile
Normal file
15
plugins/xpay/Makefile
Normal file
@ -0,0 +1,15 @@
|
||||
PLUGIN_XPAY_SRC := \
|
||||
plugins/xpay/xpay.c
|
||||
|
||||
PLUGIN_XPAY_HDRS :=
|
||||
|
||||
PLUGIN_XPAY_OBJS := $(PLUGIN_XPAY_SRC:.c=.o)
|
||||
|
||||
# Make sure these depend on everything.
|
||||
ALL_C_SOURCES += $(PLUGIN_XPAY_SRC)
|
||||
ALL_C_HEADERS += $(PLUGIN_XPAY_HDRS)
|
||||
|
||||
# Make all plugins depend on all plugin headers, for simplicity.
|
||||
$(PLUGIN_XPAY_OBJS): $(PLUGIN_XPAY_HDRS)
|
||||
|
||||
plugins/cln-xpay: $(PLUGIN_XPAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) bitcoin/chainparams.o common/gossmap.o common/gossmods_listpeerchannels.o common/fp16.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o common/sciddir_or_pubkey.o wire/bolt12_wiregen.o wire/onion_wiregen.o common/onionreply.o common/onion_encode.o common/sphinx.o common/hmac.o
|
1533
plugins/xpay/xpay.c
Normal file
1533
plugins/xpay/xpay.c
Normal file
File diff suppressed because it is too large
Load Diff
@ -121,7 +121,9 @@ def test_reserve(node_factory):
|
||||
|
||||
def test_layers(node_factory):
|
||||
"""Test manipulating information in layers"""
|
||||
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)
|
||||
# remove xpay, since it creates a layer!
|
||||
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
|
||||
opts={'disable-plugin': 'cln-xpay'})
|
||||
|
||||
assert l2.rpc.askrene_listlayers() == {'layers': []}
|
||||
with pytest.raises(RpcError, match="Unknown layer"):
|
||||
|
@ -132,3 +132,65 @@ def test_pay_fakenet(node_factory):
|
||||
|
||||
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=2)
|
||||
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=3)
|
||||
|
||||
|
||||
def test_xpay_simple(node_factory):
|
||||
l1, l2, l3, l4 = node_factory.get_nodes(4, opts={'experimental-offers': None,
|
||||
'may_reconnect': True})
|
||||
node_factory.join_nodes([l1, l2, l3], wait_for_announce=True)
|
||||
node_factory.join_nodes([l3, l4], announce_channels=False)
|
||||
|
||||
# BOLT 11, direct peer
|
||||
b11 = l2.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
|
||||
ret = l1.rpc.xpay(b11)
|
||||
assert ret['failed_parts'] == 0
|
||||
assert ret['successful_parts'] == 1
|
||||
assert ret['amount_msat'] == 10000
|
||||
assert ret['amount_sent_msat'] == 10000
|
||||
|
||||
# Fails if we try to pay again
|
||||
b11_paid = b11
|
||||
with pytest.raises(RpcError, match="Already paid"):
|
||||
l1.rpc.xpay(b11_paid)
|
||||
|
||||
# BOLT 11, indirect peer
|
||||
b11 = l3.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
|
||||
ret = l1.rpc.xpay(b11)
|
||||
assert ret['failed_parts'] == 0
|
||||
assert ret['successful_parts'] == 1
|
||||
assert ret['amount_msat'] == 10000
|
||||
assert ret['amount_sent_msat'] == 10001
|
||||
|
||||
# BOLT 11, routehint
|
||||
b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple', 'test_xpay_simple bolt11')['bolt11']
|
||||
l1.rpc.xpay(b11)
|
||||
|
||||
# BOLT 12.
|
||||
offer = l3.rpc.offer('any')['bolt12']
|
||||
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
|
||||
l1.rpc.xpay(b12)
|
||||
|
||||
# Failure from l4.
|
||||
b11 = l4.rpc.invoice('10000msat', 'test_xpay_simple2', 'test_xpay_simple2 bolt11')['bolt11']
|
||||
l4.rpc.delinvoice('test_xpay_simple2', 'unpaid')
|
||||
with pytest.raises(RpcError, match="Destination said it doesn't know invoice"):
|
||||
l1.rpc.xpay(b11)
|
||||
|
||||
offer = l4.rpc.offer('any')['bolt12']
|
||||
b12 = l1.rpc.fetchinvoice(offer, '100000msat')['invoice']
|
||||
|
||||
# Failure from l3 (with routehint)
|
||||
l4.stop()
|
||||
with pytest.raises(RpcError, match=r"Failed after 1 attempts\. We got temporary_channel_failure for the invoice's route hint \([0-9x]*/[01]\), assuming it can't carry 10000msat\. Then routing failed: We could not find a usable set of paths\. The shortest path is [0-9x]*->[0-9x]*->[0-9x]*, but [0-9x]*/[01]\ layer xpay-6 says max is 9999msat"):
|
||||
l1.rpc.xpay(b11)
|
||||
|
||||
# Failure from l3 (with blinded path)
|
||||
# FIXME: We return wrong error here!
|
||||
with pytest.raises(RpcError, match=r"Failed after 1 attempts\. Unexpected error \(invalid_onion_payload\) from intermediate node: disabling the invoice's blinded path \(0x0x0/[01]\) for this payment\. Then routing failed: We could not find a usable set of paths\. The destination has disabled 1 of 1 channels, leaving capacity only 0msat of 10605000msat\."):
|
||||
l1.rpc.xpay(b12)
|
||||
|
||||
# Restart, try pay already paid one again.
|
||||
l1.restart()
|
||||
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
|
||||
with pytest.raises(RpcError, match="Already paid"):
|
||||
l1.rpc.xpay(b11_paid)
|
||||
|
Loading…
Reference in New Issue
Block a user