lightning-agent 0.1.0 → 0.2.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/README.md CHANGED
@@ -33,7 +33,9 @@ const { paid } = await wallet.waitForPayment(paymentHash, { timeoutMs: 60000 });
33
33
 
34
34
  // Pay an invoice (spend)
35
35
  const { preimage } = await wallet.payInvoice(someInvoice);
36
- console.log(`Paid! Preimage: ${preimage}`);
36
+
37
+ // Or pay a Lightning address directly
38
+ await wallet.payAddress('alice@getalby.com', { amountSats: 10 });
37
39
 
38
40
  // Done
39
41
  wallet.close();
@@ -85,6 +87,19 @@ const { preimage, paymentHash } = await wallet.payInvoice('lnbc50u1p...');
85
87
 
86
88
  **Options:** `{ timeoutMs: 30000 }`
87
89
 
90
+ ### `wallet.payAddress(address, opts)`
91
+
92
+ Pay a Lightning address (user@domain) via LNURL-pay. Resolves the address to an invoice and pays it in one call.
93
+
94
+ ```javascript
95
+ const result = await wallet.payAddress('alice@getalby.com', {
96
+ amountSats: 100,
97
+ comment: 'Great work!', // optional
98
+ timeoutMs: 30000 // optional
99
+ });
100
+ // { preimage, paymentHash, invoice, amountSats }
101
+ ```
102
+
88
103
  ### `wallet.waitForPayment(paymentHash, opts?)`
89
104
 
90
105
  Poll until an invoice is paid (or timeout).
@@ -109,6 +124,15 @@ const { amountSats, network } = wallet.decodeInvoice('lnbc50u1p...');
109
124
 
110
125
  Close the relay connection. Call when done.
111
126
 
127
+ ### `resolveLightningAddress(address, amountSats, comment?)`
128
+
129
+ Resolve a Lightning address to a bolt11 invoice without paying (useful for inspection).
130
+
131
+ ```javascript
132
+ const { resolveLightningAddress } = require('lightning-agent');
133
+ const { invoice, minSats, maxSats } = await resolveLightningAddress('bob@walletofsatoshi.com', 50);
134
+ ```
135
+
112
136
  ### `decodeBolt11(invoice)`
113
137
 
114
138
  Standalone bolt11 decoder (no wallet instance needed).
@@ -175,7 +199,7 @@ This is built for AI agents, not humans:
175
199
 
176
200
  - **Minimal deps** — just `nostr-tools` and `ws`
177
201
  - **No UI** — pure code, works in any Node.js environment
178
- - **Connection reuse** — maintains a single relay connection across requests
202
+ - **Reliable connections** — fresh relay connection per request for maximum reliability
179
203
  - **Timeouts everywhere** — agents can't afford to hang
180
204
  - **Simple API** — `createInvoice` to charge, `payInvoice` to pay
181
205
 
@@ -10,6 +10,7 @@ Usage:
10
10
  lightning-agent balance Check wallet balance
11
11
  lightning-agent invoice <sats> [description] Create an invoice
12
12
  lightning-agent pay <bolt11> Pay an invoice
13
+ lightning-agent send <address> <sats> Pay a Lightning address
13
14
  lightning-agent decode <bolt11> Decode an invoice (offline)
14
15
  lightning-agent wait <payment_hash> [timeout] Wait for payment
15
16
 
@@ -21,6 +22,7 @@ Examples:
21
22
  lightning-agent balance
22
23
  lightning-agent invoice 50 "AI query fee"
23
24
  lightning-agent pay lnbc50u1p...
25
+ lightning-agent send alice@getalby.com 100
24
26
  lightning-agent decode lnbc50u1p...
