Skip to content

Commit ef0354b

Browse files
realbubclaude
andcommitted
v1.0.0: Wallet-scoped API, integration tests, SSE improvements
- Migrate to wallet-scoped API pattern: client.Wallet(id).Resource.Method() - Add WalletScope with all sub-resources (Key, Invoices, Payments, Addresses, Transactions, Webhooks, Events, L402) - Add RegisterAsync, MeAsync, WalletKeyResource - Add PublicInvoicesResource (CreateForWallet, CreateForAddress) - Add ResolveAsync for payment target resolution - Split account-level vs wallet-level operations - Add 56 integration tests with real API calls (ordered execution) - Fix SSE deserialization for heartbeat/empty data - 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 9905ebb commit ef0354b

23 files changed

Lines changed: 1624 additions & 291 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ jobs:
2626
run: dotnet build --no-restore --warnaserror
2727

2828
- name: Test
29-
run: dotnet test --no-build
29+
run: dotnet test --no-build --filter "Category!=Integration"

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
dotnet-version: 8.0.x
1919

2020
- name: Test
21-
run: dotnet test -c Release
21+
run: dotnet test -c Release --filter "Category!=Integration"
2222

2323
- name: Pack
2424
run: dotnet pack src/LnBot/LnBot.csproj -c Release -o ./nupkg

README.md

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ Give your AI agents, apps, and services access to Bitcoin over the Lightning Net
1111
using LnBot;
1212
using LnBot.Models;
1313

14-
using var client = new LnBotClient("key_...");
14+
using var client = new LnBotClient("uk_...");
15+
var w = client.Wallet("wal_...");
1516

