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/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
  };