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 +26 -2
- package/bin/lightning-agent.js +19 -0
- package/lib/index.js +2 -1
- package/lib/wallet.js +81 -0
- package/package.json +1 -1
- package/test.js +37 -9
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
|
-
|
|
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
|
-
- **
|
|
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
|
|
package/bin/lightning-agent.js
CHANGED
|
@@ -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
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
|
-
|
|
133
|
-
console.log(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
});
|