lightning-agent 0.1.0 → 0.3.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 +201 -87
- package/bin/lightning-agent.js +19 -0
- package/lib/auth.js +380 -0
- package/lib/escrow.js +332 -0
- package/lib/index.js +20 -2
- package/lib/stream.js +477 -0
- package/lib/wallet.js +81 -0
- package/package.json +3 -3
- package/test-v030.js +282 -0
- package/test.js +37 -9
package/test-v030.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for lightning-agent v0.3.0 new features.
|
|
6
|
+
* Tests auth, escrow, and stream modules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
createAuthServer, signAuth, authenticate,
|
|
11
|
+
createEscrowManager, EscrowState,
|
|
12
|
+
createStreamProvider, createStreamClient
|
|
13
|
+
} = require('./lib/index');
|
|
14
|
+
|
|
15
|
+
let passed = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
|
|
18
|
+
function assert(condition, msg) {
|
|
19
|
+
if (condition) {
|
|
20
|
+
passed++;
|
|
21
|
+
console.log(` ✅ ${msg}`);
|
|
22
|
+
} else {
|
|
23
|
+
failed++;
|
|
24
|
+
console.log(` ❌ ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Auth Tests ───
|
|
29
|
+
|
|
30
|
+
console.log('\n🔐 Auth Module');
|
|
31
|
+
|
|
32
|
+
(function testAuthServer() {
|
|
33
|
+
console.log('\n createAuthServer:');
|
|
34
|
+
|
|
35
|
+
const auth = createAuthServer({ challengeTtlMs: 5000 });
|
|
36
|
+
|
|
37
|
+
// Create challenge
|
|
38
|
+
const challenge = auth.createChallenge();
|
|
39
|
+
assert(challenge.k1 && challenge.k1.length === 64, 'Challenge has 64-char hex k1');
|
|
40
|
+
assert(challenge.expiresAt > Date.now(), 'Challenge has future expiry');
|
|
41
|
+
assert(auth.activeChallenges === 1, 'Active challenges count is 1');
|
|
42
|
+
|
|
43
|
+
// Verify with missing params
|
|
44
|
+
const bad = auth.verify(null, null, null);
|
|
45
|
+
assert(!bad.valid, 'Rejects null params');
|
|
46
|
+
assert(bad.error === 'Missing k1, sig, or key', 'Correct error message');
|
|
47
|
+
|
|
48
|
+
// Verify with wrong k1
|
|
49
|
+
const bad2 = auth.verify('deadbeef'.repeat(8), 'aabb', 'ccdd');
|
|
50
|
+
assert(!bad2.valid, 'Rejects unknown k1');
|
|
51
|
+
|
|
52
|
+
console.log(' Active challenges:', auth.activeChallenges);
|
|
53
|
+
})();
|
|
54
|
+
|
|
55
|
+
(function testSignAuth() {
|
|
56
|
+
console.log('\n signAuth + verify roundtrip:');
|
|
57
|
+
|
|
58
|
+
const crypto = require('crypto');
|
|
59
|
+
const privKey = crypto.randomBytes(32).toString('hex');
|
|
60
|
+
|
|
61
|
+
const auth = createAuthServer();
|
|
62
|
+
const challenge = auth.createChallenge();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const { sig, key } = signAuth(challenge.k1, privKey);
|
|
66
|
+
assert(sig && sig.length > 0, 'Signature generated');
|
|
67
|
+
assert(key && key.length === 66, 'Compressed pubkey (33 bytes hex)');
|
|
68
|
+
|
|
69
|
+
const result = auth.verify(challenge.k1, sig, key);
|
|
70
|
+
assert(result.valid === true, 'Signature verified ✓');
|
|
71
|
+
assert(result.pubkey === key, 'Returns correct pubkey');
|
|
72
|
+
|
|
73
|
+
// Replay protection
|
|
74
|
+
const replay = auth.verify(challenge.k1, sig, key);
|
|
75
|
+
assert(!replay.valid, 'Rejects replay (challenge already used)');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
assert(false, 'Sign/verify failed: ' + err.message);
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
(function testAuthWithLnurl() {
|
|
82
|
+
console.log('\n LNURL generation:');
|
|
83
|
+
|
|
84
|
+
const auth = createAuthServer({ callbackUrl: 'https://example.com/auth' });
|
|
85
|
+
const challenge = auth.createChallenge();
|
|
86
|
+
|
|
87
|
+
assert(challenge.lnurl && challenge.lnurl.startsWith('lnurl'), 'LNURL generated');
|
|
88
|
+
assert(challenge.callbackUrl.includes('k1='), 'Callback URL has k1');
|
|
89
|
+
assert(challenge.callbackUrl.includes('tag=login'), 'Callback URL has tag=login');
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
(function testAuthMiddleware() {
|
|
93
|
+
console.log('\n Middleware:');
|
|
94
|
+
|
|
95
|
+
const auth = createAuthServer();
|
|
96
|
+
const handler = auth.middleware((pubkey, req, res) => {
|
|
97
|
+
// onAuth callback
|
|
98
|
+
});
|
|
99
|
+
assert(typeof handler === 'function', 'Middleware returns function');
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
// ─── Escrow Tests ───
|
|
103
|
+
|
|
104
|
+
console.log('\n\n💰 Escrow Module');
|
|
105
|
+
|
|
106
|
+
(function testEscrowStates() {
|
|
107
|
+
console.log('\n EscrowState:');
|
|
108
|
+
assert(EscrowState.CREATED === 'created', 'CREATED state');
|
|
109
|
+
assert(EscrowState.FUNDED === 'funded', 'FUNDED state');
|
|
110
|
+
assert(EscrowState.RELEASED === 'released', 'RELEASED state');
|
|
111
|
+
assert(EscrowState.REFUNDED === 'refunded', 'REFUNDED state');
|
|
112
|
+
assert(EscrowState.EXPIRED === 'expired', 'EXPIRED state');
|
|
113
|
+
assert(EscrowState.DISPUTED === 'disputed', 'DISPUTED state');
|
|
114
|
+
})();
|
|
115
|
+
|
|
116
|
+
(async function testEscrowCreate() {
|
|
117
|
+
console.log('\n createEscrowManager:');
|
|
118
|
+
|
|
119
|
+
// Mock wallet
|
|
120
|
+
const mockWallet = {
|
|
121
|
+
createInvoice: async (opts) => ({
|
|
122
|
+
invoice: 'lnbc' + opts.amountSats + 'n1mock',
|
|
123
|
+
paymentHash: require('crypto').randomBytes(32).toString('hex'),
|
|
124
|
+
amountSats: opts.amountSats
|
|
125
|
+
}),
|
|
126
|
+
waitForPayment: async () => ({ paid: true, preimage: 'abc123' }),
|
|
127
|
+
payInvoice: async () => ({ preimage: 'def456', paymentHash: 'hash456' }),
|
|
128
|
+
payAddress: async (addr, opts) => ({
|
|
129
|
+
preimage: 'ghi789',
|
|
130
|
+
paymentHash: 'hash789',
|
|
131
|
+
invoice: 'lnbcmock',
|
|
132
|
+
amountSats: opts.amountSats
|
|
133
|
+
})
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const stateChanges = [];
|
|
137
|
+
const mgr = createEscrowManager(mockWallet, {
|
|
138
|
+
onStateChange: (id, from, to) => stateChanges.push({ id, from, to })
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create escrow
|
|
142
|
+
const escrow = await mgr.create({
|
|
143
|
+
amountSats: 500,
|
|
144
|
+
workerAddress: 'worker@getalby.com',
|
|
145
|
+
description: 'Test work',
|
|
146
|
+
deadlineMs: 60000
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert(escrow.id && escrow.id.length === 32, 'Escrow ID generated');
|
|
150
|
+
assert(escrow.state === 'created', 'Initial state is created');
|
|
151
|
+
assert(escrow.amountSats === 500, 'Amount correct');
|
|
152
|
+
assert(escrow.invoice.startsWith('lnbc'), 'Invoice generated');
|
|
153
|
+
assert(escrow.deadline > Date.now(), 'Deadline set');
|
|
154
|
+
|
|
155
|
+
// Fund
|
|
156
|
+
const funded = await mgr.fund(escrow.id);
|
|
157
|
+
assert(funded.state === 'funded', 'State transitions to funded');
|
|
158
|
+
assert(funded.fundedAt > 0, 'Funded timestamp set');
|
|
159
|
+
|
|
160
|
+
// Deliver
|
|
161
|
+
const delivered = mgr.deliver(escrow.id, { hash: 'sha256ofwork' });
|
|
162
|
+
assert(delivered.state === 'delivered', 'State transitions to delivered');
|
|
163
|
+
assert(delivered.deliveryProof.hash === 'sha256ofwork', 'Proof stored');
|
|
164
|
+
|
|
165
|
+
// Release
|
|
166
|
+
const released = await mgr.release(escrow.id);
|
|
167
|
+
assert(released.state === 'released', 'State transitions to released');
|
|
168
|
+
assert(released.releasePreimage === 'ghi789', 'Release preimage stored');
|
|
169
|
+
|
|
170
|
+
// History
|
|
171
|
+
assert(released.history.length === 4, 'Full history (4 transitions)');
|
|
172
|
+
assert(stateChanges.length === 3, 'State change callbacks fired (3)');
|
|
173
|
+
|
|
174
|
+
// List
|
|
175
|
+
const all = mgr.list();
|
|
176
|
+
assert(all.length === 1, 'List returns 1 escrow');
|
|
177
|
+
const releasedList = mgr.list('released');
|
|
178
|
+
assert(releasedList.length === 1, 'Filtered list works');
|
|
179
|
+
|
|
180
|
+
mgr.close();
|
|
181
|
+
})().catch(e => { failed++; console.log(' ❌ Escrow test error:', e.message); });
|
|
182
|
+
|
|
183
|
+
(async function testEscrowRefund() {
|
|
184
|
+
console.log('\n Escrow refund flow:');
|
|
185
|
+
|
|
186
|
+
const mockWallet = {
|
|
187
|
+
createInvoice: async (opts) => ({
|
|
188
|
+
invoice: 'lnbcmock',
|
|
189
|
+
paymentHash: require('crypto').randomBytes(32).toString('hex')
|
|
190
|
+
}),
|
|
191
|
+
waitForPayment: async () => ({ paid: true }),
|
|
192
|
+
payAddress: async () => ({ preimage: 'refund123' })
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const mgr = createEscrowManager(mockWallet);
|
|
196
|
+
const escrow = await mgr.create({
|
|
197
|
+
amountSats: 200,
|
|
198
|
+
workerAddress: 'worker@example.com',
|
|
199
|
+
deadlineMs: 60000
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await mgr.fund(escrow.id);
|
|
203
|
+
const refunded = await mgr.refund(escrow.id, 'client@getalby.com', 'Worker no-show');
|
|
204
|
+
|
|
205
|
+
assert(refunded.state === 'refunded', 'Refund succeeds');
|
|
206
|
+
assert(refunded.metadata.refundReason === 'Worker no-show', 'Refund reason stored');
|
|
207
|
+
|
|
208
|
+
mgr.close();
|
|
209
|
+
})().catch(e => { failed++; console.log(' ❌ Escrow refund error:', e.message); });
|
|
210
|
+
|
|
211
|
+
(async function testEscrowDispute() {
|
|
212
|
+
console.log('\n Escrow dispute:');
|
|
213
|
+
|
|
214
|
+
const mockWallet = {
|
|
215
|
+
createInvoice: async () => ({
|
|
216
|
+
invoice: 'lnbcmock',
|
|
217
|
+
paymentHash: require('crypto').randomBytes(32).toString('hex')
|
|
218
|
+
}),
|
|
219
|
+
waitForPayment: async () => ({ paid: true })
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const mgr = createEscrowManager(mockWallet);
|
|
223
|
+
const escrow = await mgr.create({
|
|
224
|
+
amountSats: 1000,
|
|
225
|
+
workerAddress: 'worker@example.com'
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await mgr.fund(escrow.id);
|
|
229
|
+
const disputed = mgr.dispute(escrow.id, 'Work quality insufficient', 'client');
|
|
230
|
+
|
|
231
|
+
assert(disputed.state === 'disputed', 'Dispute state set');
|
|
232
|
+
assert(disputed.metadata.dispute.reason === 'Work quality insufficient', 'Dispute reason stored');
|
|
233
|
+
assert(disputed.metadata.dispute.raisedBy === 'client', 'Dispute raiser stored');
|
|
234
|
+
|
|
235
|
+
mgr.close();
|
|
236
|
+
})().catch(e => { failed++; console.log(' ❌ Escrow dispute error:', e.message); });
|
|
237
|
+
|
|
238
|
+
// ─── Stream Tests ───
|
|
239
|
+
|
|
240
|
+
console.log('\n\n⚡ Stream Module');
|
|
241
|
+
|
|
242
|
+
(function testStreamProvider() {
|
|
243
|
+
console.log('\n createStreamProvider:');
|
|
244
|
+
|
|
245
|
+
const mockWallet = {
|
|
246
|
+
createInvoice: async (opts) => ({
|
|
247
|
+
invoice: 'lnbc1n1mock',
|
|
248
|
+
paymentHash: require('crypto').randomBytes(32).toString('hex')
|
|
249
|
+
}),
|
|
250
|
+
waitForPayment: async () => ({ paid: true })
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const provider = createStreamProvider(mockWallet, {
|
|
254
|
+
satsPerBatch: 2,
|
|
255
|
+
tokensPerBatch: 50,
|
|
256
|
+
maxBatches: 10
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
assert(typeof provider.handleRequest === 'function', 'Has handleRequest method');
|
|
260
|
+
assert(provider.activeSessions === 0, 'No active sessions initially');
|
|
261
|
+
})();
|
|
262
|
+
|
|
263
|
+
(function testStreamClient() {
|
|
264
|
+
console.log('\n createStreamClient:');
|
|
265
|
+
|
|
266
|
+
const mockWallet = {
|
|
267
|
+
payInvoice: async () => ({ preimage: 'mock_preimage' })
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const client = createStreamClient(mockWallet, { maxSats: 500 });
|
|
271
|
+
|
|
272
|
+
assert(typeof client.stream === 'function', 'Has stream method');
|
|
273
|
+
assert(client.budget.maxSats === 500, 'Budget set correctly');
|
|
274
|
+
})();
|
|
275
|
+
|
|
276
|
+
// ─── Summary ───
|
|
277
|
+
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
console.log(`\n${'─'.repeat(40)}`);
|
|
280
|
+
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
281
|
+
if (failed > 0) process.exit(1);
|
|
282
|
+
}, 500);
|
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
|
+
});
|