Skip to content

Commit 3524c67

Browse files
committed
Add payment watch, unauthenticated invoice creation, LNURL support
- Add payments.watch() SSE stream for real-time payment events (settled/failed) - Add invoices.create_for_wallet() and create_for_address() for unauthenticated invoice creation - Add PaymentEvent, AddressInvoiceResponse types (sync + async) - Update payment docs to mention LNURL as accepted target - Bump version to 0.3.0 Made-with: Cursor
1 parent 7b29b7a commit 3524c67

4 files changed

Lines changed: 92 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "lnbot"
7-
version = "0.1.1"
7+
version = "0.3.0"
88
description = "Official Python SDK for LnBot — Bitcoin for AI Agents. Send and receive sats over Lightning with a few lines of code."
99
readme = "README.md"
1010
license = "MIT"

src/lnbot/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
UnauthorizedError,
1616
)
1717
from .types import (
18+
AddressInvoiceResponse,
1819
AddressResponse,
1920
ApiKeyResponse,
2021
BackupPasskeyBeginResponse,
@@ -23,6 +24,7 @@
2324
InvoiceEvent,
2425
InvoiceResponse,
2526
InvoiceStatus,
27+
PaymentEvent,
2628
PaymentResponse,
2729
PaymentStatus,
2830
RecoveryBackupResponse,
@@ -54,8 +56,10 @@
5456
"InvoiceResponse",
5557
"InvoiceStatus",
5658
"InvoiceEvent",
59+
"AddressInvoiceResponse",
5760
"PaymentResponse",
5861
"PaymentStatus",
62+
"PaymentEvent",
5963
"AddressResponse",
6064
"TransferAddressResponse",
6165
"TransactionResponse",

src/lnbot/client.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
_extract_message,
1919
)
2020
from .types import (
21+
AddressInvoiceResponse,
2122
AddressResponse,
2223
ApiKeyResponse,
2324
BackupPasskeyBeginResponse,
2425
CreateWalletResponse,
2526
CreateWebhookResponse,
2627
InvoiceEvent,
2728
InvoiceResponse,
29+
PaymentEvent,
2830
PaymentResponse,
2931
RecoveryBackupResponse,
3032
RecoveryRestoreResponse,
@@ -137,6 +139,16 @@ def get(self, number: int) -> InvoiceResponse:
137139
"""Get a single invoice by its number."""
138140
return parse(InvoiceResponse, self._c._get(f"/v1/invoices/{number}"))
139141

142+
def create_for_wallet(self, *, wallet_id: str, amount: int, reference: str | None = None, comment: str | None = None) -> AddressInvoiceResponse:
143+
"""Create an invoice for a specific wallet by ID. No authentication required."""
144+
body = to_camel({"wallet_id": wallet_id, "amount": amount, "reference": reference, "comment": comment})
145+
return parse(AddressInvoiceResponse, self._c._post("/v1/invoices/for-wallet", body))
146+
147+
def create_for_address(self, *, address: str, amount: int, tag: str | None = None, comment: str | None = None) -> AddressInvoiceResponse:
148+
"""Create an invoice for a Lightning address. No authentication required."""
149+
body = to_camel({"address": address, "amount": amount, "tag": tag, "comment": comment})
150+
return parse(AddressInvoiceResponse, self._c._post("/v1/invoices/for-address", body))
151+
140152
def watch(self, number: int, *, timeout: int | None = None) -> Iterator[InvoiceEvent]:
141153
"""Stream SSE events until the invoice is settled or expires."""
142154
params = _qs({"timeout": timeout})
@@ -161,13 +173,13 @@ def watch(self, number: int, *, timeout: int | None = None) -> Iterator[InvoiceE
161173

162174

163175
class PaymentsResource:
164-
"""Send sats to Lightning addresses or BOLT11 invoices."""
176+
"""Send sats to Lightning addresses, LNURLs, or BOLT11 invoices."""
165177

166178
def __init__(self, client: LnBot) -> None:
167179
self._c = client
168180

169181
def create(self, *, target: str, amount: int | None = None, idempotency_key: str | None = None, max_fee: int | None = None, reference: str | None = None) -> PaymentResponse:
170-
"""Send a payment to *target* (Lightning address or BOLT11 invoice)."""
182+
"""Send a payment to *target* (Lightning address, LNURL, or BOLT11 invoice)."""
171183
body = to_camel({"target": target, "amount": amount, "idempotency_key": idempotency_key, "max_fee": max_fee, "reference": reference})
172184
return parse(PaymentResponse, self._c._post("/v1/payments", body))
173185

@@ -179,6 +191,28 @@ def get(self, number: int) -> PaymentResponse:
179191
"""Get a single payment by its number."""
180192
return parse(PaymentResponse, self._c._get(f"/v1/payments/{number}"))
181193

194+
def watch(self, number: int, *, timeout: int | None = None) -> Iterator[PaymentEvent]:
195+
"""Stream SSE events until the payment settles or fails."""
196+
params = _qs({"timeout": timeout})
197+
headers = {"Accept": "text/event-stream", "User-Agent": _USER_AGENT}
198+
if self._c._api_key:
199+
headers["Authorization"] = f"Bearer {self._c._api_key}"
200+
with self._c._http.stream("GET", f"{self._c._base_url}/v1/payments/{number}/events", params=params, headers=headers) as resp:
201+
_raise_for_status(resp)
202+
event_type = ""
203+
for line in resp.iter_lines():
204+
if line.startswith("event:"):
205+
event_type = line[6:].strip()
206+
elif line.startswith("data:"):
207+
raw = line[5:].strip()
208+
if raw and event_type:
209+
try:
210+
data = parse(PaymentResponse, json.loads(raw))
211+
yield PaymentEvent(event=event_type, data=data) # type: ignore[arg-type]
212+
except (json.JSONDecodeError, TypeError):
213+
pass
214+
event_type = ""
215+
182216

183217
class AddressesResource:
184218
"""Lightning address management."""
@@ -397,6 +431,16 @@ async def get(self, number: int) -> InvoiceResponse:
397431
"""Get a single invoice by its number."""
398432
return parse(InvoiceResponse, await self._c._get(f"/v1/invoices/{number}"))
399433

434+
async def create_for_wallet(self, *, wallet_id: str, amount: int, reference: str | None = None, comment: str | None = None) -> AddressInvoiceResponse:
435+
"""Create an invoice for a specific wallet by ID. No authentication required."""
436+
body = to_camel({"wallet_id": wallet_id, "amount": amount, "reference": reference, "comment": comment})
437+
return parse(AddressInvoiceResponse, await self._c._post("/v1/invoices/for-wallet", body))
438+
439+
async def create_for_address(self, *, address: str, amount: int, tag: str | None = None, comment: str | None = None) -> AddressInvoiceResponse:
440+
"""Create an invoice for a Lightning address. No authentication required."""
441+
body = to_camel({"address": address, "amount": amount, "tag": tag, "comment": comment})
442+
return parse(AddressInvoiceResponse, await self._c._post("/v1/invoices/for-address", body))
443+
400444
async def watch(self, number: int, *, timeout: int | None = None) -> AsyncIterator[InvoiceEvent]:
401445
"""Stream SSE events until the invoice is settled or expires."""
402446
params = _qs({"timeout": timeout})
@@ -421,13 +465,13 @@ async def watch(self, number: int, *, timeout: int | None = None) -> AsyncIterat
421465

422466

423467
class AsyncPaymentsResource:
424-
"""Send sats to Lightning addresses or BOLT11 invoices (async)."""
468+
"""Send sats to Lightning addresses, LNURLs, or BOLT11 invoices (async)."""
425469

426470
def __init__(self, client: AsyncLnBot) -> None:
427471
self._c = client
428472

429473
async def create(self, *, target: str, amount: int | None = None, idempotency_key: str | None = None, max_fee: int | None = None, reference: str | None = None) -> PaymentResponse:
430-
"""Send a payment to *target* (Lightning address or BOLT11 invoice)."""
474+
"""Send a payment to *target* (Lightning address, LNURL, or BOLT11 invoice)."""
431475
body = to_camel({"target": target, "amount": amount, "idempotency_key": idempotency_key, "max_fee": max_fee, "reference": reference})
432476
return parse(PaymentResponse, await self._c._post("/v1/payments", body))
433477

@@ -439,6 +483,28 @@ async def get(self, number: int) -> PaymentResponse:
439483
"""Get a single payment by its number."""
440484
return parse(PaymentResponse, await self._c._get(f"/v1/payments/{number}"))
441485

486+
async def watch(self, number: int, *, timeout: int | None = None) -> AsyncIterator[PaymentEvent]:
487+
"""Stream SSE events until the payment settles or fails."""
488+
params = _qs({"timeout": timeout})
489+
headers = {"Accept": "text/event-stream", "User-Agent": _USER_AGENT}
490+
if self._c._api_key:
491+
headers["Authorization"] = f"Bearer {self._c._api_key}"
492+
async with self._c._http.stream("GET", f"{self._c._base_url}/v1/payments/{number}/events", params=params, headers=headers) as resp:
493+
_raise_for_status(resp)
494+
event_type = ""
495+
async for line in resp.aiter_lines():
496+
if line.startswith("event:"):
497+
event_type = line[6:].strip()
498+
elif line.startswith("data:"):
499+
raw = line[5:].strip()
500+
if raw and event_type:
501+
try:
502+
data = parse(PaymentResponse, json.loads(raw))
503+
yield PaymentEvent(event=event_type, data=data) # type: ignore[arg-type]
504+
except (json.JSONDecodeError, TypeError):
505+
pass
506+
event_type = ""
507+
442508

443509
class AsyncAddressesResource:
444510
"""Lightning address management (async)."""

src/lnbot/types.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ class InvoiceResponse:
7171
expires_at: str | None = None
7272

7373

74+
# ---------------------------------------------------------------------------
75+
# Invoices (unauthenticated)
76+
# ---------------------------------------------------------------------------
77+
78+
@dataclass(frozen=True)
79+
class AddressInvoiceResponse:
80+
bolt11: str
81+
amount: int
82+
expires_at: str | None = None
83+
84+
7485
# ---------------------------------------------------------------------------
7586
# Payments
7687
# ---------------------------------------------------------------------------
@@ -194,6 +205,12 @@ class InvoiceEvent:
194205
data: InvoiceResponse
195206

196207

208+
@dataclass(frozen=True)
209+
class PaymentEvent:
210+
event: Literal["settled", "failed"]
211+
data: PaymentResponse
212+
213+
197214
# ---------------------------------------------------------------------------
198215
# JSON key mapping (snake_case <-> camelCase)
199216
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)