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:
Rusty Russell 2024-11-17 16:11:06 +10:30
parent 8c051c555e
commit 7c2407ef48
9 changed files with 1864 additions and 2 deletions

View File

@ -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": { "notifications": {

View File

@ -142,7 +142,8 @@ GENERATE_MARKDOWN := doc/lightning-addgossip.7 \
doc/lightning-waitinvoice.7 \ doc/lightning-waitinvoice.7 \
doc/lightning-wait.7 \ doc/lightning-wait.7 \
doc/lightning-waitsendpay.7 \ doc/lightning-waitsendpay.7 \
doc/lightning-withdraw.7 doc/lightning-withdraw.7 \
doc/lightning-xpay.7
ifeq ($(HAVE_SQLITE3),1) ifeq ($(HAVE_SQLITE3),1)
GENERATE_MARKDOWN += doc/lightning-listsqlschemas.7 \ GENERATE_MARKDOWN += doc/lightning-listsqlschemas.7 \

View File

@ -155,6 +155,7 @@ Core Lightning Documentation
lightning-waitinvoice <lightning-waitinvoice.7.md> lightning-waitinvoice <lightning-waitinvoice.7.md>
lightning-waitsendpay <lightning-waitsendpay.7.md> lightning-waitsendpay <lightning-waitsendpay.7.md>
lightning-withdraw <lightning-withdraw.7.md> lightning-withdraw <lightning-withdraw.7.md>
lightning-xpay <lightning-xpay.7.md>
lightningd-config <lightningd-config.5.md> lightningd-config <lightningd-config.5.md>
lightningd-rpc <lightningd-rpc.7.md> lightningd-rpc <lightningd-rpc.7.md>
lightningd <lightningd.8.md> lightningd <lightningd.8.md>

View 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>"
]
}

View File

@ -119,6 +119,7 @@ C_PLUGINS := \
plugins/recover \ plugins/recover \
plugins/txprepare \ plugins/txprepare \
plugins/cln-renepay \ plugins/cln-renepay \
plugins/cln-xpay \
plugins/spenderp \ plugins/spenderp \
plugins/cln-askrene plugins/cln-askrene
@ -202,6 +203,7 @@ PLUGIN_COMMON_OBJS := \
include plugins/askrene/Makefile include plugins/askrene/Makefile
include plugins/bkpr/Makefile include plugins/bkpr/Makefile
include plugins/renepay/Makefile include plugins/renepay/Makefile
include plugins/xpay/Makefile
# Make sure these depend on everything. # Make sure these depend on everything.
ALL_C_SOURCES += $(PLUGIN_ALL_SRC) ALL_C_SOURCES += $(PLUGIN_ALL_SRC)

15
plugins/xpay/Makefile Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,9 @@ def test_reserve(node_factory):
def test_layers(node_factory): def test_layers(node_factory):
"""Test manipulating information in layers""" """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': []} assert l2.rpc.askrene_listlayers() == {'layers': []}
with pytest.raises(RpcError, match="Unknown layer"): with pytest.raises(RpcError, match="Unknown layer"):

View File

@ -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=2)
l1.rpc.waitsendpay(payment_hash=hash2, timeout=TIMEOUT, partid=3) 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)