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 +38 -0
- package/lib/auth.js +12 -0
- package/lib/stream.js +15 -8
- package/lib/wallet.js +139 -3
- 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/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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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('
|
|
94
|
-
hrp.startsWith('
|
|
95
|
-
hrp.startsWith('
|
|
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
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) {
|