mirror of
https://github.com/daywalker90/cln-nip47.git
synced 2026-08-13 12:33:43 +02:00
2009 lines
66 KiB
Python
Executable file
2009 lines
66 KiB
Python
Executable file
import hashlib
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import time
|
|
import asyncio
|
|
import secrets
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Awaitable, Callable, Union
|
|
|
|
import pytest
|
|
from pyln.testing.fixtures import * # noqa: F403
|
|
from pyln.testing.utils import RpcError, wait_for, TIMEOUT
|
|
from util import generate_random_label, get_plugin, get_hold # noqa: F401
|
|
|
|
from nostr_sdk import (
|
|
Alphabet,
|
|
Client,
|
|
RelayUrl,
|
|
EventBuilder,
|
|
Filter,
|
|
Event,
|
|
Keys,
|
|
KeysendTlvRecord,
|
|
Kind,
|
|
HandleNotification,
|
|
ListTransactionsRequest,
|
|
LookupInvoiceRequest,
|
|
MakeInvoiceRequest,
|
|
NostrSdkError,
|
|
NostrSigner,
|
|
NostrWalletConnectUri,
|
|
Nwc,
|
|
PayInvoiceRequest,
|
|
PayKeysendRequest,
|
|
SingleLetterTag,
|
|
Tag,
|
|
TagKind,
|
|
Method,
|
|
PublicKey,
|
|
)
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class NotificationHandler(HandleNotification):
|
|
def __init__(self, events_list, stop_after):
|
|
self.events_list = events_list
|
|
self.stop_after = stop_after
|
|
self._done = asyncio.Event()
|
|
|
|
async def handle(self, relay_url, subscription_id, event: Event):
|
|
LOGGER.info(f"Received new event from {relay_url}: {event.as_json()}")
|
|
self.events_list.append(event)
|
|
if len(self.events_list) >= self.stop_after:
|
|
self._done.set()
|
|
|
|
async def handle_msg(self, relay_url, msg):
|
|
_var = None
|
|
|
|
|
|
Action = Union[
|
|
Callable[[], Awaitable[None]],
|
|
Callable[[], None],
|
|
Awaitable[None],
|
|
]
|
|
|
|
|
|
async def fetch_event_responses(
|
|
client: Client,
|
|
client_pubkey: PublicKey,
|
|
event_kind: int,
|
|
action: Action,
|
|
stop_after: int,
|
|
timeout: int = TIMEOUT,
|
|
) -> tuple[list[Event], Any]:
|
|
events = []
|
|
response_filter = Filter().kind(Kind(event_kind)).pubkey(client_pubkey)
|
|
|
|
id = uuid.uuid4().hex
|
|
LOGGER.info(f"Subscribing with id {id} to {response_filter}")
|
|
await client.subscribe_with_id(id, response_filter)
|
|
|
|
handler = NotificationHandler(events, stop_after)
|
|
task = asyncio.create_task(client.handle_notifications(handler))
|
|
|
|
time.sleep(1)
|
|
if inspect.iscoroutine(action):
|
|
action_result = await action
|
|
elif inspect.iscoroutinefunction(action):
|
|
action_result = await action()
|
|
elif callable(action):
|
|
action_result = await asyncio.to_thread(action)
|
|
else:
|
|
raise TypeError("action must be a callable or an awaitable")
|
|
|
|
try:
|
|
await asyncio.wait_for(handler._done.wait(), timeout=timeout)
|
|
except asyncio.TimeoutError:
|
|
print(
|
|
f"Timeout reached after {timeout} seconds, collected {len(events)} events"
|
|
)
|
|
finally:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
await client.unsubscribe_all()
|
|
assert len(events) == stop_after
|
|
return (events, action_result)
|
|
|
|
|
|
async def fetch_info_event(
|
|
client: Client,
|
|
uri: NostrWalletConnectUri,
|
|
) -> Event:
|
|
response_filter = Filter().kind(Kind(13194)).author(uri.public_key())
|
|
events = await client.fetch_events(
|
|
response_filter, timeout=timedelta(seconds=TIMEOUT)
|
|
)
|
|
start_time = datetime.now()
|
|
while events.len() < 1 and (datetime.now() - start_time) < timedelta(
|
|
seconds=TIMEOUT
|
|
):
|
|
time.sleep(1)
|
|
events = await client.fetch_events(
|
|
response_filter, timeout=timedelta(seconds=1)
|
|
)
|
|
assert events.len() == 1
|
|
|
|
return events.first()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_balance(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
node_balance = l1.rpc.call("listpeerchannels", {})["channels"][0]["spendable_msat"]
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
balance = await nwc.get_balance()
|
|
assert balance == 3000
|
|
|
|
uri_str = l1.rpc.call("nip47-create", ["test2"])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
balance = await nwc.get_balance()
|
|
assert balance == node_balance
|
|
|
|
uri_str = l1.rpc.call("nip47-create", ["test3", 0])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
balance = await nwc.get_balance()
|
|
assert balance == 0
|
|
|
|
with pytest.raises(RpcError, match="not an integer"):
|
|
uri_str = l1.rpc.call("nip47-create", ["test3", -1])["uri"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_info(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1 = node_factory.get_node(
|
|
options={
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
},
|
|
broken_log=r"Relay receiver exited with error|Connection failed",
|
|
)
|
|
node_get_info = l1.rpc.call("getinfo", {})
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
get_info = await nwc.get_info()
|
|
assert get_info.alias == node_get_info["alias"]
|
|
assert get_info.block_height == node_get_info["blockheight"]
|
|
assert get_info.color == node_get_info["color"]
|
|
assert get_info.methods == [
|
|
Method.MAKE_INVOICE,
|
|
Method.LOOKUP_INVOICE,
|
|
Method.LIST_TRANSACTIONS,
|
|
Method.GET_BALANCE,
|
|
Method.GET_INFO,
|
|
Method.PAY_INVOICE,
|
|
Method.MULTI_PAY_INVOICE,
|
|
Method.PAY_KEYSEND,
|
|
Method.MULTI_PAY_KEYSEND,
|
|
]
|
|
assert get_info.network == "regtest"
|
|
assert get_info.notifications == ["payment_received", "payment_sent"]
|
|
assert get_info.pubkey == node_get_info["id"]
|
|
|
|
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
|
|
l1.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": str(get_plugin),
|
|
"nip47-notifications": False,
|
|
},
|
|
)
|
|
l1.daemon.wait_for_log("All NWC's loaded")
|
|
time.sleep(5)
|
|
await client.connect()
|
|
info_event = await fetch_info_event(client, uri)
|
|
get_info = await nwc.get_info()
|
|
assert get_info.alias == node_get_info["alias"]
|
|
assert get_info.block_height == node_get_info["blockheight"]
|
|
assert get_info.color == node_get_info["color"]
|
|
assert get_info.methods == [
|
|
Method.MAKE_INVOICE,
|
|
Method.LOOKUP_INVOICE,
|
|
Method.LIST_TRANSACTIONS,
|
|
Method.GET_BALANCE,
|
|
Method.GET_INFO,
|
|
Method.PAY_INVOICE,
|
|
Method.MULTI_PAY_INVOICE,
|
|
Method.PAY_KEYSEND,
|
|
Method.MULTI_PAY_KEYSEND,
|
|
]
|
|
assert get_info.network == "regtest"
|
|
assert get_info.notifications == []
|
|
assert get_info.pubkey == node_get_info["id"]
|
|
|
|
assert (
|
|
info_event.content()
|
|
== "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
|
|
== "nip44_v2 nip04"
|
|
)
|
|
assert info_event.tags().find(TagKind.UNKNOWN("notifications")) is None
|
|
|
|
uri_str = l1.rpc.call("nip47-create", ["test2", 0])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
get_info = await nwc.get_info()
|
|
assert get_info.methods == [
|
|
Method.MAKE_INVOICE,
|
|
Method.LOOKUP_INVOICE,
|
|
Method.LIST_TRANSACTIONS,
|
|
Method.GET_BALANCE,
|
|
Method.GET_INFO,
|
|
]
|
|
|
|
info_event = await fetch_info_event(client, uri)
|
|
assert (
|
|
info_event.content()
|
|
== "make_invoice lookup_invoice list_transactions get_balance get_info"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
|
|
== "nip44_v2 nip04"
|
|
)
|
|
assert info_event.tags().find(TagKind.UNKNOWN("notifications")) is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_make_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1 = node_factory.get_node(
|
|
options={
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
},
|
|
broken_log=r"Relay receiver exited with error|Connection failed",
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
timestamp = int(time.time())
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3000, description="test1", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
node_invoice = l1.rpc.call("decode", [invoice.invoice])
|
|
assert invoice.payment_hash == node_invoice["payment_hash"]
|
|
assert node_invoice["amount_msat"] == invoice.amount
|
|
assert timestamp + node_invoice["expiry"] == pytest.approx(
|
|
invoice.expires_at.as_secs(), abs=3
|
|
)
|
|
assert node_invoice["created_at"] == pytest.approx(
|
|
invoice.created_at.as_secs(), abs=3
|
|
)
|
|
assert node_invoice["description"] == invoice.description
|
|
assert "description_hash" not in node_invoice
|
|
assert invoice.description_hash is None
|
|
|
|
timestamp = int(time.time())
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3001,
|
|
description="test2",
|
|
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
|
|
expiry=120,
|
|
)
|
|
)
|
|
node_invoice = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[
|
|
"invoices"
|
|
][0]
|
|
node_invoice_decode = l1.rpc.call("decode", [invoice.invoice])
|
|
assert invoice.payment_hash == node_invoice["payment_hash"]
|
|
assert node_invoice["amount_msat"] == invoice.amount
|
|
assert timestamp + node_invoice_decode["expiry"] == pytest.approx(
|
|
invoice.expires_at.as_secs(), abs=3
|
|
)
|
|
assert node_invoice_decode["created_at"] == pytest.approx(
|
|
invoice.created_at.as_secs(), abs=3
|
|
)
|
|
assert node_invoice["description"] == invoice.description
|
|
assert node_invoice_decode["description_hash"] == invoice.description_hash
|
|
|
|
with pytest.raises(
|
|
NostrSdkError.Generic, match="Must have description when using description_hash"
|
|
):
|
|
await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3001,
|
|
description=None,
|
|
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
|
|
expiry=120,
|
|
)
|
|
)
|
|
with pytest.raises(
|
|
NostrSdkError.Generic, match="description_hash not matching description"
|
|
):
|
|
await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3001,
|
|
description="test1",
|
|
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
|
|
expiry=120,
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pay_keysend(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2, l3 = node_factory.line_graph(
|
|
3,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
result = await nwc.pay_keysend(
|
|
PayKeysendRequest(
|
|
id="id123", amount=1000, pubkey=l3.info["id"], preimage=None, tlv_records=[]
|
|
)
|
|
)
|
|
pay = l1.rpc.call("listpays", {})["pays"][0]
|
|
assert result.preimage == pay["preimage"]
|
|
assert result.fees_paid == pay["amount_sent_msat"] - pay["amount_msat"]
|
|
assert result.fees_paid == 1
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_keysend(
|
|
PayKeysendRequest(
|
|
id="id123",
|
|
amount=2001,
|
|
pubkey=l2.info["id"],
|
|
preimage=None,
|
|
tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")],
|
|
)
|
|
)
|
|
with pytest.raises(
|
|
NostrSdkError.Generic, match="CLN generates the preimage itself"
|
|
):
|
|
await nwc.pay_keysend(
|
|
PayKeysendRequest(
|
|
id="id123",
|
|
amount=2001,
|
|
pubkey=l2.info["id"],
|
|
preimage="or3ijro3ijroi",
|
|
tlv_records=[KeysendTlvRecord(tlv_type=1234, value="a5c7e3d9b")],
|
|
)
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multi_keysend(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2, l3 = node_factory.line_graph(
|
|
3,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_res = l1.rpc.call("nip47-create", ["test1", 3010])
|
|
uri_str = uri_res["uri"]
|
|
client_pubkey = PublicKey.parse(uri_res["clientkey_public"])
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
content = {
|
|
"method": "multi_pay_keysend",
|
|
"params": {
|
|
"keysends": [
|
|
{"id": "4da52c32a1", "pubkey": l2.info["id"], "amount": 1000},
|
|
{"id": "3da52c32a1", "pubkey": l3.info["id"], "amount": 2000},
|
|
],
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
(responses1, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(event), 2
|
|
)
|
|
|
|
content = {
|
|
"method": "multi_pay_keysend",
|
|
"params": {
|
|
"keysends": [
|
|
{"id": "5da52c32a1", "pubkey": l2.info["id"], "amount": 5},
|
|
{"id": "2da52c32a1", "pubkey": l3.info["id"], "amount": 5},
|
|
],
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
(responses2, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(event), 2
|
|
)
|
|
|
|
reponses = responses1 + responses2
|
|
|
|
error_events = []
|
|
success_events = []
|
|
for event in reponses:
|
|
LOGGER.info(event)
|
|
assert event.tags().find(
|
|
TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.D))
|
|
)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
if "result" in content and content["result"] is not None:
|
|
success_events.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
error_events.append(content)
|
|
|
|
assert len(success_events) == 3
|
|
assert len(error_events) == 1
|
|
for content in success_events:
|
|
assert content["result_type"] == "multi_pay_keysend"
|
|
assert content["result"]["preimage"] is not None
|
|
for content in error_events:
|
|
assert content["result_type"] == "multi_pay_keysend"
|
|
assert content["error"]["message"] == "Payment exceeds budget!"
|
|
assert content["error"]["code"] == "QUOTA_EXCEEDED"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lookup_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2, l3 = node_factory.line_graph(
|
|
3,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
l1.rpc.call(
|
|
"pay",
|
|
{
|
|
"bolt11": l2.rpc.call(
|
|
"invoice",
|
|
{
|
|
"amount_msat": 500000000,
|
|
"label": generate_random_label(),
|
|
"description": "balancechannel",
|
|
},
|
|
)["bolt11"]
|
|
},
|
|
)
|
|
wait_for(
|
|
lambda: (
|
|
l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][
|
|
"spendable_msat"
|
|
]
|
|
> 3001
|
|
)
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3000, description="test1", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
|
|
with pytest.raises(
|
|
NostrSdkError.Generic, match="Neither invoice nor payment_hash given"
|
|
):
|
|
await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=None,
|
|
)
|
|
)
|
|
|
|
listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[
|
|
"invoices"
|
|
][0]
|
|
invoice_decode = l1.rpc.call("decode", [invoice.invoice])
|
|
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=invoice.payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert invoice_lookup.invoice == invoice.invoice
|
|
assert invoice_lookup.amount == 3000
|
|
assert invoice_lookup.description == "test1"
|
|
assert invoice_lookup.created_at.as_secs() == pytest.approx(
|
|
invoice_decode["created_at"], abs=3
|
|
)
|
|
assert invoice_lookup.description_hash is None
|
|
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
|
|
listpays_rpc["expires_at"], abs=3
|
|
)
|
|
assert invoice_lookup.fees_paid == 0
|
|
assert invoice_lookup.metadata is None
|
|
assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"]
|
|
assert invoice_lookup.transaction_type.name == "INCOMING"
|
|
assert invoice_lookup.state.name == "PENDING"
|
|
assert invoice_lookup.settled_at is None
|
|
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice.invoice,
|
|
)
|
|
)
|
|
assert invoice_lookup.invoice == invoice.invoice
|
|
assert invoice_lookup.amount == 3000
|
|
assert invoice_lookup.description == "test1"
|
|
assert invoice_lookup.created_at.as_secs() == pytest.approx(
|
|
invoice_decode["created_at"], abs=3
|
|
)
|
|
assert invoice_lookup.description_hash is None
|
|
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
|
|
listpays_rpc["expires_at"], abs=3
|
|
)
|
|
assert invoice_lookup.fees_paid == 0
|
|
assert invoice_lookup.metadata is None
|
|
assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"]
|
|
assert invoice_lookup.transaction_type.name == "INCOMING"
|
|
assert invoice_lookup.state.name == "PENDING"
|
|
assert invoice_lookup.settled_at is None
|
|
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3001,
|
|
description="test2",
|
|
description_hash=hashlib.sha256("test2".encode()).hexdigest(),
|
|
expiry=1000,
|
|
)
|
|
)
|
|
|
|
listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[
|
|
"invoices"
|
|
][0]
|
|
invoice_decode = l1.rpc.call("decode", [invoice.invoice])
|
|
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=invoice.payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert invoice_lookup.invoice == invoice.invoice
|
|
assert invoice_lookup.amount == 3001
|
|
assert invoice_lookup.description is None
|
|
assert invoice_lookup.created_at.as_secs() == pytest.approx(
|
|
invoice_decode["created_at"], abs=3
|
|
)
|
|
assert (
|
|
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
|
|
)
|
|
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
|
|
listpays_rpc["expires_at"], abs=3
|
|
)
|
|
assert invoice_lookup.fees_paid == 0
|
|
assert invoice_lookup.metadata is None
|
|
assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"]
|
|
assert invoice_lookup.transaction_type.name == "INCOMING"
|
|
assert invoice_lookup.state.name == "PENDING"
|
|
assert invoice_lookup.settled_at is None
|
|
|
|
l2.rpc.call("pay", {"bolt11": invoice.invoice})
|
|
listpays_rpc = l1.rpc.call("listinvoices", {"invstring": invoice.invoice})[
|
|
"invoices"
|
|
][0]
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=invoice.payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert invoice_lookup.invoice == invoice.invoice
|
|
assert invoice_lookup.amount == 3001
|
|
assert invoice_lookup.description is None
|
|
assert invoice_lookup.created_at.as_secs() == pytest.approx(
|
|
invoice_decode["created_at"], abs=3
|
|
)
|
|
assert (
|
|
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
|
|
)
|
|
assert invoice_lookup.expires_at.as_secs() == pytest.approx(
|
|
listpays_rpc["expires_at"], abs=3
|
|
)
|
|
assert invoice_lookup.fees_paid == 0
|
|
assert invoice_lookup.metadata is None
|
|
assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"]
|
|
assert invoice_lookup.transaction_type.name == "INCOMING"
|
|
assert invoice_lookup.state.name == "SETTLED"
|
|
assert invoice_lookup.settled_at.as_secs() == pytest.approx(
|
|
listpays_rpc["paid_at"], abs=3
|
|
)
|
|
|
|
invoice = l3.rpc.call(
|
|
"invoice",
|
|
{
|
|
"amount_msat": 4000,
|
|
"label": generate_random_label(),
|
|
"description": "outgoing",
|
|
},
|
|
)
|
|
invoice_decode = l3.rpc.call("decode", [invoice["bolt11"]])
|
|
pay = l1.rpc.call("pay", {"bolt11": invoice["bolt11"]})
|
|
listpays_rpc = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0]
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=pay["payment_hash"],
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert invoice_lookup.invoice == invoice["bolt11"]
|
|
assert invoice_lookup.amount == 4000
|
|
assert invoice_lookup.description == "outgoing"
|
|
assert invoice_lookup.created_at.as_secs() == pytest.approx(
|
|
invoice_decode["created_at"], abs=3
|
|
)
|
|
assert invoice_lookup.description_hash is None
|
|
assert invoice_lookup.expires_at is None
|
|
assert invoice_lookup.fees_paid == 1
|
|
assert invoice_lookup.metadata is None
|
|
assert invoice_lookup.payment_hash == listpays_rpc["payment_hash"]
|
|
assert invoice_lookup.transaction_type.name == "OUTGOING"
|
|
assert invoice_lookup.state.name == "SETTLED"
|
|
assert invoice_lookup.settled_at.as_secs() == pytest.approx(
|
|
listpays_rpc["completed_at"], abs=3
|
|
)
|
|
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=0, description="test_0_amt", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
invoice_lookup = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=invoice.payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert invoice_lookup.amount == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_transactions(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
l1.rpc.call(
|
|
"pay",
|
|
{
|
|
"bolt11": l2.rpc.call(
|
|
"invoice",
|
|
{
|
|
"amount_msat": 500000000,
|
|
"label": generate_random_label(),
|
|
"description": "balancechannel",
|
|
},
|
|
)["bolt11"]
|
|
},
|
|
)
|
|
wait_for(
|
|
lambda: (
|
|
l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][
|
|
"spendable_msat"
|
|
]
|
|
> 30001
|
|
)
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"]
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
for i in range(10):
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{
|
|
"label": generate_random_label(),
|
|
"description": "test1",
|
|
"amount_msat": 3000,
|
|
},
|
|
)
|
|
result = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
assert result.preimage is not None
|
|
for i in range(10):
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3000, description="test2", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
result = l2.rpc.call("pay", [invoice.invoice])
|
|
|
|
invoice = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=0, description="test_0_amt", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
result = l2.rpc.call("pay", [invoice.invoice, 1111])
|
|
|
|
result = await nwc.list_transactions(
|
|
ListTransactionsRequest(
|
|
_from=None,
|
|
until=None,
|
|
limit=None,
|
|
offset=None,
|
|
unpaid=None,
|
|
transaction_type=None,
|
|
)
|
|
)
|
|
assert len(result) == 22
|
|
for tx in result:
|
|
tx.description is not None
|
|
tx.invoice is not None
|
|
tx.amount is not None
|
|
tx.created_at is not None
|
|
tx.description_hash is None
|
|
tx.expires_at is None
|
|
tx.preimage is not None
|
|
tx.settled_at is not None
|
|
tx.metadata is None
|
|
tx.transaction_type is not None
|
|
tx.state is not None
|
|
tx.payment_hash is not None
|
|
tx.fees_paid is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_notifications(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2, l3 = node_factory.line_graph(
|
|
3,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
{"log-level": "debug", "plugin": get_plugin, "nip47-relays": url},
|
|
],
|
|
)
|
|
uri_res = l1.rpc.call("nip47-create", ["test1"])
|
|
uri_str = uri_res["uri"]
|
|
client_pubkey = PublicKey.parse(uri_res["clientkey_public"])
|
|
LOGGER.info(uri_str)
|
|
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
|
|
invoice = l3.rpc.call(
|
|
"invoice",
|
|
{
|
|
"label": generate_random_label(),
|
|
"description": "test1",
|
|
"amount_msat": 500000000,
|
|
},
|
|
)
|
|
|
|
(responses1, pay1) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
lambda: l1.rpc.call("pay", [invoice["bolt11"]]),
|
|
1,
|
|
)
|
|
|
|
invoice1_rpc = l3.rpc.call("listinvoices", {"invstring": invoice["bolt11"]})[
|
|
"invoices"
|
|
][0]
|
|
invoice1_decode = l3.rpc.call("decode", [invoice["bolt11"]])
|
|
pay1_list = l1.rpc.call("listpays", {"bolt11": invoice["bolt11"]})["pays"][0]
|
|
|
|
wait_for(
|
|
lambda: (
|
|
l2.rpc.call("listpeerchannels", [l1.info["id"]])["channels"][0][
|
|
"spendable_msat"
|
|
]
|
|
> 3000
|
|
)
|
|
)
|
|
wait_for(
|
|
lambda: (
|
|
l3.rpc.call("listpeerchannels", [l2.info["id"]])["channels"][0][
|
|
"spendable_msat"
|
|
]
|
|
> 3000
|
|
)
|
|
)
|
|
|
|
result = await nwc.make_invoice(
|
|
MakeInvoiceRequest(
|
|
amount=3000, description="test2", description_hash=None, expiry=None
|
|
)
|
|
)
|
|
(responses2, pay2) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
lambda: l3.rpc.call("pay", [result.invoice]),
|
|
1,
|
|
)
|
|
invoice2_list = l1.rpc.call("listinvoices", {"invstring": result.invoice})[
|
|
"invoices"
|
|
][0]
|
|
invoice2_decode = l3.rpc.call("decode", [result.invoice])
|
|
|
|
responses = responses1 + responses2
|
|
LOGGER.info(f"response1: {responses1} response2: {responses2}")
|
|
assert len(responses) == 2
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
received_events = []
|
|
sent_events = []
|
|
for event in responses:
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if content["notification_type"] == "payment_received":
|
|
received_events.append(content)
|
|
if content["notification_type"] == "payment_sent":
|
|
sent_events.append(content)
|
|
assert content["notification"]["preimage"] is not None
|
|
assert len(received_events) == 1
|
|
assert len(sent_events) == 1
|
|
assert received_events[0]["notification"]["type"] == "incoming"
|
|
assert received_events[0]["notification"]["invoice"] == result.invoice
|
|
assert received_events[0]["notification"]["description"] == "test2"
|
|
assert "description_hash" not in received_events[0]["notification"]
|
|
assert received_events[0]["notification"]["preimage"] == pay2["payment_preimage"]
|
|
assert received_events[0]["notification"]["payment_hash"] == pay2["payment_hash"]
|
|
assert received_events[0]["notification"]["amount"] == 3000
|
|
assert received_events[0]["notification"]["fees_paid"] == 0
|
|
assert received_events[0]["notification"]["created_at"] == pytest.approx(
|
|
invoice2_decode["created_at"], abs=3
|
|
)
|
|
assert "expires_at" not in received_events[0]["notification"]
|
|
assert received_events[0]["notification"]["settled_at"] == pytest.approx(
|
|
invoice2_list["paid_at"], abs=3
|
|
)
|
|
assert "metadata" not in received_events[0]["notification"]
|
|
|
|
assert sent_events[0]["notification"]["type"] == "outgoing"
|
|
assert sent_events[0]["notification"]["invoice"] == invoice["bolt11"]
|
|
assert sent_events[0]["notification"]["description"] == "test1"
|
|
assert "description_hash" not in sent_events[0]["notification"]
|
|
assert sent_events[0]["notification"]["preimage"] == pay1["payment_preimage"]
|
|
assert (
|
|
sent_events[0]["notification"]["payment_hash"] == invoice1_rpc["payment_hash"]
|
|
)
|
|
assert sent_events[0]["notification"]["amount"] == 500000000
|
|
assert sent_events[0]["notification"]["fees_paid"] == 5001
|
|
assert sent_events[0]["notification"]["created_at"] == pytest.approx(
|
|
invoice1_decode["created_at"], abs=3
|
|
)
|
|
assert "expires_at" not in sent_events[0]["notification"]
|
|
assert sent_events[0]["notification"]["settled_at"] == pytest.approx(
|
|
pay1_list["completed_at"], abs=3
|
|
)
|
|
assert "metadata" not in sent_events[0]["notification"]
|
|
|
|
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
|
|
l1.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": str(get_plugin),
|
|
"nip47-notifications": False,
|
|
},
|
|
)
|
|
l1.daemon.wait_for_log("All NWC's loaded")
|
|
time.sleep(3)
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
|
|
invoice = l3.rpc.call(
|
|
"invoice",
|
|
{
|
|
"label": generate_random_label(),
|
|
"description": "test3",
|
|
"amount_msat": 500,
|
|
},
|
|
)
|
|
with pytest.raises(AssertionError, match="0 == 1"):
|
|
(responses3, pay3) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
),
|
|
1,
|
|
6,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pay_invoice(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3001])["uri"]
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
LOGGER.info(uri_str)
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
|
|
result = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
pay = l1.rpc.call("listpays", {"payment_hash": invoice["payment_hash"]})["pays"][0]
|
|
assert result.preimage == pay["preimage"]
|
|
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test2", "amount_msat": 1},
|
|
)
|
|
with pytest.raises(NostrSdkError.Generic, match="unnecessary"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=1, invoice=invoice["bolt11"])
|
|
)
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test3", "amount_msat": 2},
|
|
)
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multi_pay(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_res = l1.rpc.call("nip47-create", ["test1", 30000])
|
|
uri_str = uri_res["uri"]
|
|
client_pubkey = PublicKey.parse(uri_res["clientkey_public"])
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
invoice1 = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
invoice2 = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test2", "amount_msat": 4000},
|
|
)
|
|
invoice3 = l2.rpc.call(
|
|
"invoice",
|
|
{
|
|
"label": generate_random_label(),
|
|
"description": "test3",
|
|
"amount_msat": 23001,
|
|
},
|
|
)
|
|
content = {
|
|
"method": "multi_pay_invoice",
|
|
"params": {
|
|
"invoices": [
|
|
{"id": "4da52c32a1", "invoice": invoice1["bolt11"]},
|
|
{"id": "3da52c32a1", "invoice": invoice2["bolt11"]},
|
|
{"id": "af3g2k2o11", "invoice": invoice3["bolt11"]},
|
|
],
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
encrypted_content = await signer.nip44_encrypt(uri.public_key(), content)
|
|
request_event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
|
|
(responses, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(request_event), 3
|
|
)
|
|
|
|
success_pays = []
|
|
error_pays = []
|
|
for response in responses:
|
|
d_tag = response.tags().find(
|
|
TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.D))
|
|
)
|
|
content = await signer.nip44_decrypt(uri.public_key(), response.content())
|
|
content = json.loads(content)
|
|
assert content["result_type"] == "multi_pay_invoice"
|
|
if "result" in content and content["result"] is not None:
|
|
assert d_tag is not None
|
|
assert content["result"]["preimage"] is not None
|
|
success_pays.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
assert d_tag.content() == "af3g2k2o11"
|
|
assert content["error"]["code"] == "QUOTA_EXCEEDED"
|
|
assert content["error"]["message"] == "Payment exceeds budget!"
|
|
error_pays.append(content)
|
|
assert len(success_pays) == 2
|
|
assert len(error_pays) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persistency(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
|
|
l1.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": str(get_plugin),
|
|
},
|
|
)
|
|
l1.daemon.wait_for_log("All NWC's loaded")
|
|
time.sleep(3)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
result = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
assert result.preimage is not None
|
|
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 1},
|
|
)
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
|
|
l1.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": str(get_plugin),
|
|
},
|
|
)
|
|
l1.daemon.wait_for_log("All NWC's loaded")
|
|
time.sleep(3)
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
|
|
revoke = l1.rpc.call("nip47-revoke", ["test1"])
|
|
assert revoke["revoked"] == "test1"
|
|
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000, "10sec"])["uri"]
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
invoice_exceeded = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
result = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
assert result.preimage is not None
|
|
|
|
list = l1.rpc.call("nip47-list", ["test1"])[0]
|
|
assert list["test1"]["budget_msat"] == 0
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"])
|
|
)
|
|
|
|
time.sleep(11)
|
|
|
|
list = l1.rpc.call("nip47-list", ["test1"])[0]
|
|
assert list["test1"]["budget_msat"] == 3000
|
|
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 3000},
|
|
)
|
|
result = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
assert result.preimage is not None
|
|
|
|
list = l1.rpc.call("nip47-list", ["test1"])[0]
|
|
assert list["test1"]["budget_msat"] == 0
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"])
|
|
)
|
|
|
|
l1.rpc.call("plugin", {"subcommand": "stop", "plugin": "cln-nip47"})
|
|
l1.rpc.call(
|
|
"plugin",
|
|
{
|
|
"subcommand": "start",
|
|
"plugin": str(get_plugin),
|
|
},
|
|
)
|
|
l1.daemon.wait_for_log("All NWC's loaded")
|
|
time.sleep(3)
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice_exceeded["bolt11"])
|
|
)
|
|
|
|
time.sleep(11)
|
|
|
|
list = l1.rpc.call("nip47-list", ["test1"])[0]
|
|
assert list["test1"]["budget_msat"] == 3000
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_budget_command(nostr_relay, node_factory, get_plugin): # noqa: F811
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"nip47-relays": url,
|
|
"broken_log": r"Relay receiver exited with error|Connection failed",
|
|
},
|
|
{"log-level": "debug"},
|
|
],
|
|
)
|
|
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
|
|
LOGGER.info(uri_str)
|
|
invoice = l2.rpc.call(
|
|
"invoice",
|
|
{"label": generate_random_label(), "description": "test1", "amount_msat": 5000},
|
|
)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
client = Client(signer)
|
|
await client.add_relay(RelayUrl.parse(url))
|
|
await client.connect()
|
|
await fetch_info_event(client, uri)
|
|
nwc = Nwc(uri)
|
|
balance = await nwc.get_balance()
|
|
assert balance == 3000
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
|
|
l1.rpc.call("nip47-budget", ["test1", 4000])
|
|
balance = await nwc.get_balance()
|
|
assert balance == 4000
|
|
|
|
with pytest.raises(NostrSdkError.Generic, match="Payment exceeds budget"):
|
|
await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
|
|
l1.rpc.call("nip47-budget", ["test1", 5000, "15s"])
|
|
balance = await nwc.get_balance()
|
|
assert balance == 5000
|
|
|
|
with pytest.raises(
|
|
RpcError, match="`budget_msat` must be greater than 0 if you use `interval`"
|
|
):
|
|
l1.rpc.call("nip47-budget", ["test1", 0, "1s"])
|
|
|
|
pay = await nwc.pay_invoice(
|
|
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
|
|
)
|
|
assert pay.preimage is not None
|
|
|
|
balance = await nwc.get_balance()
|
|
assert balance == 0
|
|
|
|
get_info = await nwc.get_info()
|
|
assert get_info.methods == [
|
|
Method.MAKE_INVOICE,
|
|
Method.LOOKUP_INVOICE,
|
|
Method.LIST_TRANSACTIONS,
|
|
Method.GET_BALANCE,
|
|
Method.GET_INFO,
|
|
Method.PAY_INVOICE,
|
|
Method.MULTI_PAY_INVOICE,
|
|
Method.PAY_KEYSEND,
|
|
Method.MULTI_PAY_KEYSEND,
|
|
]
|
|
|
|
info_event = await fetch_info_event(client, uri)
|
|
|
|
assert (
|
|
info_event.content()
|
|
== "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend notifications"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
|
|
== "nip44_v2 nip04"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("notifications")).content()
|
|
== "payment_received payment_sent"
|
|
)
|
|
|
|
time.sleep(18)
|
|
|
|
balance = await nwc.get_balance()
|
|
assert balance == 5000
|
|
|
|
l1.rpc.call("nip47-budget", ["test1", 0])
|
|
balance = await nwc.get_balance()
|
|
assert balance == 0
|
|
|
|
get_info = await nwc.get_info()
|
|
assert get_info.methods == [
|
|
Method.MAKE_INVOICE,
|
|
Method.LOOKUP_INVOICE,
|
|
Method.LIST_TRANSACTIONS,
|
|
Method.GET_BALANCE,
|
|
Method.GET_INFO,
|
|
]
|
|
|
|
info_event = await fetch_info_event(client, uri)
|
|
assert (
|
|
info_event.content()
|
|
== "make_invoice lookup_invoice list_transactions get_balance get_info notifications"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
|
|
== "nip44_v2 nip04"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("notifications")).content()
|
|
== "payment_received payment_sent"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hold_invoice(
|
|
node_factory,
|
|
executor,
|
|
get_plugin, # noqa: F811
|
|
get_hold, # noqa: F811
|
|
nostr_relay,
|
|
):
|
|
url = nostr_relay
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
wait_for_announce=True,
|
|
opts=[
|
|
{
|
|
"log-level": "debug",
|
|
"may_reconnect": True,
|
|
},
|
|
{
|
|
"log-level": "debug",
|
|
"plugin": get_plugin,
|
|
"important-plugin": get_hold,
|
|
"nip47-relays": url,
|
|
"may_reconnect": True,
|
|
},
|
|
],
|
|
)
|
|
uri_res = l2.rpc.call("nip47-create", ["test1", 3010])
|
|
uri_str = uri_res["uri"]
|
|
client_pubkey = PublicKey.parse(uri_res["clientkey_public"])
|
|
LOGGER.info(uri_str)
|
|
uri = NostrWalletConnectUri.parse(uri_str)
|
|
|
|
nwc = Nwc(uri)
|
|
|
|
preimage = secrets.token_hex(32)
|
|
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
|
LOGGER.info(f"preimage: {preimage}")
|
|
LOGGER.info(f"payment_hash: {payment_hash}")
|
|
content = {
|
|
"method": "make_hold_invoice",
|
|
"params": {
|
|
"amount": 5000,
|
|
"payment_hash": payment_hash,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
client = Client(signer)
|
|
relay_url = RelayUrl.parse(url)
|
|
await client.add_relay(relay_url)
|
|
await client.connect()
|
|
|
|
await fetch_info_event(client, uri)
|
|
|
|
(responses1, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(event), 1
|
|
)
|
|
error_events = []
|
|
success_events = []
|
|
for event in responses1:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if "result" in content and content["result"] is not None:
|
|
success_events.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
error_events.append(content)
|
|
|
|
assert len(success_events) == 1
|
|
assert len(error_events) == 0
|
|
|
|
assert success_events[0]["result_type"] == "make_hold_invoice"
|
|
assert success_events[0]["result"]["payment_hash"] == payment_hash
|
|
assert success_events[0]["result"]["type"] == "incoming"
|
|
assert success_events[0]["result"]["invoice"] is not None
|
|
invoice1 = success_events[0]["result"]["invoice"]
|
|
assert "description" not in success_events[0]["result"]
|
|
assert "description_hash" not in success_events[0]["result"]
|
|
assert success_events[0]["result"]["amount"] == 5000
|
|
invoice1_created_at = pytest.approx(int(time.time()), abs=1)
|
|
assert success_events[0]["result"]["created_at"] == invoice1_created_at
|
|
invoice1_expires_at = pytest.approx(int(time.time()) + 3600, abs=1)
|
|
assert success_events[0]["result"]["expires_at"] == invoice1_expires_at
|
|
assert "metadata" not in success_events[0]["result"]
|
|
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert lookup_hold.invoice == success_events[0]["result"]["invoice"]
|
|
assert lookup_hold.amount == 5000
|
|
assert lookup_hold.description is None
|
|
assert lookup_hold.created_at.as_secs() == success_events[0]["result"]["created_at"]
|
|
assert lookup_hold.description_hash is None
|
|
assert lookup_hold.expires_at.as_secs() == success_events[0]["result"]["expires_at"]
|
|
assert lookup_hold.fees_paid == 0
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage is None
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "PENDING"
|
|
assert lookup_hold.settled_at is None
|
|
|
|
(responses2, _res) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
lambda: executor.submit(
|
|
l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]]
|
|
),
|
|
1,
|
|
)
|
|
hold_events = []
|
|
for event in responses2:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if content["notification_type"] == "hold_invoice_accepted":
|
|
hold_events.append(content)
|
|
assert content["notification"]["payment_hash"] == payment_hash
|
|
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice1,
|
|
)
|
|
)
|
|
assert lookup_hold.invoice == invoice1
|
|
assert lookup_hold.amount == 5000
|
|
assert lookup_hold.description is None
|
|
assert lookup_hold.created_at.as_secs() == invoice1_created_at
|
|
assert lookup_hold.description_hash is None
|
|
assert lookup_hold.expires_at.as_secs() == invoice1_expires_at
|
|
assert lookup_hold.fees_paid == 0
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage is None
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "PENDING" # TODO ACCEPTED STATE
|
|
assert lookup_hold.settled_at is None
|
|
|
|
content = {
|
|
"method": "settle_hold_invoice",
|
|
"params": {
|
|
"preimage": preimage,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
|
|
(responses3, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(event), 1
|
|
)
|
|
error_events = []
|
|
success_events = []
|
|
for event in responses3:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if (
|
|
"result" in content
|
|
and content["result"] is not None
|
|
and content["result_type"] == "settle_hold_invoice"
|
|
):
|
|
success_events.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
error_events.append(content)
|
|
|
|
assert len(success_events) == 1
|
|
assert len(error_events) == 0
|
|
for content in success_events:
|
|
assert content["result_type"] == "settle_hold_invoice"
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice1,
|
|
)
|
|
)
|
|
assert lookup_hold.invoice == invoice1
|
|
assert lookup_hold.amount == 5000
|
|
assert lookup_hold.description is None
|
|
assert lookup_hold.created_at.as_secs() == invoice1_created_at
|
|
assert lookup_hold.description_hash is None
|
|
assert lookup_hold.expires_at.as_secs() == invoice1_expires_at
|
|
assert lookup_hold.fees_paid == 0
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage == preimage
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "SETTLED"
|
|
assert lookup_hold.settled_at.as_secs() == pytest.approx(
|
|
int(time.time()), abs=1
|
|
)
|
|
|
|
wait_for(
|
|
lambda: l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0][
|
|
"status"
|
|
]
|
|
== "complete"
|
|
)
|
|
|
|
preimage = secrets.token_hex(32)
|
|
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
|
content = {
|
|
"method": "make_hold_invoice",
|
|
"params": {
|
|
"amount": 5000,
|
|
"payment_hash": payment_hash,
|
|
"description": "cancel_hold",
|
|
"expiry": 1000,
|
|
"cltv_expiry_delta": 200,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
signer = NostrSigner.keys(Keys(uri.secret()))
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
|
|
(responses4, _res) = await fetch_event_responses(
|
|
client, client_pubkey, 23195, client.send_event(event), 1
|
|
)
|
|
error_events = []
|
|
success_events = []
|
|
for event in responses4:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if (
|
|
"result" in content
|
|
and content["result"] is not None
|
|
and content["result_type"] == "make_hold_invoice"
|
|
and content["result"]["payment_hash"] == payment_hash
|
|
):
|
|
success_events.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
error_events.append(content)
|
|
|
|
assert len(success_events) == 1
|
|
assert len(error_events) == 0
|
|
for content in success_events:
|
|
assert content["result_type"] == "make_hold_invoice"
|
|
assert content["result"]["payment_hash"] == payment_hash
|
|
invoice2 = content["result"]["invoice"]
|
|
invoice2_created_at = pytest.approx(int(time.time()), abs=1)
|
|
invoice2_expires_at = pytest.approx(int(time.time()) + 1000, abs=1)
|
|
|
|
(responses5, _res) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
lambda: executor.submit(
|
|
l1.rpc.call, "xpay", [success_events[0]["result"]["invoice"]]
|
|
),
|
|
1,
|
|
)
|
|
hold_events = []
|
|
for event in responses5:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if (
|
|
content["notification_type"] == "hold_invoice_accepted"
|
|
and content["notification"]["payment_hash"] == payment_hash
|
|
):
|
|
hold_events.append(content)
|
|
assert len(hold_events) == 1
|
|
|
|
content = {
|
|
"method": "cancel_hold_invoice",
|
|
"params": {
|
|
"payment_hash": payment_hash,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
|
|
(responses6, _res) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23195,
|
|
client.send_event(event),
|
|
1,
|
|
)
|
|
error_events = []
|
|
success_events = []
|
|
for event in responses6:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if (
|
|
"result" in content
|
|
and content["result"] is not None
|
|
and content["result_type"] == "cancel_hold_invoice"
|
|
):
|
|
success_events.append(content)
|
|
if "error" in content and content["error"] is not None:
|
|
error_events.append(content)
|
|
|
|
assert len(success_events) == 1
|
|
assert len(error_events) == 0
|
|
for content in success_events:
|
|
assert content["result_type"] == "cancel_hold_invoice"
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice2,
|
|
)
|
|
)
|
|
assert lookup_hold.invoice == invoice2
|
|
assert lookup_hold.amount == 5000
|
|
assert lookup_hold.description == "cancel_hold"
|
|
assert lookup_hold.created_at.as_secs() == invoice2_created_at
|
|
assert lookup_hold.description_hash is None
|
|
assert lookup_hold.expires_at.as_secs() == invoice2_expires_at
|
|
assert lookup_hold.fees_paid == 0
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage is None
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "EXPIRED"
|
|
assert lookup_hold.settled_at is None
|
|
|
|
invoice2_decoded = l1.rpc.call("decode", [invoice2])
|
|
assert invoice2_decoded["min_final_cltv_expiry"] == 200
|
|
|
|
wait_for(
|
|
lambda: l1.rpc.call("listpays", {"payment_hash": payment_hash})["pays"][0][
|
|
"status"
|
|
]
|
|
== "failed"
|
|
)
|
|
|
|
nwc = Nwc(uri)
|
|
|
|
invoice_lookup1 = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
invoice_lookup2 = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice2,
|
|
)
|
|
)
|
|
invoice_lookup3 = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=invoice2,
|
|
)
|
|
)
|
|
assert invoice_lookup1 == invoice_lookup2
|
|
assert invoice_lookup1 == invoice_lookup3
|
|
|
|
invoice_lookup4 = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=None,
|
|
invoice=invoice1,
|
|
)
|
|
)
|
|
|
|
result = await nwc.list_transactions(
|
|
ListTransactionsRequest(
|
|
_from=None,
|
|
until=None,
|
|
limit=None,
|
|
offset=None,
|
|
unpaid=True,
|
|
transaction_type=None,
|
|
)
|
|
)
|
|
assert len(result) == 2
|
|
assert result == [invoice_lookup1, invoice_lookup4] or result == [
|
|
invoice_lookup4,
|
|
invoice_lookup1,
|
|
]
|
|
|
|
description3 = "test3"
|
|
description_hash3 = hashlib.sha256(description3.encode()).hexdigest()
|
|
preimage = secrets.token_hex(32)
|
|
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
|
content = {
|
|
"method": "make_hold_invoice",
|
|
"params": {
|
|
"amount": 5001,
|
|
"payment_hash": payment_hash,
|
|
"description": description3,
|
|
"description_hash": description_hash3,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
await client.send_event(event)
|
|
|
|
start_time = datetime.now()
|
|
while (datetime.now() - start_time) < timedelta(seconds=10):
|
|
time.sleep(1)
|
|
try:
|
|
await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert lookup_hold.amount == 5001
|
|
assert lookup_hold.description is None
|
|
assert lookup_hold.description_hash == description_hash3
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage is None
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "PENDING"
|
|
assert lookup_hold.settled_at is None
|
|
|
|
description4 = "test4"
|
|
description_hash4 = hashlib.sha256(description4.encode()).hexdigest()
|
|
preimage = secrets.token_hex(32)
|
|
payment_hash = hashlib.sha256(bytes.fromhex(preimage)).hexdigest()
|
|
content = {
|
|
"method": "make_hold_invoice",
|
|
"params": {
|
|
"amount": 5002,
|
|
"payment_hash": payment_hash,
|
|
"description_hash": description_hash4,
|
|
},
|
|
}
|
|
content = json.dumps(content)
|
|
encrypted_content = await signer.nip04_encrypt(uri.public_key(), content)
|
|
event = (
|
|
await EventBuilder(Kind(23194), encrypted_content)
|
|
.tags([Tag.public_key(uri.public_key())])
|
|
.sign(signer)
|
|
)
|
|
await client.send_event(event)
|
|
|
|
start_time = datetime.now()
|
|
while (datetime.now() - start_time) < timedelta(seconds=10):
|
|
time.sleep(1)
|
|
try:
|
|
await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
lookup_hold = await nwc.lookup_invoice(
|
|
LookupInvoiceRequest(
|
|
payment_hash=payment_hash,
|
|
invoice=None,
|
|
)
|
|
)
|
|
assert lookup_hold.amount == 5002
|
|
assert lookup_hold.description is None
|
|
assert lookup_hold.description_hash == description_hash4
|
|
assert lookup_hold.metadata is None
|
|
assert lookup_hold.preimage is None
|
|
assert lookup_hold.payment_hash == payment_hash
|
|
assert lookup_hold.transaction_type.name == "INCOMING"
|
|
assert lookup_hold.state.name == "PENDING"
|
|
assert lookup_hold.settled_at is None
|
|
|
|
info_event = await fetch_info_event(client, uri)
|
|
assert (
|
|
info_event.content()
|
|
== "make_invoice lookup_invoice list_transactions get_balance get_info pay_invoice multi_pay_invoice pay_keysend multi_pay_keysend make_hold_invoice cancel_hold_invoice settle_hold_invoice notifications"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("encryption")).content()
|
|
== "nip44_v2 nip04"
|
|
)
|
|
assert (
|
|
info_event.tags().find(TagKind.UNKNOWN("notifications")).content()
|
|
== "payment_received payment_sent hold_invoice_accepted"
|
|
)
|
|
|
|
l2.restart()
|
|
|
|
l1.rpc.connect(l2.info["id"], "localhost", l2.port)
|
|
|
|
(responses7, _res) = await fetch_event_responses(
|
|
client,
|
|
client_pubkey,
|
|
23196,
|
|
lambda: executor.submit(l1.rpc.call, "xpay", [lookup_hold.invoice]),
|
|
1,
|
|
)
|
|
hold_events = []
|
|
for event in responses7:
|
|
LOGGER.info(event)
|
|
content = await signer.nip04_decrypt(uri.public_key(), event.content())
|
|
content = json.loads(content)
|
|
LOGGER.info(content)
|
|
if (
|
|
content["notification_type"] == "hold_invoice_accepted"
|
|
and content["notification"]["payment_hash"] == payment_hash
|
|
):
|
|
hold_events.append(content)
|
|
assert len(hold_events) == 1
|