cln-nip47/tests/test_clnnwc.py
daywalker90 2529bb5863
init
2025-04-02 17:38:52 +02:00

1090 lines
37 KiB
Python

#!/usr/bin/python
import hashlib
import importlib.resources as pkg_resources
import json
import logging
import socket
import subprocess
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from threading import Thread
import pytest
import pytest_asyncio
import yaml
from pyln.testing.fixtures import * # noqa: F403
from pyln.testing.utils import RpcError, wait_for
from util import generate_random_label, get_plugin # noqa: F401
if sys.version_info >= (3, 9):
from nostr_sdk import (
Alphabet,
Client,
EventBuilder,
Filter,
Keys,
KeysendTlvRecord,
Kind,
ListTransactionsRequest,
LookupInvoiceRequest,
MakeInvoiceRequest,
NostrSdkError,
NostrSigner,
NostrWalletConnectUri,
Nwc,
PayInvoiceRequest,
PayKeysendRequest,
SingleLetterTag,
Tag,
TagKind,
)
LOGGER = logging.getLogger(__name__)
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_get_balance(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"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)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
balance = await nwc.get_balance()
assert balance == 3000
uri_str = l1.rpc.call("nip47-create", ["test2"])["uri"]
LOGGER.info(uri_str)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
balance = await nwc.get_balance()
assert balance == node_balance
uri_str = l1.rpc.call("nip47-create", ["test3", 0])["uri"]
LOGGER.info(uri_str)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_get_info(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1 = node_factory.get_node(
options={
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
)
node_get_info = l1.rpc.call("getinfo", {})
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
LOGGER.info(uri_str)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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 == [
"pay_invoice",
"multi_pay_invoice",
"pay_keysend",
"multi_pay_keysend",
"make_invoice",
"lookup_invoice",
"list_transactions",
"get_balance",
"get_info",
]
assert get_info.network == "regtest"
assert get_info.notifications == ["payment_received", "payment_sent"]
assert get_info.pubkey == node_get_info["id"]
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_make_invoice(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1 = node_factory.get_node(
options={
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
)
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
LOGGER.info(uri_str)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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"] == 3000
assert node_invoice["expiry"] == 604800
assert node_invoice["description"] == "test1"
assert "description_hash" not in node_invoice
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"] == 3001
assert node_invoice_decode["expiry"] == 120
assert node_invoice["description"] == "test2"
assert (
node_invoice_decode["description_hash"]
== hashlib.sha256("test2".encode()).hexdigest()
)
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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_pay_keysend(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"log-level": "debug"},
],
)
uri_str = l1.rpc.call("nip47-create", ["test1", 3000])["uri"]
LOGGER.info(uri_str)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
result = await nwc.pay_keysend(
PayKeysendRequest(
id="id123", amount=1000, pubkey=l2.info["id"], preimage=None, tlv_records=[]
)
)
pay = l1.rpc.call("listpays", {})["pays"][0]
assert result.preimage == pay["preimage"]
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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_multi_keysend(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2, l3 = node_factory.line_graph(
3,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"log-level": "debug"},
{"log-level": "debug"},
],
)
uri_str = l1.rpc.call("nip47-create", ["test1", 3010])["uri"]
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(f"ws://{url}")
await client.connect()
await client.send_event(event)
content = {
"method": "multi_pay_keysend",
"params": {
"keysends": [
{"id": "4da52c32a1", "pubkey": l2.info["id"], "amount": 5},
{"id": "3da52c32a1", "pubkey": l3.info["id"], "amount": 5},
],
},
}
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(f"ws://{url}")
await client.connect()
await client.send_event(event)
response_filter = Filter().kind(Kind(23195)).author(uri.public_key())
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
start_time = datetime.now()
while events.len() < 4 and (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
events = await client.fetch_events(
response_filter, timeout=timedelta(seconds=1)
)
assert events.len() == 4
error_events = []
success_events = []
for event in events.to_vec():
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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_lookup_invoice(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2, l3 = node_factory.line_graph(
3,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"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)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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,
)
)
invoice_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() == invoice_decode["created_at"]
assert invoice_lookup.description_hash is None
assert invoice_lookup.expires_at.as_secs() == invoice_rpc["expires_at"]
assert invoice_lookup.fees_paid == 0
assert invoice_lookup.metadata is None
assert invoice_lookup.payment_hash == invoice_rpc["payment_hash"]
assert invoice_lookup.transaction_type.name == "INCOMING"
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() == invoice_decode["created_at"]
assert invoice_lookup.description_hash is None
assert invoice_lookup.expires_at.as_secs() == invoice_rpc["expires_at"]
assert invoice_lookup.fees_paid == 0
assert invoice_lookup.metadata is None
assert invoice_lookup.payment_hash == invoice_rpc["payment_hash"]
assert invoice_lookup.transaction_type.name == "INCOMING"
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,
)
)
invoice_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() == invoice_decode["created_at"]
assert (
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
)
assert invoice_lookup.expires_at.as_secs() == invoice_rpc["expires_at"]
assert invoice_lookup.fees_paid == 0
assert invoice_lookup.metadata is None
assert invoice_lookup.payment_hash == invoice_rpc["payment_hash"]
assert invoice_lookup.transaction_type.name == "INCOMING"
assert invoice_lookup.settled_at is None
l2.rpc.call("pay", {"bolt11": invoice.invoice})
invoice_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() == invoice_decode["created_at"]
assert (
invoice_lookup.description_hash == hashlib.sha256("test2".encode()).hexdigest()
)
assert invoice_lookup.expires_at.as_secs() == invoice_rpc["expires_at"]
assert invoice_lookup.fees_paid == 0
assert invoice_lookup.metadata is None
assert invoice_lookup.payment_hash == invoice_rpc["payment_hash"]
assert invoice_lookup.transaction_type.name == "INCOMING"
assert invoice_lookup.settled_at.as_secs() == invoice_rpc["paid_at"]
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"]})
invoice_rpc = l3.rpc.call("listinvoices", {"invstring": invoice["bolt11"]})[
"invoices"
][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() == invoice_decode["created_at"]
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 == invoice_rpc["payment_hash"]
assert invoice_lookup.transaction_type.name == "OUTGOING"
assert invoice_lookup.settled_at.as_secs() == invoice_rpc["paid_at"]
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_list_transactions(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"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)
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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])
result = await nwc.list_transactions(
ListTransactionsRequest(
_from=None,
until=None,
limit=None,
offset=None,
unpaid=None,
transaction_type=None,
)
)
assert len(result) == 21
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.payment_hash is not None
tx.fees_paid is not None
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_notifications(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2, l3 = node_factory.line_graph(
3,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"log-level": "debug"},
{"log-level": "debug"},
],
)
uri_str = l1.rpc.call("nip47-create", ["test1"])["uri"]
LOGGER.info(uri_str)
uri = NostrWalletConnectUri.parse(uri_str)
nwc = Nwc(uri)
invoice = l3.rpc.call(
"invoice",
{
"label": generate_random_label(),
"description": "test1",
"amount_msat": 500000000,
},
)
pay1 = await nwc.pay_invoice(
PayInvoiceRequest(id=None, amount=None, invoice=invoice["bolt11"])
)
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
)
)
pay2 = l3.rpc.call("pay", {"bolt11": result.invoice})
invoice2_list = l1.rpc.call("listinvoices", {"invstring": result.invoice})[
"invoices"
][0]
invoice2_decode = l3.rpc.call("decode", [result.invoice])
response_filter = Filter().kind(Kind(23196)).author(uri.public_key())
events = await nostr_client.fetch_events(
response_filter, timeout=timedelta(seconds=10)
)
start_time = datetime.now()
while events.len() < 2 and (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
events = await nostr_client.fetch_events(
response_filter, timeout=timedelta(seconds=1)
)
assert events.len() == 2
signer = NostrSigner.keys(Keys(uri.secret()))
received_events = []
sent_events = []
for event in events.to_vec():
LOGGER.info(event)
content = await signer.nip04_decrypt(uri.public_key(), event.content())
content = json.loads(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"]
== invoice2_decode["created_at"]
)
assert "expires_at" not in received_events[0]["notification"]
assert received_events[0]["notification"]["settled_at"] == invoice2_list["paid_at"]
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.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"] == invoice1_decode["created_at"]
assert "expires_at" not in sent_events[0]["notification"]
assert sent_events[0]["notification"]["settled_at"] == pay1_list["completed_at"]
assert "metadata" not in sent_events[0]["notification"]
@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_pay_invoice(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"log-level": "debug"},
],
)
uri_str = l1.rpc.call("nip47-create", ["test1", 3001])["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="msatoshi parameter 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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_multi_pay(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"log-level": "debug"},
],
)
uri_str = l1.rpc.call("nip47-create", ["test1", 30000])["uri"]
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)
event = (
await EventBuilder(Kind(23194), encrypted_content)
.tags([Tag.public_key(uri.public_key())])
.sign(signer)
)
client = Client(signer)
await client.add_relay(f"ws://{url}")
await client.connect()
await client.send_event(event)
response_filter = Filter().kind(Kind(23195)).author(uri.public_key())
events = await client.fetch_events(response_filter, timeout=timedelta(seconds=10))
start_time = datetime.now()
while events.len() < 3 and (datetime.now() - start_time) < timedelta(seconds=10):
time.sleep(1)
events = await client.fetch_events(
response_filter, timeout=timedelta(seconds=1)
)
assert events.len() == 3
success_pays = []
error_pays = []
for event in events.to_vec():
LOGGER.info(event)
d_tag = event.tags().find(
TagKind.SINGLE_LETTER(SingleLetterTag.lowercase(Alphabet.D))
)
content = await signer.nip44_decrypt(uri.public_key(), event.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.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher")
@pytest.mark.asyncio
async def test_persistency(node_factory, get_plugin, nostr_client): # noqa: F811
nostr_client, relay_port = nostr_client
url = f"127.0.0.1:{relay_port}"
l1, l2 = node_factory.line_graph(
2,
wait_for_announce=True,
opts=[
{
"log-level": "debug",
"plugin": get_plugin,
"nip47-relays": f"ws://{url}",
},
{"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")
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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")
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"]
nwc = Nwc(NostrWalletConnectUri.parse(uri_str))
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["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["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["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")
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["budget_msat"] == 3000
@pytest_asyncio.fixture(scope="function")
async def nostr_client(nostr_relay):
port = nostr_relay
keys = Keys.generate()
signer = NostrSigner.keys(keys)
client = Client(signer)
relay_url = f"ws://127.0.0.1:{port}"
await client.add_relay(relay_url)
await client.connect()
yield client, port
await client.disconnect()
@pytest_asyncio.fixture(scope="module")
async def nostr_relay(test_base_dir):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("127.0.0.1", 0))
dynamic_port = s.getsockname()[1]
s.close()
try:
config_file = pkg_resources.files("nostr_relay").joinpath("config.yaml")
except KeyError:
raise FileNotFoundError("config.yaml not found in the nostr package")
with open(config_file, "r") as file:
config = yaml.safe_load(file)
config["gunicorn"]["bind"] = f"127.0.0.1:{dynamic_port}"
config["authentication"]["valid_urls"] = [
f"ws://localhost:{dynamic_port}",
f"ws://127.0.0.1:{dynamic_port}",
]
sqlite_file = Path(test_base_dir) / "nostr.sqlite3"
config["storage"]["sqlalchemy.url"] = f"sqlite+aiosqlite:///{str(sqlite_file)}"
config["storage"]["validators"] = [
"nostr_relay.validators.is_signed",
"nostr_relay.validators.is_recent",
"nostr_relay.validators.is_not_hellthread",
]
config_file = Path(test_base_dir) / "config.yaml"
with open(config_file, "w") as file:
yaml.safe_dump(config, file)
LOGGER.info(f"{config_file}")
process = subprocess.Popen(
["nostr-relay", "-c", config_file, "serve"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout_thread = Thread(target=log_pipe, args=(process.stdout, LOGGER, logging.INFO))
stderr_thread = Thread(
target=log_pipe, args=(process.stderr, LOGGER, logging.ERROR)
)
stdout_thread.start()
stderr_thread.start()
time.sleep(2)
yield dynamic_port
process.terminate()
process.wait()
stdout_thread.join()
stderr_thread.join()
def log_pipe(pipe, logger, log_level):
while True:
line = pipe.readline()
if not line:
break
logger.log(log_level, line.strip())