Skip to content

Commit aaba124

Browse files
realbubclaude
andcommitted
v1.0.0: Wallet-scoped API, integration tests, SSE improvements
- Migrate to wallet-scoped API pattern: ln.wallet(id).resource.method() - Add Wallet/AsyncWallet with all sub-resources (key, invoices, payments, addresses, transactions, webhooks, events, l402) - Add register(), me(), WalletKeyResource, PublicInvoicesResource - Add payments.resolve() for target resolution - Add ResolveTargetResponse, RegisterResponse, MeResponse, WalletListItem, WalletKeyResponse, WalletKeyInfoResponse - Update CreateWalletResponse (remove keys), remove CreateWalletRequest - Split account-level vs wallet-level operations - Add 56 integration tests with real API calls - Exclude integration tests from CI/release workflows - Update README for v1.0.0 wallet-scoped API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d08fbdf commit aaba124

File tree

11 files changed

+1311
-406
lines changed

11 files changed

+1311
-406
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ jobs:
1919
python-version: ${{ matrix.python-version }}
2020

2121
- run: pip install -e ".[test]"
22-
- run: pytest tests/ -v
22+
- run: pytest tests/ -v -m "not integration"

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
python-version: "3.13"
2121

2222
- run: pip install -e ".[test]"
23-
- run: pytest tests/ -v
23+
- run: pytest tests/ -v -m "not integration"
2424

2525
- run: pip install build
2626
- run: python -m build

README.md

Lines changed: 85 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ Give your AI agents, apps, and services access to Bitcoin over the Lightning Net
1212
```python
1313
from lnbot import LnBot
1414

15-
ln = LnBot(api_key="key_...")
15+
ln = LnBot(api_key="uk_...")
16+
w = ln.wallet("wal_...")
1617

17-
invoice = ln.invoices.create(amount=1000, memo="Coffee")
18-
ln.payments.create(target="alice@ln.bot", amount=500)
18+
invoice = w.invoices.create(amount=1000, memo="Coffee")
1919
```
2020

