lightning-agent 0.2.0 → 0.3.1
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 +193 -103
- package/lib/auth.js +409 -0
- package/lib/escrow.js +332 -0
- package/lib/index.js +18 -1
- package/lib/stream.js +477 -0
- package/package.json +17 -3
- package/test-v030.js +282 -0
package/lib/escrow.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightning escrow for agent-to-agent work.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Client and worker agree on terms (amount, deadline, verifier)
|
|
8
|
+
* 2. Client funds the escrow (pays invoice to escrow wallet)
|
|
9
|
+
* 3. Worker delivers work + proof
|
|
10
|
+
* 4. Escrow releases payment to worker (or refunds on timeout/dispute)
|
|
11
|
+
*
|
|
12
|
+
* The escrow wallet is custodial — it holds funds between funding and release.
|
|
13
|
+
* This is the practical tradeoff: real escrow without hold invoices.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const mgr = createEscrowManager(escrowWallet);
|
|
17
|
+
* const escrow = await mgr.create({
|
|
18
|
+
* amountSats: 500,
|
|
19
|
+
* workerAddress: 'worker@getalby.com',
|
|
20
|
+
* description: 'Translate 200 words EN→ES',
|
|
21
|
+
* deadlineMs: 3600000 // 1 hour
|
|
22
|
+
* });
|
|
23
|
+
* // Client pays: escrow.invoice
|
|
24
|
+
* await mgr.fund(escrow.id);
|
|
25
|
+
* // Worker delivers...
|
|
26
|
+
* await mgr.release(escrow.id);
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const crypto = require('crypto');
|
|
30
|
+
|
|
31
|
+
// Escrow states
|
|
32
|
+
const State = {
|
|
33
|
+
CREATED: 'created', // Terms defined, invoice generated, awaiting payment
|
|
34
|
+
FUNDED: 'funded', // Client paid, worker can begin
|
|
35
|
+
DELIVERED: 'delivered', // Worker submitted proof, awaiting verification
|
|
36
|
+
RELEASED: 'released', // Payment sent to worker — terminal
|
|
37
|
+
REFUNDED: 'refunded', // Payment returned to client — terminal
|
|
38
|
+
EXPIRED: 'expired', // Deadline passed without delivery — terminal
|
|
39
|
+
DISPUTED: 'disputed' // Dispute raised, manual resolution needed
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an escrow manager backed by an NWC wallet.
|
|
44
|
+
*
|
|
45
|
+
* @param {NWCWallet} wallet - Wallet that holds escrowed funds
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {function} [opts.onStateChange] - Called with (escrowId, oldState, newState, escrow)
|
|
48
|
+
* @param {number} [opts.defaultDeadlineMs=3600000] - Default deadline (1 hour)
|
|
49
|
+
* @returns {EscrowManager}
|
|
50
|
+
*/
|
|
51
|
+
function createEscrowManager(wallet, opts = {}) {
|
|
52
|
+
if (!wallet) throw new Error('Escrow wallet is required');
|
|
53
|
+
|
|
54
|
+
const defaultDeadlineMs = opts.defaultDeadlineMs || 60 * 60 * 1000;
|
|
55
|
+
const onStateChange = opts.onStateChange || null;
|
|
56
|
+
const escrows = new Map();
|
|
57
|
+
const timers = new Map();
|
|
58
|
+
|
|
59
|
+
function transition(id, newState) {
|
|
60
|
+
const e = escrows.get(id);
|
|
61
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
62
|
+
const old = e.state;
|
|
63
|
+
e.state = newState;
|
|
64
|
+
e.updatedAt = Date.now();
|
|
65
|
+
e.history.push({ from: old, to: newState, at: e.updatedAt });
|
|
66
|
+
if (onStateChange) onStateChange(id, old, newState, e);
|
|
67
|
+
return e;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function startDeadlineTimer(id) {
|
|
71
|
+
const e = escrows.get(id);
|
|
72
|
+
if (!e || !e.deadline) return;
|
|
73
|
+
const remaining = e.deadline - Date.now();
|
|
74
|
+
if (remaining <= 0) {
|
|
75
|
+
if (e.state === State.FUNDED || e.state === State.CREATED) {
|
|
76
|
+
transition(id, State.EXPIRED);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
const current = escrows.get(id);
|
|
82
|
+
if (current && (current.state === State.FUNDED || current.state === State.CREATED)) {
|
|
83
|
+
transition(id, State.EXPIRED);
|
|
84
|
+
}
|
|
85
|
+
timers.delete(id);
|
|
86
|
+
}, remaining);
|
|
87
|
+
timer.unref(); // Don't keep process alive
|
|
88
|
+
timers.set(id, timer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
State,
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a new escrow.
|
|
96
|
+
* Generates a Lightning invoice for the client to pay.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} config
|
|
99
|
+
* @param {number} config.amountSats - Escrow amount
|
|
100
|
+
* @param {string} config.workerAddress - Worker's Lightning address (user@domain)
|
|
101
|
+
* @param {string} [config.workerInvoice] - Or a specific bolt11 invoice to pay on release
|
|
102
|
+
* @param {string} [config.description] - Work description
|
|
103
|
+
* @param {number} [config.deadlineMs] - Time until auto-expire (from creation)
|
|
104
|
+
* @param {string} [config.clientPubkey] - Client identifier
|
|
105
|
+
* @param {string} [config.workerPubkey] - Worker identifier
|
|
106
|
+
* @param {object} [config.metadata] - Arbitrary metadata
|
|
107
|
+
* @returns {Promise<Escrow>}
|
|
108
|
+
*/
|
|
109
|
+
async create(config) {
|
|
110
|
+
if (!config.amountSats || config.amountSats <= 0) {
|
|
111
|
+
throw new Error('amountSats must be positive');
|
|
112
|
+
}
|
|
113
|
+
if (!config.workerAddress && !config.workerInvoice) {
|
|
114
|
+
throw new Error('workerAddress or workerInvoice required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const id = crypto.randomBytes(16).toString('hex');
|
|
118
|
+
const now = Date.now();
|
|
119
|
+
const deadlineMs = config.deadlineMs || defaultDeadlineMs;
|
|
120
|
+
|
|
121
|
+
// Create invoice for client to pay
|
|
122
|
+
const inv = await wallet.createInvoice({
|
|
123
|
+
amountSats: config.amountSats,
|
|
124
|
+
description: `Escrow: ${config.description || id}`,
|
|
125
|
+
expiry: Math.ceil(deadlineMs / 1000)
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const escrow = {
|
|
129
|
+
id,
|
|
130
|
+
state: State.CREATED,
|
|
131
|
+
amountSats: config.amountSats,
|
|
132
|
+
description: config.description || null,
|
|
133
|
+
workerAddress: config.workerAddress || null,
|
|
134
|
+
workerInvoice: config.workerInvoice || null,
|
|
135
|
+
clientPubkey: config.clientPubkey || null,
|
|
136
|
+
workerPubkey: config.workerPubkey || null,
|
|
137
|
+
metadata: config.metadata || {},
|
|
138
|
+
invoice: inv.invoice,
|
|
139
|
+
paymentHash: inv.paymentHash,
|
|
140
|
+
deadline: now + deadlineMs,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
updatedAt: now,
|
|
143
|
+
fundedAt: null,
|
|
144
|
+
deliveredAt: null,
|
|
145
|
+
releasedAt: null,
|
|
146
|
+
refundedAt: null,
|
|
147
|
+
deliveryProof: null,
|
|
148
|
+
releasePreimage: null,
|
|
149
|
+
refundAddress: null,
|
|
150
|
+
history: [{ from: null, to: State.CREATED, at: now }]
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
escrows.set(id, escrow);
|
|
154
|
+
startDeadlineTimer(id);
|
|
155
|
+
|
|
156
|
+
return { ...escrow };
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Mark escrow as funded (client payment received).
|
|
161
|
+
* Call this after confirming the client's payment landed.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} id - Escrow ID
|
|
164
|
+
* @param {object} [opts]
|
|
165
|
+
* @param {boolean} [opts.autoDetect=true] - Poll wallet for payment confirmation
|
|
166
|
+
* @param {number} [opts.timeoutMs=60000] - Payment detection timeout
|
|
167
|
+
* @returns {Promise<Escrow>}
|
|
168
|
+
*/
|
|
169
|
+
async fund(id, opts = {}) {
|
|
170
|
+
const e = escrows.get(id);
|
|
171
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
172
|
+
if (e.state !== State.CREATED) {
|
|
173
|
+
throw new Error(`Cannot fund escrow in state: ${e.state}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const autoDetect = opts.autoDetect !== false;
|
|
177
|
+
|
|
178
|
+
if (autoDetect && e.paymentHash) {
|
|
179
|
+
// Wait for payment confirmation
|
|
180
|
+
const result = await wallet.waitForPayment(e.paymentHash, {
|
|
181
|
+
timeoutMs: opts.timeoutMs || 60000
|
|
182
|
+
});
|
|
183
|
+
if (!result.paid) {
|
|
184
|
+
throw new Error('Payment not received within timeout');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
e.fundedAt = Date.now();
|
|
189
|
+
return { ...transition(id, State.FUNDED) };
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Worker submits delivery proof.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} id - Escrow ID
|
|
196
|
+
* @param {string|object} proof - Delivery proof (hash, URL, description, etc.)
|
|
197
|
+
* @returns {Escrow}
|
|
198
|
+
*/
|
|
199
|
+
deliver(id, proof) {
|
|
200
|
+
const e = escrows.get(id);
|
|
201
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
202
|
+
if (e.state !== State.FUNDED) {
|
|
203
|
+
throw new Error(`Cannot deliver on escrow in state: ${e.state}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
e.deliveryProof = proof;
|
|
207
|
+
e.deliveredAt = Date.now();
|
|
208
|
+
return { ...transition(id, State.DELIVERED) };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Release escrowed funds to the worker.
|
|
213
|
+
* Pays the worker's Lightning address or invoice.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} id - Escrow ID
|
|
216
|
+
* @returns {Promise<Escrow>}
|
|
217
|
+
*/
|
|
218
|
+
async release(id) {
|
|
219
|
+
const e = escrows.get(id);
|
|
220
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
221
|
+
if (e.state !== State.FUNDED && e.state !== State.DELIVERED) {
|
|
222
|
+
throw new Error(`Cannot release escrow in state: ${e.state}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let payResult;
|
|
226
|
+
if (e.workerInvoice) {
|
|
227
|
+
payResult = await wallet.payInvoice(e.workerInvoice);
|
|
228
|
+
} else if (e.workerAddress) {
|
|
229
|
+
payResult = await wallet.payAddress(e.workerAddress, {
|
|
230
|
+
amountSats: e.amountSats,
|
|
231
|
+
comment: `Escrow release: ${e.description || e.id}`
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
throw new Error('No worker payment destination');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
e.releasePreimage = payResult.preimage;
|
|
238
|
+
e.releasedAt = Date.now();
|
|
239
|
+
|
|
240
|
+
// Cancel deadline timer
|
|
241
|
+
const timer = timers.get(id);
|
|
242
|
+
if (timer) { clearTimeout(timer); timers.delete(id); }
|
|
243
|
+
|
|
244
|
+
return { ...transition(id, State.RELEASED) };
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Refund escrowed funds to the client.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} id - Escrow ID
|
|
251
|
+
* @param {string} refundAddress - Client's Lightning address for refund
|
|
252
|
+
* @param {string} [reason] - Refund reason
|
|
253
|
+
* @returns {Promise<Escrow>}
|
|
254
|
+
*/
|
|
255
|
+
async refund(id, refundAddress, reason) {
|
|
256
|
+
const e = escrows.get(id);
|
|
257
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
258
|
+
if (e.state === State.RELEASED) {
|
|
259
|
+
throw new Error('Cannot refund — already released');
|
|
260
|
+
}
|
|
261
|
+
if (e.state === State.REFUNDED) {
|
|
262
|
+
throw new Error('Already refunded');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (refundAddress) {
|
|
266
|
+
await wallet.payAddress(refundAddress, {
|
|
267
|
+
amountSats: e.amountSats,
|
|
268
|
+
comment: `Escrow refund: ${reason || e.id}`
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
e.refundAddress = refundAddress;
|
|
273
|
+
e.refundedAt = Date.now();
|
|
274
|
+
e.metadata.refundReason = reason || 'manual';
|
|
275
|
+
|
|
276
|
+
const timer = timers.get(id);
|
|
277
|
+
if (timer) { clearTimeout(timer); timers.delete(id); }
|
|
278
|
+
|
|
279
|
+
return { ...transition(id, State.REFUNDED) };
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Raise a dispute on an escrow.
|
|
284
|
+
*
|
|
285
|
+
* @param {string} id - Escrow ID
|
|
286
|
+
* @param {string} reason - Dispute reason
|
|
287
|
+
* @param {string} raisedBy - 'client' or 'worker'
|
|
288
|
+
* @returns {Escrow}
|
|
289
|
+
*/
|
|
290
|
+
dispute(id, reason, raisedBy) {
|
|
291
|
+
const e = escrows.get(id);
|
|
292
|
+
if (!e) throw new Error('Unknown escrow: ' + id);
|
|
293
|
+
if (e.state === State.RELEASED || e.state === State.REFUNDED) {
|
|
294
|
+
throw new Error(`Cannot dispute — escrow already ${e.state}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
e.metadata.dispute = { reason, raisedBy, at: Date.now() };
|
|
298
|
+
return { ...transition(id, State.DISPUTED) };
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get escrow status.
|
|
303
|
+
* @param {string} id
|
|
304
|
+
* @returns {Escrow|null}
|
|
305
|
+
*/
|
|
306
|
+
get(id) {
|
|
307
|
+
const e = escrows.get(id);
|
|
308
|
+
return e ? { ...e } : null;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* List all escrows, optionally filtered by state.
|
|
313
|
+
* @param {string} [state] - Filter by state
|
|
314
|
+
* @returns {Escrow[]}
|
|
315
|
+
*/
|
|
316
|
+
list(state) {
|
|
317
|
+
const all = [...escrows.values()];
|
|
318
|
+
if (state) return all.filter(e => e.state === state).map(e => ({ ...e }));
|
|
319
|
+
return all.map(e => ({ ...e }));
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Cleanup — cancel all timers.
|
|
324
|
+
*/
|
|
325
|
+
close() {
|
|
326
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
327
|
+
timers.clear();
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = { createEscrowManager, State };
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { createWallet, parseNwcUrl, decodeBolt11, resolveLightningAddress, NWCWallet } = require('./wallet');
|
|
4
|
+
const { createAuthServer, signAuth, authenticate } = require('./auth');
|
|
5
|
+
const { createEscrowManager, State: EscrowState } = require('./escrow');
|
|
6
|
+
const { createStreamProvider, createStreamClient } = require('./stream');
|
|
4
7
|
|
|
5
8
|
module.exports = {
|
|
9
|
+
// Wallet (v0.1.0)
|
|
6
10
|
createWallet,
|
|
7
11
|
parseNwcUrl,
|
|
8
12
|
decodeBolt11,
|
|
9
13
|
resolveLightningAddress,
|
|
10
|
-
NWCWallet
|
|
14
|
+
NWCWallet,
|
|
15
|
+
|
|
16
|
+
// Auth (v0.3.0)
|
|
17
|
+
createAuthServer,
|
|
18
|
+
signAuth,
|
|
19
|
+
authenticate,
|
|
20
|
+
|
|
21
|
+
// Escrow (v0.3.0)
|
|
22
|
+
createEscrowManager,
|
|
23
|
+
EscrowState,
|
|
24
|
+
|
|
25
|
+
// Streaming payments (v0.3.0)
|
|
26
|
+
createStreamProvider,
|
|
27
|
+
createStreamClient
|
|
11
28
|
};
|