25
27
  `.trim();
26
28
 
@@ -99,6 +101,23 @@ async function main() {
99
101
  break;
100
102
  }
101
103
 
104
+ case 'send': {
105
+ const address = args[1];
106
+ const sats = parseInt(args[2], 10);
107
+ if (!address || !address.includes('@')) {
108
+ console.error('Error: Lightning address required (user@domain)');
109
+ process.exit(1);
110
+ }
111
+ if (!sats || sats <= 0) {
112
+ console.error('Error: amount in sats required (positive integer)');
113
+ process.exit(1);
114
+ }
115
+ console.error(`Sending ${sats} sats to ${address}...`);
116
+ const result = await wallet.payAddress(address, { amountSats: sats });
117
+ console.log(`Paid! Preimage: ${result.preimage}`);
118
+ break;
119
+ }
120
+
102
121
  case 'wait': {
103
122
  const paymentHash = args[1];
104
123
  if (!paymentHash) {
package/lib/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { createWallet, parseNwcUrl, decodeBolt11, NWCWallet } = require('./wallet');
3
+ const { createWallet, parseNwcUrl, decodeBolt11, resolveLightningAddress, NWCWallet } = require('./wallet');
4
4
 
5
5
  module.exports = {
6
6
  createWallet,
7
7
  parseNwcUrl,
8
8
  decodeBolt11,
9
+ resolveLightningAddress,
9
10
  NWCWallet
10
11
  };
package/lib/wallet.js CHANGED
@@ -96,6 +96,57 @@ function decodeBolt11(invoice) {
96
96
  };
97
97
  }
98
98
 
99
+ // ─── Lightning Address (LNURL-pay) resolver ───
100
+
101
+ /**
102
+ * Resolve a Lightning address to a bolt11 invoice via LNURL-pay.
103
+ * Lightning address format: user@domain → https://domain/.well-known/lnurlp/user
104
+ * @param {string} address - Lightning address (user@domain)
105
+ * @param {number} amountSats - Amount in satoshis
106
+ * @param {string} [comment] - Optional payer comment
107
+ * @returns {Promise<{ invoice: string, minSats: number, maxSats: number }>}
108
+ */
109
+ async function resolveLightningAddress(address, amountSats, comment) {
110
+ const [name, domain] = address.split('@');
111
+ if (!name || !domain) throw new Error('Invalid Lightning address: ' + address);
112
+
113
+ // Step 1: Fetch LNURL-pay metadata
114
+ const metaUrl = `https://${domain}/.well-known/lnurlp/${name}`;
115
+ const metaRes = await fetch(metaUrl);
116
+ if (!metaRes.ok) throw new Error(`LNURL fetch failed (${metaRes.status}): ${metaUrl}`);
117
+ const meta = await metaRes.json();
118
+
119
+ if (meta.status === 'ERROR') throw new Error('LNURL error: ' + (meta.reason || 'unknown'));
120
+ if (!meta.callback) throw new Error('LNURL response missing callback URL');
121
+
122
+ const minSats = Math.ceil((meta.minSendable || 1000) / 1000);
123
+ const maxSats = Math.floor((meta.maxSendable || 100000000000) / 1000);
124
+
125
+ if (amountSats < minSats) throw new Error(`Amount ${amountSats} below minimum ${minSats} sats`);
126
+ if (amountSats > maxSats) throw new Error(`Amount ${amountSats} above maximum ${maxSats} sats`);
127
+
128
+ // Step 2: Request invoice from callback
129
+ const amountMsats = amountSats * 1000;
130
+ const sep = meta.callback.includes('?') ? '&' : '?';
131
+ let cbUrl = `${meta.callback}${sep}amount=${amountMsats}`;
132
+ if (comment && meta.commentAllowed && comment.length <= meta.commentAllowed) {
133
+ cbUrl += `&comment=${encodeURIComponent(comment)}`;
134
+ }
135
+
136
+ const invoiceRes = await fetch(cbUrl);
137
+ if (!invoiceRes.ok) throw new Error(`LNURL callback failed (${invoiceRes.status})`);
138
+ const invoiceData = await invoiceRes.json();
139
+
140
+ if (invoiceData.status === 'ERROR') throw new Error('LNURL error: ' + (invoiceData.reason || 'unknown'));
141
+ if (!invoiceData.pr) throw new Error('LNURL response missing invoice (pr field)');
142
+
143
+ return {
144
+ invoice: invoiceData.pr,
145
+ minSats,
146
+ maxSats
147
+ };
148
+ }
149
+
99
150
  // ─── NWC URL parser ───
100
151
 