21-
> ln.bot also ships a **[TypeScript SDK](https://www.npmjs.com/package/@lnbot/sdk)**, **[Go SDK](https://pkg.go.dev/github.com/lnbotdev/go-sdk)**, **[Rust SDK](https://crates.io/crates/lnbot)**, **[CLI](https://ln.bot/docs)**, and **[MCP server](https://ln.bot/docs)**.
21+
> ln.bot also ships a **[TypeScript SDK](https://www.npmjs.com/package/@lnbot/sdk)**, **[C# SDK](https://www.nuget.org/packages/LnBot)**, **[Go SDK](https://pkg.go.dev/github.com/lnbotdev/go-sdk)**, **[Rust SDK](https://crates.io/crates/lnbot)**, **[CLI](https://ln.bot/docs)**, and **[MCP server](https://ln.bot/docs)**.
2222
2323
---
2424

@@ -32,48 +32,90 @@ pip install lnbot
3232

3333
## Quick start
3434

35-
### 1. Create a wallet
35+
### Register an account
3636

3737
```python
3838
from lnbot import LnBot
3939

4040
ln = LnBot()
41-
wallet = ln.wallets.create(name="my-agent")
41+
account = ln.register()
42+
print(account.primary_key)
43+
print(account.recovery_passphrase)
44+
```
45+
46+
### Create a wallet
4247

43-
print(wallet.primary_key) # your API key
44-
print(wallet.address) # your Lightning address
45-
print(wallet.recovery_passphrase) # back this up!
48+
```python
49+
ln = LnBot(api_key=account.primary_key)
50+
wallet = ln.wallets.create()
51+
print(wallet.wallet_id)
4652
```
4753

48-
### 2. Receive sats
54+
### Receive sats
4955

5056
```python
51-
ln = LnBot(api_key=wallet.primary_key)
57+
w = ln.wallet(wallet.wallet_id)
5258

53-
invoice = ln.invoices.create(amount=1000, memo="Payment for task #42")
59+
invoice = w.invoices.create(amount=1000, memo="Payment for task #42")
5460
print(invoice.bolt11)
5561
```
5662

57-
### 3. Wait for payment
63+
### Wait for payment (SSE)
5864

5965
```python
60-
for event in ln.invoices.watch(invoice.number):
66+
for event in w.invoices.watch(invoice.number):
6167
if event.event == "settled":
6268
print("Paid!")
69+
break
70+
```
71+
72+
### Send sats
73+
74+
```python
75+
w.payments.create(target="alice@ln.bot", amount=500)
76+
```
77+
78+
### Check balance
79+
80+
```python
81+
info = w.get()
82+
print(f"{info.available} sats available")
6383
```
6484

65-
### 4. Send sats
85+
---
86+
87+
## Wallet-scoped API
88+
89+
All wallet operations go through a `Wallet` handle obtained via `ln.wallet(wallet_id)`:
6690

6791
```python
68-
ln.payments.create(target="alice@ln.bot", amount=500)
69-
ln.payments.create(target="lnbc10u1p...")
92+
w = ln.wallet("wal_abc123")
93+
94+
# Wallet info
95+
info = w.get()
96+
w.update(name="production")
97+
98+
# Sub-resources
99+
w.key # Wallet key management (wk_ keys)
100+
w.invoices # Create, list, get, watch invoices
101+
w.payments # Send, list, get, watch, resolve payments
102+
w.addresses # Create, list, delete, transfer Lightning addresses
103+
w.transactions # List transaction history
104+
w.webhooks # Create, list, delete webhook endpoints
105+
w.events # Real-time SSE event stream
106+
w.l402 # L402 paywall authentication
70107
```
71108

72-
### 5. Check balance
109+
Account-level operations stay on the client:
73110

74111
```python
75-
wallet = ln.wallets.current()
76-
print(f"{wallet.available} sats available")
112+
ln.register() # Register new account
113+
ln.me() # Get authenticated identity
114+
ln.wallets.create() # Create wallet
115+
ln.wallets.list() # List wallets
116+
ln.keys.rotate(0) # Rotate account key
117+
ln.invoices.create_for_wallet(...) # Public invoice by wallet ID
118+
ln.invoices.create_for_address(...) # Public invoice by address
77119
```
78120

79121
---
@@ -85,13 +127,15 @@ Every method has an async equivalent via `AsyncLnBot`:
85127
```python
86128
from lnbot import AsyncLnBot
87129

88-
async with AsyncLnBot(api_key="key_...") as ln:
89-
wallet = await ln.wallets.current()
90-
invoice = await ln.invoices.create(amount=1000)
130+
async with AsyncLnBot(api_key="uk_...") as ln:
131+
w = ln.wallet("wal_...")
132+
info = await w.get()
133+
invoice = await w.invoices.create(amount=1000)
91134

92-
async for event in ln.invoices.watch(invoice.number):
135+
async for event in w.invoices.watch(invoice.number):
93136
if event.event == "settled":
94137
print("Paid!")
138+
break
95139
```
96140

97141
---
@@ -102,7 +146,7 @@ async with AsyncLnBot(api_key="key_...") as ln:
102146
from lnbot import LnBot, BadRequestError, UnauthorizedError, NotFoundError, ConflictError, LnBotError
103147

104148
try:
105-
ln.payments.create(target="invalid", amount=100)
149+
w.payments.create(target="invalid", amount=100)
106150
except BadRequestError:
107151
... # 400
108152
except UnauthorizedError:
@@ -121,9 +165,9 @@ except LnBotError as e:
121165
from lnbot import LnBot
122166

123167
ln = LnBot(
124-
api_key="key_...", # or set LNBOT_API_KEY env var
125-
base_url="https://api.ln.bot", # optional — this is the default
126-
timeout=30.0, # optional — request timeout in seconds
168+
api_key="uk_...", # or set LNBOT_API_KEY env var
169+
base_url="https://api.ln.bot", # optional — this is the default
170+
timeout=30.0, # optional — request timeout in seconds
127171
)
128172
```
129173

@@ -133,98 +177,30 @@ The API key can also be provided via the `LNBOT_API_KEY` environment variable. I
133177

134178
## L402 paywalls
135179

136-
Monetize APIs with Lightning-native authentication:
137-
138180
```python
181+
w = ln.wallet("wal_...")
182+
139183
# Create a challenge (server side)
140-
challenge = ln.l402.create_challenge(amount=100, description="API access", expiry_seconds=3600)
184+
challenge = w.l402.create_challenge(amount=100, description="API access", expiry_seconds=3600)
141185

142186
# Pay the challenge (client side)
143-
result = ln.l402.pay(www_authenticate=challenge.www_authenticate)
187+
result = w.l402.pay(www_authenticate=challenge.www_authenticate)
144188

145189
# Verify a token (server side, stateless)
146-
v = ln.l402.verify(authorization=result.authorization)
190+
v = w.l402.verify(authorization=result.authorization)
147191
print(v.valid)
148192
```
149193

150194
---
151195

152-
## API reference
153-
154-
### Wallets
155-
156-
| Method | Description |
157-
| --- | --- |
158-
| `ln.wallets.create(name=)` | Create a new wallet (no auth required) |
159-
| `ln.wallets.current()` | Get current wallet info and balance |
160-
| `ln.wallets.update(name=)` | Update wallet name |
196+
## Features
161197

162-
### Invoices
163-
164-
| Method | Description |
165-
| --- | --- |
166-
| `ln.invoices.create(amount=, memo=, reference=)` | Create a BOLT11 invoice |
167-
| `ln.invoices.list(limit=, after=)` | List invoices |
168-
| `ln.invoices.get(number)` | Get invoice by number |
169-
| `ln.invoices.watch(number, timeout=)` | SSE stream for settlement/expiry |
170-
171-
### Payments
172-
173-
| Method | Description |
174-
| --- | --- |
175-
| `ln.payments.create(target=, amount=, ...)` | Send sats to a Lightning address or BOLT11 invoice |
176-
| `ln.payments.list(limit=, after=)` | List payments |
177-
| `ln.payments.get(number)` | Get payment by number |
178-
179-
### Addresses
180-
181-
| Method | Description |
182-
| --- | --- |
183-
| `ln.addresses.create(address=)` | Create a random or vanity Lightning address |
184-
| `ln.addresses.list()` | List all addresses |
185-
| `ln.addresses.delete(address)` | Delete an address |
186-
| `ln.addresses.transfer(address, target_wallet_key=)` | Transfer address to another wallet |
187-
188-
### Transactions
189-
190-
| Method | Description |
191-
| --- | --- |
192-
| `ln.transactions.list(limit=, after=)` | List credit and debit transactions |
193-
194-
### Webhooks
195-
196-
| Method | Description |
197-
| --- | --- |
198-
| `ln.webhooks.create(url=)` | Register a webhook endpoint (max 10) |
199-
| `ln.webhooks.list()` | List all webhooks |
200-
| `ln.webhooks.delete(webhook_id)` | Delete a webhook |
201-
202-
### API Keys
203-
204-
| Method | Description |
205-
| --- | --- |
206-
| `ln.keys.rotate(slot)` | Rotate a key (0 = primary, 1 = secondary) |
207-
208-
### L402
209-
210-
| Method | Description |
211-
| --- | --- |
212-
| `ln.l402.create_challenge(amount=, ...)` | Create an L402 challenge (invoice + macaroon) |
213-
| `ln.l402.verify(authorization=)` | Verify an L402 token (stateless) |
214-
| `ln.l402.pay(www_authenticate=, ...)` | Pay an L402 challenge, get Authorization header |
215-
216-
### Backup & Restore
217-
218-
| Method | Description |
219-
| --- | --- |
220-
| `ln.backup.recovery()` | Generate 12-word BIP-39 recovery passphrase |
221-
| `ln.backup.passkey_begin()` | Start passkey backup (WebAuthn) |
222-
| `ln.backup.passkey_complete(session_id=, attestation=)` | Complete passkey backup |
223-
| `ln.restore.recovery(passphrase=)` | Restore wallet with recovery passphrase |
224-
| `ln.restore.passkey_begin()` | Start passkey restore (WebAuthn) |
225-
| `ln.restore.passkey_complete(session_id=, assertion=)` | Complete passkey restore |
226-
227-
---
198+
- **Zero extra dependencies** — only `httpx`
199+
- **Wallet-scoped API**`ln.wallet(id)` returns a typed scope with all sub-resources
200+
- **Sync + async**`LnBot` and `AsyncLnBot` with identical APIs
201+
- **Typed exceptions**`BadRequestError`, `NotFoundError`, `ConflictError`, `UnauthorizedError`, `ForbiddenError`
202+
- **SSE support**`watch()` returns an iterator/async iterator for real-time events
203+
- **Dataclass responses** — all responses are frozen dataclasses
228204

229205
## Requirements
230206

@@ -241,6 +217,7 @@ print(v.valid)
241217
## Other SDKs
242218

243219
- [TypeScript SDK](https://github.com/lnbotdev/typescript-sdk) · [npm](https://www.npmjs.com/package/@lnbot/sdk)
220+
- [C# SDK](https://github.com/lnbotdev/csharp-sdk) · [NuGet](https://www.nuget.org/packages/LnBot)
244221
- [Go SDK](https://github.com/lnbotdev/go-sdk) · [pkg.go.dev](https://pkg.go.dev/github.com/lnbotdev/go-sdk)
245222
- [Rust SDK](https://github.com/lnbotdev/rust-sdk) · [crates.io](https://crates.io/crates/lnbot) · [docs.rs](https://docs.rs/lnbot)
246223

pyproject.toml

Lines changed: 2 additions & 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.5.0"
7+
version = "1.0.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"
@@ -57,6 +57,7 @@ Issues = "https://github.com/lnbotdev/python-sdk/issues"
5757

5858
[tool.pytest.ini_options]
5959
testpaths = ["tests"]
60+
markers = ["integration: integration tests that hit the live API"]
6061

6162
[tool.hatch.build.targets.wheel]
6263
packages = ["src/lnbot"]

src/lnbot/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
except PackageNotFoundError:
66
__version__ = "0.0.0"
77

8-
from .client import AsyncLnBot, LnBot
8+
from .client import AsyncLnBot, AsyncWallet, LnBot, Wallet
99
from .errors import (
1010
BadRequestError,
1111
ConflictError,
@@ -26,12 +26,18 @@
2626
L402ChallengeResponse,
2727
L402PayResponse,
2828
L402PaymentStatus,
29+
MeResponse,
2930
PaymentEvent,
3031
PaymentResponse,
3132
PaymentStatus,
33+
RegisterResponse,
34+
ResolveTargetResponse,
3235
VerifyL402Response,
3336
WalletEvent,
3437
WalletEventType,
38+
WalletKeyInfoResponse,
39+
WalletKeyResponse,
40+
WalletListItem,
3541
RecoveryBackupResponse,
3642
RecoveryRestoreResponse,
3743
RestorePasskeyBeginResponse,
@@ -48,14 +54,21 @@
4854
"__version__",
4955
"LnBot",
5056
"AsyncLnBot",
57+
"Wallet",
58+
"AsyncWallet",
5159
"LnBotError",
5260
"BadRequestError",
5361
"UnauthorizedError",
5462
"ForbiddenError",
5563
"NotFoundError",
5664
"ConflictError",
65+
"RegisterResponse",
66+
"MeResponse",
5767
"WalletResponse",
5868
"CreateWalletResponse",
69+
"WalletListItem",
70+
"WalletKeyResponse",
71+
"WalletKeyInfoResponse",
5972
"RotateApiKeyResponse",
6073
"InvoiceResponse",
6174
"InvoiceStatus",
@@ -64,6 +77,7 @@
6477
"PaymentResponse",
6578
"PaymentStatus",
6679
"PaymentEvent",
80+
"ResolveTargetResponse",
6781
"WalletEvent",
6882
"WalletEventType",
6983
"AddressResponse",

0 commit comments

Comments
 (0)