lightning-agent 0.3.3 → 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 +38 -0
- package/lib/wallet.js +136 -0
- package/package.json +1 -1
- package/test.js +29 -1
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/wallet.js
CHANGED
|
@@ -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
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) {
|