16-
var invoice = await client.Invoices.CreateAsync(new CreateInvoiceRequest
17+
var invoice = await w.Invoices.CreateAsync(new CreateInvoiceRequest
1718
{
1819
Amount = 1000,
1920
Memo = "Coffee",
@@ -34,27 +35,31 @@ dotnet add package LnBot
3435

3536
## Quick start
3637

37-
### Create a wallet
38+
### Register an account
3839

3940
```csharp
4041
using LnBot;
41-
using LnBot.Models;
4242

4343
using var client = new LnBotClient();
44+
var account = await client.RegisterAsync();
45+
Console.WriteLine(account.PrimaryKey);
46+
Console.WriteLine(account.RecoveryPassphrase);
47+
```
4448

45-
var wallet = await client.Wallets.CreateAsync(new CreateWalletRequest
46-
{
47-
Name = "my-agent",
48-
});
49-
Console.WriteLine(wallet.PrimaryKey);
49+
### Create a wallet
50+
51+
```csharp
52+
using var client = new LnBotClient(account.PrimaryKey);
53+
var wallet = await client.Wallets.CreateAsync();
54+
Console.WriteLine(wallet.WalletId);
5055
```
5156

5257
### Receive sats
5358

5459
```csharp
55-
using var client = new LnBotClient(wallet.PrimaryKey);
60+
var w = client.Wallet(wallet.WalletId);
5661

57-
var invoice = await client.Invoices.CreateAsync(new CreateInvoiceRequest
62+
var invoice = await w.Invoices.CreateAsync(new CreateInvoiceRequest
5863
{
5964
Amount = 1000,
6065
Memo = "Payment for task #42",
@@ -65,7 +70,7 @@ Console.WriteLine(invoice.Bolt11);
6570
### Wait for payment (SSE)
6671

6772
```csharp
68-
await foreach (var evt in client.Invoices.WatchAsync(invoice.Number))
73+
await foreach (var evt in w.Invoices.WatchAsync(invoice.Number))
6974
{
7075
if (evt.Event == "settled")
7176
{
@@ -78,7 +83,7 @@ await foreach (var evt in client.Invoices.WatchAsync(invoice.Number))
7883
### Send sats
7984

8085
```csharp
81-
var payment = await client.Payments.CreateAsync(new CreatePaymentRequest
86+
var payment = await w.Payments.CreateAsync(new CreatePaymentRequest
8287
{
8388
Target = "alice@ln.bot",
8489
Amount = 500,
@@ -88,8 +93,42 @@ var payment = await client.Payments.CreateAsync(new CreatePaymentRequest
8893
### Check balance
8994

9095
```csharp
91-
var current = await client.Wallets.CurrentAsync();
92-
Console.WriteLine($"{current.Available} sats available");
96+
var info = await w.GetAsync();
97+
Console.WriteLine($"{info.Available} sats available");
98+
```
99+
100+
---
101+
102+
## Wallet-scoped API
103+
104+
All wallet operations go through a `WalletScope` obtained via `client.Wallet(walletId)`:
105+
106+
```csharp
107+
var w = client.Wallet("wal_abc123");
108+
109+
// Wallet info
110+
var info = await w.GetAsync();
111+
await w.UpdateAsync(new UpdateWalletRequest { Name = "production" });
112+
113+
// Sub-resources
114+
w.Key // Wallet key management (wk_ keys)
115+
w.Invoices // Create, list, get, watch invoices
116+
w.Payments // Send, list, get, watch, resolve payments
117+
w.Addresses // Create, list, delete, transfer Lightning addresses
118+
w.Transactions // List transaction history
119+
w.Webhooks // Create, list, delete webhook endpoints
120+
w.Events // Real-time SSE event stream
121+
w.L402 // L402 paywall authentication
122+
```
123+
124+
Account-level operations stay on the client:
125+
126+
```csharp
127+
await client.RegisterAsync(); // Register new account
128+
await client.MeAsync(); // Get authenticated identity
129+
await client.Wallets.CreateAsync(); // Create wallet
130+
await client.Wallets.ListAsync(); // List wallets
131+
await client.Keys.RotateAsync(0); // Rotate account key
93132
```
94133

95134
---
@@ -101,7 +140,7 @@ using LnBot.Exceptions;
101140

102141
try
103142
{
104-
var wallet = await client.Wallets.CurrentAsync();
143+
var info = await w.GetAsync();
105144
}
106145
catch (NotFoundException ex)
107146
{
@@ -124,7 +163,7 @@ catch (LnBotException ex)
124163
## Configuration
125164

126165
```csharp
127-
using var client = new LnBotClient("key_...", new LnBotClientOptions
166+
using var client = new LnBotClient("uk_...", new LnBotClientOptions
128167
{
129168
BaseUrl = "https://api.ln.bot",
130169
Timeout = TimeSpan.FromSeconds(30),
@@ -135,7 +174,7 @@ Or bring your own `HttpClient`:
135174

136175
```csharp
137176
var httpClient = new HttpClient();
138-
using var client = new LnBotClient("key_...", new LnBotClientOptions
177+
using var client = new LnBotClient("uk_...", new LnBotClientOptions
139178
{
140179
HttpClient = httpClient,
141180
});
@@ -146,22 +185,24 @@ using var client = new LnBotClient("key_...", new LnBotClientOptions
146185
## L402 paywalls
147186

148187
```csharp
188+
var w = client.Wallet("wal_...");
189+
149190
// Create a challenge (server side)
150-
var challenge = await client.L402.CreateChallengeAsync(new CreateL402ChallengeRequest
191+
var challenge = await w.L402.CreateChallengeAsync(new CreateL402ChallengeRequest
151192
{
152193
Amount = 100,
153194
Description = "API access",
154195
ExpirySeconds = 3600,
155196
});
156197

157198
// Pay the challenge (client side)
158-
var result = await client.L402.PayAsync(new PayL402Request
199+
var result = await w.L402.PayAsync(new PayL402Request
159200
{
160201
WwwAuthenticate = challenge.WwwAuthenticate,
161202
});
162203

163204
// Verify a token (server side, stateless)
164-
var v = await client.L402.VerifyAsync(new VerifyL402Request
205+
var v = await w.L402.VerifyAsync(new VerifyL402Request
165206
{
166207
Authorization = result.Authorization!,
167208
});
@@ -172,6 +213,7 @@ var v = await client.L402.VerifyAsync(new VerifyL402Request
172213
## Features
173214

174215
- **Zero dependencies**`System.Net.Http` + `System.Text.Json` only
216+
- **Wallet-scoped API**`client.Wallet(id)` returns a typed scope with all sub-resources
175217
- **Async-first** — every method returns `Task<T>` with `CancellationToken` support
176218
- **Typed exceptions**`BadRequestException`, `NotFoundException`, `ConflictException`, `UnauthorizedException`, `ForbiddenException`
177219
- **SSE support**`WatchAsync` returns `IAsyncEnumerable<T>` for real-time events

src/LnBot/ILnBotClient.cs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using LnBot.Models;
12
using LnBot.Resources;
23

34
namespace LnBot;
@@ -7,36 +8,27 @@ namespace LnBot;
78
/// </summary>
89
public interface ILnBotClient : IDisposable
910
{
10-
/// <summary>Wallet management.</summary>
11+
/// <summary>Account-level wallet management (create, list).</summary>
1112
WalletsResource Wallets { get; }
1213

13-
/// <summary>Create and query invoices.</summary>
14-
InvoicesResource Invoices { get; }
15-
16-
/// <summary>Send payments.</summary>
17-
PaymentsResource Payments { get; }
18-
19-
/// <summary>Lightning address management.</summary>
20-
AddressesResource Addresses { get; }
21-
22-
/// <summary>Transaction history.</summary>
23-
TransactionsResource Transactions { get; }
24-
25-
/// <summary>Webhook endpoints.</summary>
26-
WebhooksResource Webhooks { get; }
27-
28-
/// <summary>API key management.</summary>
14+
/// <summary>Account-level API key management (uk_ keys).</summary>
2915
KeysResource Keys { get; }
3016

31-
/// <summary>L402 paywall authentication.</summary>
32-
L402Resource L402 { get; }
33-
34-
/// <summary>Real-time event stream.</summary>
35-
EventsResource Events { get; }
17+
/// <summary>Public invoice creation (no auth required).</summary>
18+
PublicInvoicesResource Invoices { get; }
3619

3720
/// <summary>Backup wallet access.</summary>
3821
BackupResource Backup { get; }
3922

4023
/// <summary>Restore wallet access.</summary>
4124
RestoreResource Restore { get; }
25+
26+
/// <summary>Registers a new account.</summary>
27+
Task<RegisterResponse> RegisterAsync(CancellationToken cancellationToken = default);
28+
29+
/// <summary>Returns the authenticated identity.</summary>
30+
Task<MeResponse> MeAsync(CancellationToken cancellationToken = default);
31+
32+
/// <summary>Returns a wallet-scoped handle.</summary>
33+
WalletScope Wallet(string walletId);
4234
}

src/LnBot/LnBot.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<!-- NuGet package metadata -->
1212
<PackageId>LnBot</PackageId>
13-
<Version>0.5.0</Version>
13+
<Version>1.0.0</Version>
1414
<Authors>LnBot</Authors>
1515
<Description>The official .NET SDK for LnBot — Bitcoin for AI Agents. Create wallets, send and receive sats over the Lightning Network.</Description>
1616
<PackageTags>bitcoin;lightning;lightning-network;lnbot;sdk;payments;sats</PackageTags>

src/LnBot/LnBotClient.cs

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Text;
44
using System.Text.Json;
55
using LnBot.Exceptions;
6+
using LnBot.Models;
67
using LnBot.Resources;
78

89
namespace LnBot;
@@ -12,7 +13,7 @@ namespace LnBot;
1213
/// </summary>
1314
public sealed class LnBotClient : ILnBotClient
1415
{
15-
internal const string Version = "0.5.0";
16+
internal const string Version = "1.0.0";
1617
internal static readonly string DefaultBaseUrl = "https://api.ln.bot";
1718

1819
private static readonly JsonSerializerOptions JsonOptions = new()
@@ -25,32 +26,14 @@ public sealed class LnBotClient : ILnBotClient
2526
private readonly HttpClient _http;
2627
private readonly bool _ownsHttpClient;
2728

28-
/// <summary>Wallet management.</summary>
29+
/// <summary>Account-level wallet management (create, list).</summary>
2930
public WalletsResource Wallets { get; }
3031

31-
/// <summary>Create and query invoices.</summary>
32-
public InvoicesResource Invoices { get; }
33-
34-
/// <summary>Send payments.</summary>
35-
public PaymentsResource Payments { get; }
36-
37-
/// <summary>Lightning address management.</summary>
38-
public AddressesResource Addresses { get; }
39-
40-
/// <summary>Transaction history.</summary>
41-
public TransactionsResource Transactions { get; }
42-
43-
/// <summary>Webhook endpoints.</summary>
44-
public WebhooksResource Webhooks { get; }
45-
46-
/// <summary>API key management.</summary>
32+
/// <summary>Account-level API key management (uk_ keys).</summary>
4733
public KeysResource Keys { get; }
4834

49-
/// <summary>L402 paywall authentication.</summary>
50-
public L402Resource L402 { get; }
51-
52-
/// <summary>Real-time event stream.</summary>
53-
public EventsResource Events { get; }
35+
/// <summary>Public invoice creation (no auth required).</summary>
36+
public PublicInvoicesResource Invoices { get; }
5437

5538
/// <summary>Backup wallet access.</summary>
5639
public BackupResource Backup { get; }
@@ -61,7 +44,7 @@ public sealed class LnBotClient : ILnBotClient
6144
/// <summary>
6245
/// Creates a new LnBot client.
6346
/// </summary>
64-
/// <param name="apiKey">API key for authenticated endpoints. Pass null or empty for unauthenticated usage (e.g. wallet creation).</param>
47+
/// <param name="apiKey">API key (uk_ or wk_). Pass null for unauthenticated usage (e.g. public invoice creation).</param>
6548
/// <param name="options">Optional configuration.</param>
6649
public LnBotClient(string? apiKey = null, LnBotClientOptions? options = null)
6750
{
@@ -93,18 +76,37 @@ public LnBotClient(string? apiKey = null, LnBotClientOptions? options = null)
9376
}
9477

9578
Wallets = new WalletsResource(this);
96-
Invoices = new InvoicesResource(this);
97-
Payments = new PaymentsResource(this);
98-
Addresses = new AddressesResource(this);
99-
Transactions = new TransactionsResource(this);
100-
Webhooks = new WebhooksResource(this);
10179
Keys = new KeysResource(this);
102-
L402 = new L402Resource(this);
103-
Events = new EventsResource(this);
80+
Invoices = new PublicInvoicesResource(this);
10481
Backup = new BackupResource(this);
10582
Restore = new RestoreResource(this);
10683
}
10784

85+
/// <summary>
86+
/// Registers a new account. Returns user keys and recovery passphrase.
87+
/// </summary>
88+
public Task<RegisterResponse> RegisterAsync(CancellationToken cancellationToken = default)
89+
=> PostAsync<RegisterResponse>("/v1/register", null, cancellationToken);
90+
91+
/// <summary>
92+
/// Returns the authenticated identity.
93+
/// </summary>
94+
public Task<MeResponse> MeAsync(CancellationToken cancellationToken = default)
95+
=> GetAsync<MeResponse>("/v1/me", cancellationToken);
96+
97+
/// <summary>
98+
/// Returns a wallet-scoped handle for the given wallet ID.
99+
/// This is a factory method — it does not make an HTTP call.
100+
/// </summary>
101+
/// <param name="walletId">The wallet ID (e.g. "wal_abc123").</param>
102+
public WalletScope Wallet(string walletId)
103+
{
104+
ArgumentException.ThrowIfNullOrEmpty(walletId);
105+
return new WalletScope(this, walletId);
106+
}
107+
108+
// ── Internal HTTP helpers ──
109+
108110
internal async Task<T> GetAsync<T>(string path, CancellationToken cancellationToken = default)
109111
{
110112
using var response = await _http.GetAsync(path, cancellationToken).ConfigureAwait(false);

src/LnBot/Models/Requests.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@
22

33
namespace LnBot.Models;
44

5-
public sealed class CreateWalletRequest
6-
{
7-
[JsonPropertyName("name")]
8-
public string? Name { get; set; }
9-
}
10-
115
public sealed class UpdateWalletRequest
126
{
137
[JsonPropertyName("name")]

0 commit comments

Comments
 (0)