lightning-agent 0.3.2 → 0.4.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
@@ -53,6 +53,44 @@ wallet.close();
53
53
 
54
54
  ---
55
55
 
56
+ ## Batch Payments
57
+
58
+ Pay multiple invoices or addresses in parallel with concurrency control:
59
+
60
+ ```javascript
61
+ const wallet = createWallet('nostr+walletconnect://...');
62
+
63
+ // Pay multiple invoices at once
64
+ const { results, successCount, totalSats } = await wallet.payBatch([
65
+ 'lnbc100n1...',
66
+ 'lnbc200n1...',
67
+ 'lnbc300n1...'
68
+ ], {
69
+ concurrency: 3, // Max parallel payments
70
+ stopOnError: false // Continue even if one fails
71
+ });
72
+
73
+ console.log(`Paid ${successCount} invoices, total ${totalSats} sats`);
74
+
75
+ // Pay multiple Lightning addresses
76
+ const { results: addrResults } = await wallet.payAddresses([
77
+ { address: 'alice@getalby.com', amountSats: 50, comment: 'Thanks!' },
78
+ { address: 'bob@walletofsatoshi.com', amountSats: 100 },
79
+ { address: 'carol@strike.me', amountSats: 75 }
80
+ ], { concurrency: 2 });
81
+
82
+ // Check individual results
83
+ for (const r of results) {
84
+ if (r.success) {
85
+ console.log(`✓ Paid ${r.amountSats} sats, preimage: ${r.preimage}`);
86
+ } else {
87
+ console.log(`✗ Failed: ${r.error}`);
88
+ }
89
+ }
90
+ ```
91
+
92
+ ---
93
+
56
94
  ## Auth (LNURL-auth)
57
95
 
58
96
  Login with a Lightning wallet. No passwords, no OAuth — just a signed cryptographic challenge.
