lightning-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeletor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # ⚡ lightning-agent
2
+
3
+ Lightning payments for AI agents. Two functions: charge and pay.
4
+
5
+ A tiny SDK that gives any AI agent the ability to send and receive Bitcoin Lightning payments using [Nostr Wallet Connect (NWC)](https://nwc.dev). No browser, no UI — just code. Connect your agent to an NWC-compatible wallet (Alby Hub, Mutiny, etc.) and start transacting in sats.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install lightning-agent
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```javascript
16
+ const { createWallet } = require('lightning-agent');
17
+
18
+ const wallet = createWallet('nostr+walletconnect://...');
19
+
20
+ // Check balance
21
+ const { balanceSats } = await wallet.getBalance();
22
+ console.log(`Balance: ${balanceSats} sats`);
23
+
24
+ // Create an invoice (get paid)
25
+ const { invoice, paymentHash } = await wallet.createInvoice({
26
+ amountSats: 50,
27
+ description: 'Text generation query'
28
+ });
29
+ console.log(`Pay me: ${invoice}`);
30
+
31
+ // Wait for payment
32
+ const { paid } = await wallet.waitForPayment(paymentHash, { timeoutMs: 60000 });
33
+
34
+ // Pay an invoice (spend)
35
+ const { preimage } = await wallet.payInvoice(someInvoice);
36
+ console.log(`Paid! Preimage: ${preimage}`);
37
+
38
+ // Done
39
+ wallet.close();
40
+ ```
41
+
42
+ ## API Reference
43
+
44
+ ### `createWallet(nwcUrl?)`
45
+
46
+ Create a wallet instance. Pass an NWC URL directly or set the `NWC_URL` environment variable.
47
+
48
+ ```javascript
49
+ const wallet = createWallet('nostr+walletconnect://...');
50
+ // or
51
+ process.env.NWC_URL = 'nostr+walletconnect://...';
52
+ const wallet = createWallet();
53
+ ```
54
+
55
+ ### `wallet.getBalance(opts?)`
56
+
57
+ Get the wallet balance.
58
+
59
+ ```javascript
60
+ const { balanceSats, balanceMsats } = await wallet.getBalance();
61
+ ```
62
+
63
+ **Options:** `{ timeoutMs: 15000 }`
64
+
65
+ ### `wallet.createInvoice(opts)`
66
+
67
+ Create a Lightning invoice (receive payment).
68
+
69
+ ```javascript
70
+ const { invoice, paymentHash, amountSats } = await wallet.createInvoice({
71
+ amountSats: 100,
72
+ description: 'API call fee',
73
+ expiry: 3600, // optional, seconds
74
+ timeoutMs: 15000 // optional
75
+ });
76
+ ```
77
+
78
+ ### `wallet.payInvoice(invoice, opts?)`
79
+
80
+ Pay a Lightning invoice.
81
+
82
+ ```javascript
83
+ const { preimage, paymentHash } = await wallet.payInvoice('lnbc50u1p...');
84
+ ```
85
+
86
+ **Options:** `{ timeoutMs: 30000 }`
87
+
88
+ ### `wallet.waitForPayment(paymentHash, opts?)`
89
+
90
+ Poll until an invoice is paid (or timeout).
91
+
92
+ ```javascript
93
+ const { paid, preimage, settledAt } = await wallet.waitForPayment(hash, {
94
+ timeoutMs: 60000, // total wait (default 60s)
95
+ pollIntervalMs: 2000 // poll frequency (default 2s)
96
+ });
97
+ ```
98
+
99
+ ### `wallet.decodeInvoice(invoice)`
100
+
101
+ Decode a bolt11 invoice offline (no wallet connection needed). Extracts amount and network.
102
+
103
+ ```javascript
104
+ const { amountSats, network } = wallet.decodeInvoice('lnbc50u1p...');
105
+ // { amountSats: 5000, network: 'mainnet', description: null, paymentHash: null }
106
+ ```
107
+
108
+ ### `wallet.close()`
109
+
110
+ Close the relay connection. Call when done.
111
+
112
+ ### `decodeBolt11(invoice)`
113
+
114
+ Standalone bolt11 decoder (no wallet instance needed).
115
+
116
+ ```javascript
117
+ const { decodeBolt11 } = require('lightning-agent');
118
+ const { amountSats } = decodeBolt11('lnbc210n1p...');
119
+ // amountSats = 21
120
+ ```
121
+
122
+ ### `parseNwcUrl(url)`
123
+
124
+ Parse an NWC URL into its components.
125
+
126
+ ```javascript
127
+ const { parseNwcUrl } = require('lightning-agent');
128
+ const { walletPubkey, relay, secret } = parseNwcUrl('nostr+walletconnect://...');
129
+ ```
130
+
131
+ ## CLI
132
+
133
+ ```bash
134
+ # Set your NWC URL
135
+ export NWC_URL="nostr+walletconnect://..."
136
+
137
+ # Check balance
138
+ lightning-agent balance
139
+
140
+ # Create an invoice for 50 sats
141
+ lightning-agent invoice 50 "Text generation query"
142
+
143
+ # Pay an invoice
144
+ lightning-agent pay lnbc50u1p...
145
+
146
+ # Decode an invoice (offline)
147
+ lightning-agent decode lnbc50u1p...
148
+
149
+ # Wait for a payment
150
+ lightning-agent wait <payment_hash> [timeout_ms]
151
+ ```
152
+
153
+ ## Getting an NWC URL
154
+
155
+ You need a Nostr Wallet Connect URL from a compatible wallet:
156
+
157
+ - **[Alby Hub](https://albyhub.com)** — Self-hosted Lightning node with NWC. Recommended for agents.
158
+ - **[Mutiny Wallet](https://mutinywallet.com)** — Mobile-first with NWC support.
159
+ - **[Coinos](https://coinos.io)** — Web wallet with NWC.
160
+
161
+ The URL looks like: `nostr+walletconnect://<wallet_pubkey>?relay=wss://...&secret=<hex>`
162
+
163
+ ## How It Works
164
+
165
+ lightning-agent uses the [NWC protocol (NIP-47)](https://github.com/nostr-protocol/nips/blob/master/47.md):
166
+
167
+ 1. Your agent signs NWC requests (kind 23194) with the secret from the NWC URL
168
+ 2. Requests are encrypted with NIP-04 and sent to the wallet's relay
169
+ 3. The wallet service processes the request and returns an encrypted response (kind 23195)
170
+ 4. All communication happens over Nostr relays — no direct connection to the wallet needed
171
+
172
+ ## Design Philosophy
173
+
174
+ This is built for AI agents, not humans:
175
+
176
+ - **Minimal deps** — just `nostr-tools` and `ws`
177
+ - **No UI** — pure code, works in any Node.js environment
178
+ - **Connection reuse** — maintains a single relay connection across requests
179
+ - **Timeouts everywhere** — agents can't afford to hang
180
+ - **Simple API** — `createInvoice` to charge, `payInvoice` to pay
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { createWallet, decodeBolt11 } = require('../lib');
5
+
6
+ const USAGE = `
7
+ lightning-agent — Lightning payments for AI agents
8
+
9
+ Usage:
10
+ lightning-agent balance Check wallet balance
11
+ lightning-agent invoice <sats> [description] Create an invoice
12
+ lightning-agent pay <bolt11> Pay an invoice
13
+ lightning-agent decode <bolt11> Decode an invoice (offline)
14
+ lightning-agent wait <payment_hash> [timeout] Wait for payment
15
+
16
+ Environment:
17
+ NWC_URL Nostr Wallet Connect URL (nostr+walletconnect://...)
18
+
19
+ Examples:
20
+ export NWC_URL="nostr+walletconnect://..."
21
+ lightning-agent balance
22
+ lightning-agent invoice 50 "AI query fee"
23
+ lightning-agent pay lnbc50u1p...
24
+ lightning-agent decode lnbc50u1p...
25
+ `.trim();
26
+
27
+ async function main() {
28
+ const args = process.argv.slice(2);
29
+ const command = args[0];
30
+
31
+ if (!command || command === '--help' || command === '-h') {
32
+ console.log(USAGE);
33
+ process.exit(0);
34
+ }
35
+
36
+ // decode is offline — no wallet needed
37
+ if (command === 'decode') {
38
+ const invoice = args[1];
39
+ if (!invoice) {
40
+ console.error('Error: bolt11 invoice required');
41
+ process.exit(1);
42
+ }
43
+ try {
44
+ const details = decodeBolt11(invoice);
45
+ console.log(JSON.stringify(details, null, 2));
46
+ } catch (err) {
47
+ console.error('Error:', err.message);
48
+ process.exit(1);
49
+ }
50
+ return;
51
+ }
52
+
53
+ // All other commands need a wallet
54
+ if (!process.env.NWC_URL) {
55
+ console.error('Error: NWC_URL environment variable not set');
56
+ console.error('Set it: export NWC_URL="nostr+walletconnect://..."');
57
+ process.exit(1);
58
+ }
59
+
60
+ let wallet;
61
+ try {
62
+ wallet = createWallet(process.env.NWC_URL);
63
+ } catch (err) {
64
+ console.error('Error creating wallet:', err.message);
65
+ process.exit(1);
66
+ }
67
+
68
+ try {
69
+ switch (command) {
70
+ case 'balance': {
71
+ const { balanceSats } = await wallet.getBalance();
72
+ console.log(`${balanceSats} sats`);
73
+ break;
74
+ }
75
+
76
+ case 'invoice': {
77
+ const sats = parseInt(args[1], 10);
78
+ if (!sats || sats <= 0) {
79
+ console.error('Error: amount in sats required (positive integer)');
80
+ process.exit(1);
81
+ }
82
+ const description = args.slice(2).join(' ') || undefined;
83
+ const result = await wallet.createInvoice({ amountSats: sats, description });
84
+ console.log(result.invoice);
85
+ if (result.paymentHash) {
86
+ console.error(`Payment hash: ${result.paymentHash}`);
87
+ }
88
+ break;
89
+ }
90
+
91
+ case 'pay': {
92
+ const invoice = args[1];
93
+ if (!invoice) {
94
+ console.error('Error: bolt11 invoice required');
95
+ process.exit(1);
96
+ }
97
+ const result = await wallet.payInvoice(invoice);
98
+ console.log(`Paid! Preimage: ${result.preimage}`);
99
+ break;
100
+ }
101
+
102
+ case 'wait': {
103
+ const paymentHash = args[1];
104
+ if (!paymentHash) {
105
+ console.error('Error: payment_hash required');
106
+ process.exit(1);
107
+ }
108
+ const timeoutMs = parseInt(args[2], 10) || 60000;
109
+ console.error(`Waiting for payment (timeout: ${timeoutMs}ms)...`);
110
+ const result = await wallet.waitForPayment(paymentHash, { timeoutMs });
111
+ if (result.paid) {
112
+ console.log(`Paid! Preimage: ${result.preimage}`);
113
+ } else {
114
+ console.log('Not paid (timed out)');
115
+ process.exit(1);
116
+ }
117
+ break;
118
+ }
119
+
120
+ default:
121
+ console.error(`Unknown command: ${command}`);
122
+ console.log(USAGE);
123
+ process.exit(1);
124
+ }
125
+ } catch (err) {
126
+ console.error('Error:', err.message);
127
+ process.exit(1);
128
+ } finally {
129
+ wallet.close();
130
+ }
131
+ }
132
+
133
+ main();
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ const { createWallet, parseNwcUrl, decodeBolt11, NWCWallet } = require('./wallet');
4
+
5
+ module.exports = {
6
+ createWallet,
7
+ parseNwcUrl,
8
+ decodeBolt11,
9
+ NWCWallet
10
+ };
package/lib/wallet.js ADDED
@@ -0,0 +1,331 @@
1
+ 'use strict';
2
+
3
+ const { finalizeEvent, getPublicKey } = require('nostr-tools');
4
+ const { Relay } = require('nostr-tools/relay');
5
+ const nip04 = require('nostr-tools/nip04');
6
+
7
+ // ─── Bolt11 minimal decoder ───
8
+
9
+ const MULTIPLIERS = {
10
+ m: 1e-3, // milli-BTC
11
+ u: 1e-6, // micro-BTC
12
+ n: 1e-9, // nano-BTC
13
+ p: 1e-12 // pico-BTC
14
+ };
15
+ const BTC_TO_SATS = 1e8;
16
+
17
+ /**
18
+ * Decode a bolt11 invoice's human-readable part to extract amount in sats.
19
+ * Format: ln<network><amount><multiplier>
20
+ * Examples: lnbc50u = 50 micro-BTC = 5000 sats
21
+ * lnbc210n = 210 nano-BTC = 21 sats
22
+ * lnbc1m = 1 milli-BTC = 100000 sats
23
+ */
24
+ function decodeBolt11(invoice) {
25
+ if (!invoice || typeof invoice !== 'string') {
26
+ throw new Error('Invalid bolt11 invoice');
27
+ }
28
+
29
+ const lower = invoice.toLowerCase();
30
+
31
+ // Find the separator: last '1' before the data part
32
+ // The human-readable part is everything before the last '1' that's followed by data
33
+ // Bolt11 uses bech32: <hrp>1<data><checksum>
34
+ const lastOneIdx = lower.lastIndexOf('1');
35
+ if (lastOneIdx < 4) throw new Error('Invalid bolt11: no separator found');
36
+
37
+ const hrp = lower.substring(0, lastOneIdx);
38
+
39
+ // Parse HRP: ln + network prefix + amount
40
+ // Network prefixes: bc (mainnet), tb (testnet), bcrt (regtest), tbs (signet)
41
+ let rest;
42
+ if (hrp.startsWith('lnbc')) {
43
+ rest = hrp.substring(4);
44
+ } else if (hrp.startsWith('lntbs')) {
45
+ rest = hrp.substring(5);
46
+ } else if (hrp.startsWith('lntb')) {
47
+ rest = hrp.substring(4);
48
+ } else if (hrp.startsWith('lnbcrt')) {
49
+ rest = hrp.substring(6);
50
+ } else {
51
+ throw new Error('Unknown bolt11 network prefix');
52
+ }
53
+
54
+ let amountSats = null;
55
+
56
+ if (rest.length > 0) {
57
+ // Last char might be a multiplier
58
+ const lastChar = rest[rest.length - 1];
59
+ if (MULTIPLIERS[lastChar] !== undefined) {
60
+ const numStr = rest.substring(0, rest.length - 1);
61
+ const num = parseFloat(numStr);
62
+ if (isNaN(num)) throw new Error('Invalid bolt11 amount: ' + numStr);
63
+ const btcAmount = num * MULTIPLIERS[lastChar];
64
+ amountSats = Math.round(btcAmount * BTC_TO_SATS);
65
+ } else {
66
+ // No multiplier — amount is in BTC
67
+ const num = parseFloat(rest);
68
+ if (isNaN(num)) throw new Error('Invalid bolt11 amount: ' + rest);
69
+ amountSats = Math.round(num * BTC_TO_SATS);
70
+ }
71
+ }
72
+ // If rest is empty, it's a zero-amount invoice
73
+
74
+ // Try to extract description from tagged fields (best effort)
75
+ // Tagged fields are in the data part after the separator, bech32-decoded
76
+ // This is complex — we do minimal extraction
77
+ let description = null;
78
+ let paymentHash = null;
79
+
80
+ try {
81
+ const dataPart = lower.substring(lastOneIdx + 1);
82
+ // First 7 chars = timestamp (bech32 chars, 35 bits)
83
+ // Then tagged fields follow
84
+ // Each tag: 1 char type + 2 chars data length + data
85
+ // We'd need full bech32 decoding for this — skip for now
86
+ // Payment hash and description require full bech32 decode
87
+ } catch (_) { /* best effort */ }
88
+
89
+ return {
90
+ amountSats,
91
+ description,
92
+ paymentHash,
93
+ network: hrp.startsWith('lntb') ? 'testnet' :
94
+ hrp.startsWith('lnbcrt') ? 'regtest' :
95
+ hrp.startsWith('lntbs') ? 'signet' : 'mainnet'
96
+ };
97
+ }
98
+
99
+ // ─── NWC URL parser ───
100
+
101
+ function parseNwcUrl(nwcUrl) {
102
+ if (!nwcUrl || typeof nwcUrl !== 'string') {
103
+ throw new Error('NWC URL is required');
104
+ }
105
+
106
+ if (!nwcUrl.startsWith('nostr+walletconnect://')) {
107
+ throw new Error('Invalid NWC URL: must start with nostr+walletconnect://');
108
+ }
109
+
110
+ const url = new URL(nwcUrl);
111
+ const walletPubkey = url.hostname || url.pathname.replace('//', '');
112
+ const relay = url.searchParams.get('relay');
113
+ const secret = url.searchParams.get('secret');
114
+
115
+ if (!walletPubkey || walletPubkey.length !== 64) {
116
+ throw new Error('Invalid NWC URL: bad wallet pubkey');
117
+ }
118
+ if (!relay) {
119
+ throw new Error('Invalid NWC URL: missing relay parameter');
120
+ }
121
+ if (!secret || secret.length !== 64) {
122
+ throw new Error('Invalid NWC URL: missing or invalid secret');
123
+ }
124
+
125
+ return { walletPubkey, relay, secret };
126
+ }
127
+
128
+ // ─── Wallet class ───
129
+
130
+ class NWCWallet {
131
+ constructor(nwcUrl) {
132
+ const parsed = parseNwcUrl(nwcUrl);
133
+ this.walletPubkey = parsed.walletPubkey;
134
+ this.relayUrl = parsed.relay;
135
+ this.secret = parsed.secret;
136
+ this.secretBytes = Uint8Array.from(Buffer.from(parsed.secret, 'hex'));
137
+ this.clientPubkey = getPublicKey(this.secretBytes);
138
+
139
+ this._closed = false;
140
+ }
141
+
142
+ // ─── Core NWC request ───
143
+ // Uses a fresh relay connection per request for reliability.
144
+ // NWC relays (especially Alby) handle connection reuse poorly.
145
+
146
+ async _nwcRequest(method, params = {}, timeoutMs = 15000) {
147
+ const payload = JSON.stringify({ method, params });
148
+ const encrypted = await nip04.encrypt(this.secretBytes, this.walletPubkey, payload);
149
+
150
+ const event = finalizeEvent({
151
+ kind: 23194,
152
+ created_at: Math.floor(Date.now() / 1000),
153
+ tags: [['p', this.walletPubkey]],
154
+ content: encrypted
155
+ }, this.secretBytes);
156
+
157
+ // Fresh connection per request — more reliable than reuse
158
+ const relay = await Relay.connect(this.relayUrl);
159
+ await relay.publish(event);
160
+
161
+ return new Promise((resolve, reject) => {
162
+ const timer = setTimeout(() => {
163
+ sub.close();
164
+ try { relay.close(); } catch (_) {}
165
+ reject(new Error(`NWC request timed out after ${timeoutMs}ms`));
166
+ }, timeoutMs);
167
+
168
+ const sub = relay.subscribe(
169
+ [{ kinds: [23195], '#e': [event.id], limit: 1 }],
170
+ {
171
+ onevent: async (e) => {
172
+ clearTimeout(timer);
173
+ try {
174
+ const decrypted = await nip04.decrypt(this.secretBytes, e.pubkey, e.content);
175
+ const parsed = JSON.parse(decrypted);
176
+
177
+ if (parsed.error) {
178
+ reject(new Error(`NWC error (${parsed.error.code}): ${parsed.error.message}`));
179
+ } else {
180
+ resolve(parsed);
181
+ }
182
+ } catch (err) {
183
+ reject(new Error('NWC decrypt failed: ' + err.message));
184
+ }
185
+ sub.close();
186
+ try { relay.close(); } catch (_) {}
187
+ }
188
+ }
189
+ );
190
+ });
191
+ }
192
+
193
+ // ─── Public API ───
194
+
195
+ /**
196
+ * Get wallet balance in sats.
197
+ */
198
+ async getBalance(opts = {}) {
199
+ const timeoutMs = opts.timeoutMs || 15000;
200
+ const res = await this._nwcRequest('get_balance', {}, timeoutMs);
201
+ // NWC returns balance in millisats
202
+ const balanceMsats = res.result?.balance || 0;
203
+ return {
204
+ balanceSats: Math.round(balanceMsats / 1000),
205
+ balanceMsats
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Create a Lightning invoice (get paid).
211
+ * @param {object} opts
212
+ * @param {number} opts.amountSats - Amount in satoshis
213
+ * @param {string} [opts.description] - Invoice description
214
+ * @param {number} [opts.timeoutMs] - Request timeout
215
+ */
216
+ async createInvoice(opts = {}) {
217
+ if (!opts.amountSats || opts.amountSats <= 0) {
218
+ throw new Error('amountSats is required and must be positive');
219
+ }
220
+
221
+ const params = {
222
+ amount: opts.amountSats * 1000, // NWC uses millisats
223
+ };
224
+ if (opts.description) params.description = opts.description;
225
+ if (opts.expiry) params.expiry = opts.expiry;
226
+
227
+ const timeoutMs = opts.timeoutMs || 15000;
228
+ const res = await this._nwcRequest('make_invoice', params, timeoutMs);
229
+
230
+ return {
231
+ invoice: res.result?.invoice,
232
+ paymentHash: res.result?.payment_hash,
233
+ description: res.result?.description || opts.description || null,
234
+ amountSats: opts.amountSats
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Pay a Lightning invoice.
240
+ * @param {string} invoice - Bolt11 invoice string
241
+ * @param {object} [opts]
242
+ * @param {number} [opts.timeoutMs] - Request timeout (default 30s for payments)
243
+ */
244
+ async payInvoice(invoice, opts = {}) {
245
+ if (!invoice || typeof invoice !== 'string') {
246
+ throw new Error('invoice is required');
247
+ }
248
+
249
+ const timeoutMs = opts.timeoutMs || 30000; // Longer default for payments
250
+ const res = await this._nwcRequest('pay_invoice', { invoice }, timeoutMs);
251
+
252
+ return {
253
+ preimage: res.result?.preimage,
254
+ paymentHash: res.result?.payment_hash || null
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Wait for an invoice to be paid by polling lookup_invoice.
260
+ * @param {string} paymentHash - Payment hash to check
261
+ * @param {object} [opts]
262
+ * @param {number} [opts.timeoutMs] - Total wait timeout (default 60s)
263
+ * @param {number} [opts.pollIntervalMs] - Poll interval (default 2s)
264
+ */
265
+ async waitForPayment(paymentHash, opts = {}) {
266
+ if (!paymentHash) throw new Error('paymentHash is required');
267
+
268
+ const timeoutMs = opts.timeoutMs || 60000;
269
+ const pollIntervalMs = opts.pollIntervalMs || 2000;
270
+ const start = Date.now();
271
+
272
+ while (Date.now() - start < timeoutMs) {
273
+ try {
274
+ const res = await this._nwcRequest('lookup_invoice', { payment_hash: paymentHash }, 10000);
275
+ // Check if settled
276
+ if (res.result?.settled_at || res.result?.preimage) {
277
+ return {
278
+ paid: true,
279
+ preimage: res.result.preimage || null,
280
+ settledAt: res.result.settled_at || null
281
+ };
282
+ }
283
+ } catch (err) {
284
+ // lookup_invoice might not be supported — continue polling
285
+ if (err.message.includes('NOT_IMPLEMENTED')) {
286
+ throw new Error('lookup_invoice not supported by this wallet');
287
+ }
288
+ // Other errors: retry
289
+ }
290
+
291
+ // Wait before next poll
292
+ await new Promise(r => setTimeout(r, pollIntervalMs));
293
+ }
294
+
295
+ return { paid: false, preimage: null, settledAt: null };
296
+ }
297
+
298
+ /**
299
+ * Decode a bolt11 invoice (offline, no NWC needed).
300
+ * @param {string} invoice - Bolt11 invoice string
301
+ */
302
+ decodeInvoice(invoice) {
303
+ return decodeBolt11(invoice);
304
+ }
305
+
306
+ /**
307
+ * Close the relay connection.
308
+ */
309
+ close() {
310
+ this._closed = true;
311
+ }
312
+ }
313
+
314
+ // ─── Factory function ───
315
+
316
+ function createWallet(nwcUrl) {
317
+ const url = nwcUrl || process.env.NWC_URL;
318
+ if (!url) {
319
+ throw new Error('NWC URL required. Pass it directly or set NWC_URL env var.');
320
+ }
321
+ return new NWCWallet(url);
322
+ }
323
+
324
+ // ─── Exports ───
325
+
326
+ module.exports = {
327
+ createWallet,
328
+ parseNwcUrl,
329
+ decodeBolt11,
330
+ NWCWallet
331
+ };
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "lightning-agent",
3
+ "version": "0.1.0",
4
+ "description": "Lightning payments for AI agents. Two functions: charge and pay.",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "lightning-agent": "bin/lightning-agent.js"
8
+ },
9
+ "keywords": ["lightning", "bitcoin", "ai", "agent", "nostr", "nwc", "payments"],
10
+ "author": "Jeletor",
11
+ "license": "MIT",
12
+ "dependencies": {
13
+ "nostr-tools": "^2.0.0",
14
+ "ws": "^8.0.0"
15
+ }
16
+ }
package/test.js ADDED
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ const { createWallet, parseNwcUrl, decodeBolt11, NWCWallet } = require('./lib');
4
+
5
+ let passed = 0;
6
+ let failed = 0;
7
+
8
+ function assert(condition, message) {
9
+ if (condition) {
10
+ passed++;
11
+ console.log(` ✅ ${message}`);
12
+ } else {
13
+ failed++;
14
+ console.log(` ❌ ${message}`);
15
+ }
16
+ }
17
+
18
+ function assertThrows(fn, message) {
19
+ try {
20
+ fn();
21
+ failed++;
22
+ console.log(` ❌ ${message} (did not throw)`);
23
+ } catch (_) {
24
+ passed++;
25
+ console.log(` ✅ ${message}`);
26
+ }
27
+ }
28
+
29
+ // ─── NWC URL Parsing ───
30
+ console.log('\n📡 NWC URL Parsing');
31
+
32
+ const testNwcUrl = 'nostr+walletconnect://962852f75958e8920c8dfeffd59baa8a75bc7029143a7cea82875772863b0721?relay=wss://relay.getalby.com/v1&secret=7ed367a99f9bde637f4f960f398ab310aaadb0e10ff8273ca6f97e136146272f';
33
+
34
+ const parsed = parseNwcUrl(testNwcUrl);
35
+ assert(parsed.walletPubkey === '962852f75958e8920c8dfeffd59baa8a75bc7029143a7cea82875772863b0721', 'extracts walletPubkey');
36
+ assert(parsed.relay === 'wss://relay.getalby.com/v1', 'extracts relay URL');
37
+ assert(parsed.secret === '7ed367a99f9bde637f4f960f398ab310aaadb0e10ff8273ca6f97e136146272f', 'extracts secret');
38
+
39
+ assertThrows(() => parseNwcUrl('https://example.com'), 'rejects non-NWC URL');
40
+ assertThrows(() => parseNwcUrl(''), 'rejects empty string');
41
+ assertThrows(() => parseNwcUrl(null), 'rejects null');
42
+ assertThrows(
43
+ () => parseNwcUrl('nostr+walletconnect://abc?relay=wss://x.com&secret=abc'),
44
+ 'rejects bad pubkey length'
45
+ );
46
+ assertThrows(
47
+ () => parseNwcUrl('nostr+walletconnect://962852f75958e8920c8dfeffd59baa8a75bc7029143a7cea82875772863b0721?secret=7ed367a99f9bde637f4f960f398ab310aaadb0e10ff8273ca6f97e136146272f'),
48
+ 'rejects missing relay'
49
+ );
50
+
51
+ // ─── Bolt11 Decoding ───
52
+ console.log('\n⚡ Bolt11 Amount Decoding');
53
+
54
+ // lnbc1u = 1 micro-BTC = 100 sats
55
+ const d1 = decodeBolt11('lnbc1u1ptest');
56
+ assert(d1.amountSats === 100, 'lnbc1u = 100 sats');
57
+
58
+ // lnbc210n = 210 nano-BTC = 21 sats
59
+ const d2 = decodeBolt11('lnbc210n1ptest');
60
+ assert(d2.amountSats === 21, 'lnbc210n = 21 sats');
61
+
62
+ // lnbc50u = 50 micro-BTC = 5000 sats
63
+ const d3 = decodeBolt11('lnbc50u1ptest');
64
+ assert(d3.amountSats === 5000, 'lnbc50u = 5000 sats');
65
+
66
+ // lnbc1m = 1 milli-BTC = 100000 sats
67
+ const d4 = decodeBolt11('lnbc1m1ptest');
68
+ assert(d4.amountSats === 100000, 'lnbc1m = 100000 sats');
69
+
70
+ // lnbc100n = 100 nano-BTC = 10 sats
71
+ const d5 = decodeBolt11('lnbc100n1ptest');
72
+ assert(d5.amountSats === 10, 'lnbc100n = 10 sats');
73
+
74
+ // lnbc2500u = 2500 micro-BTC = 250000 sats
75
+ const d6 = decodeBolt11('lnbc2500u1ptest');
76
+ assert(d6.amountSats === 250000, 'lnbc2500u = 250000 sats');
77
+
78
+ // lnbc10m = 10 milli-BTC = 1000000 sats
79
+ const d7 = decodeBolt11('lnbc10m1ptest');
80
+ assert(d7.amountSats === 1000000, 'lnbc10m = 1000000 sats');
81
+
82
+ // lnbc1500p = 1500 pico-BTC = 0.15 sats ≈ 0 sats (rounds)
83
+ const d8 = decodeBolt11('lnbc1500p1ptest');
84
+ assert(d8.amountSats === 0, 'lnbc1500p = 0 sats (sub-sat)');
85
+
86
+ // lnbc20n = 20 nano-BTC = 2 sats
87
+ const d9 = decodeBolt11('lnbc20n1ptest');
88
+ assert(d9.amountSats === 2, 'lnbc20n = 2 sats');
89
+
90
+ // Network detection
91
+ console.log('\n🌐 Network Detection');
92
+ assert(decodeBolt11('lnbc50u1ptest').network === 'mainnet', 'lnbc = mainnet');
93
+ assert(decodeBolt11('lntb50u1ptest').network === 'testnet', 'lntb = testnet');
94
+
95
+ // No amount (zero-amount invoice)
96
+ const d10 = decodeBolt11('lnbc1ptesttesttest');
97
+ assert(d10.amountSats === null, 'lnbc with no amount = null');
98
+
99
+ // Error cases
100
+ console.log('\n🚫 Bolt11 Error Cases');
101
+ assertThrows(() => decodeBolt11(''), 'rejects empty string');
102
+ assertThrows(() => decodeBolt11(null), 'rejects null');
103
+ assertThrows(() => decodeBolt11('notaninvoice'), 'rejects garbage');
104
+
105
+ // ─── createWallet interface ───
106
+ console.log('\n🔧 createWallet Interface');
107
+
108
+ const wallet = new NWCWallet(testNwcUrl);
109
+ assert(typeof wallet.getBalance === 'function', 'has getBalance()');
110
+ assert(typeof wallet.createInvoice === 'function', 'has createInvoice()');
111
+ assert(typeof wallet.payInvoice === 'function', 'has payInvoice()');
112
+ assert(typeof wallet.waitForPayment === 'function', 'has waitForPayment()');
113
+ assert(typeof wallet.decodeInvoice === 'function', 'has decodeInvoice()');
114
+ assert(typeof wallet.close === 'function', 'has close()');
115
+
116
+ // Test decodeInvoice works via wallet instance
117
+ const decoded = wallet.decodeInvoice('lnbc50u1ptest');
118
+ assert(decoded.amountSats === 5000, 'wallet.decodeInvoice works');
119
+ wallet.close();
120
+
121
+ // createWallet from env
122
+ console.log('\n🌍 createWallet from env');
123
+ process.env.NWC_URL = testNwcUrl;
124
+ const walletFromEnv = createWallet();
125
+ assert(walletFromEnv instanceof NWCWallet, 'createWallet() reads NWC_URL env');
126
+ walletFromEnv.close();
127
+ delete process.env.NWC_URL;
128
+
129
+ assertThrows(() => createWallet(), 'createWallet() throws without URL or env');
130
+
131
+ // ─── Summary ───
132
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`);
133
+ console.log(`Results: ${passed} passed, ${failed} failed`);
134
+ if (failed > 0) {
135
+ process.exit(1);
136
+ } else {
137
+ console.log('All tests passed! ✅');
138
+ }