101
152
  function parseNwcUrl(nwcUrl) {
@@ -295,6 +346,35 @@ class NWCWallet {
295
346
  return { paid: false, preimage: null, settledAt: null };
296
347
  }
297
348
 
349
+ /**
350
+ * Pay a Lightning address (LNURL-pay) like user@domain.com.
351
+ * Resolves the address to a bolt11 invoice, then pays it.
352
+ * @param {string} address - Lightning address (user@domain)
353
+ * @param {object} opts
354
+ * @param {number} opts.amountSats - Amount in satoshis (required)
355
+ * @param {string} [opts.comment] - Optional payer comment
356
+ * @param {number} [opts.timeoutMs] - Payment timeout
357
+ * @returns {Promise<{ preimage, paymentHash, invoice, amountSats }>}
358
+ */
359
+ async payAddress(address, opts = {}) {
360
+ if (!address || !address.includes('@')) {
361
+ throw new Error('Invalid Lightning address: must be user@domain');
362
+ }
363
+ if (!opts.amountSats || opts.amountSats <= 0) {
364
+ throw new Error('amountSats is required and must be positive');
365
+ }
366
+
367
+ const resolved = await resolveLightningAddress(address, opts.amountSats, opts.comment);
368
+ const payResult = await this.payInvoice(resolved.invoice, { timeoutMs: opts.timeoutMs || 30000 });
369
+
370
+ return {
371
+ preimage: payResult.preimage,
372
+ paymentHash: payResult.paymentHash,
373
+ invoice: resolved.invoice,
374
+ amountSats: opts.amountSats
375
+ };
376
+ }
377
+
298
378
  /**
299
379
  * Decode a bolt11 invoice (offline, no NWC needed).
300
380
  * @param {string} invoice - Bolt11 invoice string
@@ -327,5 +407,6 @@ module.exports = {
327
407
  createWallet,
328
408
  parseNwcUrl,
329
409
  decodeBolt11,
410
+ resolveLightningAddress,
330
411
  NWCWallet
331
412
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightning-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lightning payments for AI agents. Two functions: charge and pay.",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/test.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { createWallet, parseNwcUrl, decodeBolt11, NWCWallet } = require('./lib');
3
+ const { createWallet, parseNwcUrl, decodeBolt11, resolveLightningAddress, NWCWallet } = require('./lib');
4
4
 
5
5
  let passed = 0;
6
6
  let failed = 0;
@@ -118,6 +118,32 @@ const decoded = wallet.decodeInvoice('lnbc50u1ptest');
118
118
  assert(decoded.amountSats === 5000, 'wallet.decodeInvoice works');
119
119
  wallet.close();
120
120
 
121
+ // ─── Lightning Address tests ───
122
+ console.log('\n⚡ Lightning Address');
123
+
124
+ assert(typeof resolveLightningAddress === 'function', 'resolveLightningAddress is exported');
125
+
126
+ // Wallet has payAddress method
127
+ const walletForAddr = createWallet(testNwcUrl);
128
+ assert(typeof walletForAddr.payAddress === 'function', 'wallet has payAddress()');
129
+ walletForAddr.close();
130
+
131
+ // payAddress validation
132
+ const walletForAddrTest = createWallet(testNwcUrl);
133
+
134
+ // Test via promise catches
135
+ const addrTests = Promise.all([
136
+ walletForAddrTest.payAddress('invalid', { amountSats: 10 })
137
+ .then(() => assert(false, 'payAddress rejects invalid address'))
138
+ .catch(e => assert(e.message.includes('Invalid Lightning address'), 'payAddress rejects invalid address')),
139
+ walletForAddrTest.payAddress('user@domain.com', {})
140
+ .then(() => assert(false, 'payAddress requires amountSats'))
141
+ .catch(e => assert(e.message.includes('amountSats'), 'payAddress requires amountSats')),
142
+ walletForAddrTest.payAddress('user@domain.com', { amountSats: -5 })
143
+ .then(() => assert(false, 'payAddress rejects negative amount'))
144
+ .catch(e => assert(e.message.includes('amountSats'), 'payAddress rejects negative amount')),
145
+ ]).then(() => walletForAddrTest.close());
146
+
121
147
  // createWallet from env
122
148
  console.log('\n🌍 createWallet from env');
123
149
  process.env.NWC_URL = testNwcUrl;
@@ -128,11 +154,13 @@ delete process.env.NWC_URL;
128
154
 
129
155
  assertThrows(() => createWallet(), 'createWallet() throws without URL or env');
130
156
 
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
- }
157
+ // ─── Summary (wait for async tests) ───
158
+ addrTests.then(() => {
159
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`);
160
+ console.log(`Results: ${passed} passed, ${failed} failed`);
161
+ if (failed > 0) {
162
+ process.exit(1);
163
+ } else {
164
+ console.log('All tests passed! ✅');
165
+ }
166
+ });