package/lib/auth.js CHANGED
@@ -169,6 +169,18 @@ function createAuthServer(opts = {}) {
169
169
  * // Send sig + key back to the auth server
170
170
  */
171
171
  function signAuth(k1, privateKey) {
172
+ // Validate k1 is a 64-character hex string (32 bytes)
173
+ if (typeof k1 !== 'string' || !/^[0-9a-f]{64}$/i.test(k1)) {
174
+ throw new Error('k1 must be a 64-character hex string');
175
+ }
176
+
177
+ // Validate privateKey is valid hex when provided as string
178
+ if (typeof privateKey === 'string') {
179
+ if (!/^[0-9a-f]+$/i.test(privateKey) || privateKey.length === 0) {
180
+ throw new Error('privateKey must be a valid hex string');
181
+ }
182
+ }
183
+
172
184
  const privBuf = typeof privateKey === 'string'
173
185
  ? Buffer.from(privateKey, 'hex')
174
186
  : Buffer.from(privateKey);
package/lib/stream.js CHANGED
@@ -415,14 +415,21 @@ function createStreamClient(wallet, opts = {}) {
415
415
 
416
416
  // POST preimage back to provider
417
417
  if (sessionId && payResult.preimage) {
418
- fetch(url, {
419
- method: 'POST',
420
- headers: { 'Content-Type': 'application/json' },
421
- body: JSON.stringify({
422
- sessionId,
423
- preimage: payResult.preimage
424
- })
425
- }).catch(() => {}); // Best effort
418
+ try {
419
+ const proofRes = await fetch(url, {
420
+ method: 'POST',
421
+ headers: { 'Content-Type': 'application/json' },
422
+ body: JSON.stringify({
423
+ sessionId,
424
+ preimage: payResult.preimage
425
+ })
426
+ });
427
+ if (!proofRes.ok) {
428
+ console.error('Preimage POST failed:', proofRes.status);
429
+ }
430
+ } catch (postErr) {
431
+ console.error('Preimage POST error:', postErr.message);
432
+ }
426
433
  }
427
434
  } catch (err) {
428
435
  // Payment failed — stream will pause
package/lib/wallet.js CHANGED
@@ -90,9 +90,9 @@ function decodeBolt11(invoice) {
90
90
  amountSats,
91
91
  description,
92
92
  paymentHash,
93
- network: hrp.startsWith('lntb') ? 'testnet' :
94
- hrp.startsWith('lnbcrt') ? 'regtest' :
95
- hrp.startsWith('lntbs') ? 'signet' : 'mainnet'
93
+ network: hrp.startsWith('lntbs') ? 'signet' :
94
+ hrp.startsWith('lntb') ? 'testnet' :
95
+ hrp.startsWith('lnbcrt') ? 'regtest' : 'mainnet'
96
96
  };
97
97
  }
98
98
 
@@ -385,6 +385,142 @@ class NWCWallet {
385
385
  return decodeBolt11(invoice);
386
386
  }
387
387
 
388
+ /**
389
+ * Pay multiple invoices in parallel with concurrency control.
390
+ * @param {string[]} invoices - Array of bolt11 invoice strings
391
+ * @param {object} [opts]
392
+ * @param {number} [opts.concurrency=3] - Max concurrent payments
393
+ * @param {number} [opts.timeoutMs=30000] - Timeout per payment
394
+ * @param {boolean} [opts.stopOnError=false] - Stop all payments on first error
395
+ * @returns {Promise<{ results: Array<{invoice, success, preimage?, error?}>, successCount, failedCount, totalSats }>}
396
+ */
397
+ async payBatch(invoices, opts = {}) {
398
+ if (!Array.isArray(invoices) || invoices.length === 0) {
399
+ throw new Error('invoices array is required');
400
+ }
401
+
402
+ const concurrency = opts.concurrency || 3;
403
+ const timeoutMs = opts.timeoutMs || 30000;
404
+ const stopOnError = opts.stopOnError || false;
405
+
406
+ const results = [];
407
+ let totalSats = 0;
408
+ let stopped = false;
409
+
410
+ // Process in chunks
411
+ for (let i = 0; i < invoices.length && !stopped; i += concurrency) {
412
+ const chunk = invoices.slice(i, i + concurrency);
413
+
414
+ const chunkResults = await Promise.all(
415
+ chunk.map(async (invoice) => {
416
+ if (stopped) return { invoice, success: false, error: 'Batch stopped' };
417
+
418
+ try {
419
+ const decoded = decodeBolt11(invoice);
420
+ const result = await this.payInvoice(invoice, { timeoutMs });
421
+ totalSats += decoded.amountSats || 0;
422
+ return {
423
+ invoice,
424
+ success: true,
425
+ preimage: result.preimage,
426
+ paymentHash: result.paymentHash,
427
+ amountSats: decoded.amountSats
428
+ };
429
+ } catch (err) {
430
+ if (stopOnError) stopped = true;
431
+ return {
432
+ invoice,
433
+ success: false,
434
+ error: err.message
435
+ };
436
+ }
437
+ })
438
+ );
439
+
440
+ results.push(...chunkResults);
441
+ }
442
+
443
+ const successCount = results.filter(r => r.success).length;
444
+ const failedCount = results.length - successCount;
445
+
446
+ return {
447
+ results,
448
+ successCount,
449
+ failedCount,
450
+ totalSats
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Pay multiple Lightning addresses in parallel.
456
+ * @param {Array<{address: string, amountSats: number, comment?: string}>} payments
457
+ * @param {object} [opts]
458
+ * @param {number} [opts.concurrency=3] - Max concurrent payments
459
+ * @param {number} [opts.timeoutMs=30000] - Timeout per payment
460
+ * @param {boolean} [opts.stopOnError=false] - Stop all payments on first error
461
+ * @returns {Promise<{ results: Array, successCount, failedCount, totalSats }>}
462
+ */
463
+ async payAddresses(payments, opts = {}) {
464
+ if (!Array.isArray(payments) || payments.length === 0) {
465
+ throw new Error('payments array is required');
466
+ }
467
+
468
+ const concurrency = opts.concurrency || 3;
469
+ const timeoutMs = opts.timeoutMs || 30000;
470
+ const stopOnError = opts.stopOnError || false;
471
+
472
+ const results = [];
473
+ let totalSats = 0;
474
+ let stopped = false;
475
+
476
+ for (let i = 0; i < payments.length && !stopped; i += concurrency) {
477
+ const chunk = payments.slice(i, i + concurrency);
478
+
479
+ const chunkResults = await Promise.all(
480
+ chunk.map(async (payment) => {
481
+ if (stopped) return { address: payment.address, success: false, error: 'Batch stopped' };
482
+
483
+ try {
484
+ const result = await this.payAddress(payment.address, {
485
+ amountSats: payment.amountSats,
486
+ comment: payment.comment,
487
+ timeoutMs
488
+ });
489
+ totalSats += payment.amountSats;
490
+ return {
491
+ address: payment.address,
492
+ success: true,
493
+ preimage: result.preimage,
494
+ paymentHash: result.paymentHash,
495
+ invoice: result.invoice,
496
+ amountSats: payment.amountSats
497
+ };
498
+ } catch (err) {
499
+ if (stopOnError) stopped = true;
500
+ return {
501
+ address: payment.address,
502
+ success: false,
503
+ error: err.message,
504
+ amountSats: payment.amountSats
505
+ };
506
+ }
507
+ })
508
+ );
509
+
510
+ results.push(...chunkResults);
511
+ }
512
+
513
+ const successCount = results.filter(r => r.success).length;
514
+ const failedCount = results.length - successCount;
515
+
516
+ return {
517
+ results,
518
+ successCount,
519
+ failedCount,
520
+ totalSats
521
+ };
522
+ }
523
+
388
524
  /**
389
525
  * Close the relay connection.
390
526
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightning-agent",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Lightning toolkit for AI agents. Payments, auth, escrow, and streaming micropayments.",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/test.js CHANGED
@@ -154,8 +154,36 @@ delete process.env.NWC_URL;
154
154
 
155
155
  assertThrows(() => createWallet(), 'createWallet() throws without URL or env');
156
156
 
157
+ // ─── Batch Payment Methods ───
158
+ console.log('\n📦 Batch Payment Methods');
159
+
160
+ const walletForBatch = createWallet(testNwcUrl);
161
+
162
+ // payBatch validation
163
+ const batchTests = Promise.all([
164
+ // Test empty array
165
+ walletForBatch.payBatch([])
166
+ .then(() => assert(false, 'payBatch rejects empty array'))
167
+ .catch(e => assert(e.message.includes('required'), 'payBatch rejects empty array')),
168
+
169
+ // Test non-array
170
+ walletForBatch.payBatch('not an array')
171
+ .then(() => assert(false, 'payBatch rejects non-array'))
172
+ .catch(e => assert(e.message.includes('required'), 'payBatch rejects non-array')),
173
+
174
+ // payAddresses validation
175
+ walletForBatch.payAddresses([])
176
+ .then(() => assert(false, 'payAddresses rejects empty array'))
177
+ .catch(e => assert(e.message.includes('required'), 'payAddresses rejects empty array')),
178
+ ]).then(() => {
179
+ // Test that methods exist with correct signatures
180
+ assert(typeof walletForBatch.payBatch === 'function', 'payBatch method exists');
181
+ assert(typeof walletForBatch.payAddresses === 'function', 'payAddresses method exists');
182
+ walletForBatch.close();
183
+ });
184
+
157
185
  // ─── Summary (wait for async tests) ───
158
- addrTests.then(() => {
186
+ Promise.all([addrTests, batchTests]).then(() => {
159
187
  console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━`);
160
188
  console.log(`Results: ${passed} passed, ${failed} failed`);
161
189
  if (failed > 0) {