agent-escrow 0.1.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/LICENSE +21 -0
- package/PROTOCOL.md +180 -0
- package/README.md +179 -0
- package/package.json +57 -0
- package/src/bid.cjs +92 -0
- package/src/cli.cjs +263 -0
- package/src/delivery.cjs +85 -0
- package/src/escrow.cjs +97 -0
- package/src/index.cjs +50 -0
- package/src/marketplace.cjs +362 -0
- package/src/nostr.cjs +150 -0
- package/src/resolution.cjs +80 -0
- package/src/task.cjs +148 -0
- package/src/trust.cjs +120 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
KINDS, TASK_STATUS, RESOLUTION_TYPES,
|
|
5
|
+
connectRelays, publishToRelays, signEvent, pubkeyFromSecret,
|
|
6
|
+
closeRelays
|
|
7
|
+
} = require('./nostr.cjs');
|
|
8
|
+
const { createTaskEvent, updateTaskEvent, parseTask, findTasks, getTask } = require('./task.cjs');
|
|
9
|
+
const { createBidEvent, parseBid, findBids, findMyBids } = require('./bid.cjs');
|
|
10
|
+
const { createDeliveryEvent, parseDelivery, verifyDelivery, findDeliveries } = require('./delivery.cjs');
|
|
11
|
+
const { createResolutionEvent, parseResolution, findResolutions } = require('./resolution.cjs');
|
|
12
|
+
const { getTrustScore, meetsTrustThreshold, publishWorkAttestation, publishDisputeAttestation } = require('./trust.cjs');
|
|
13
|
+
const { paymentsAvailable, createWallet, payWorker, checkBalance, createManualPaymentResult } = require('./escrow.cjs');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a marketplace instance
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} opts
|
|
19
|
+
* @param {string[]} opts.relays - Relay URLs
|
|
20
|
+
* @param {string|Uint8Array} opts.secretKey - Nostr secret key (hex or bytes)
|
|
21
|
+
* @param {string} [opts.nwcUrl] - NWC URL for Lightning payments
|
|
22
|
+
* @param {string} [opts.lightningAddress] - Your Lightning address
|
|
23
|
+
* @param {number} [opts.defaultMinTrust] - Default minimum trust for tasks
|
|
24
|
+
* @param {number} [opts.connectTimeoutMs] - Relay connection timeout
|
|
25
|
+
*/
|
|
26
|
+
function createMarketplace(opts) {
|
|
27
|
+
const {
|
|
28
|
+
relays: relayUrls = ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
29
|
+
secretKey,
|
|
30
|
+
nwcUrl,
|
|
31
|
+
lightningAddress,
|
|
32
|
+
defaultMinTrust = 0,
|
|
33
|
+
connectTimeoutMs = 5000
|
|
34
|
+
} = opts;
|
|
35
|
+
|
|
36
|
+
if (!secretKey) throw new Error('secretKey is required');
|
|
37
|
+
|
|
38
|
+
const pubkey = pubkeyFromSecret(secretKey);
|
|
39
|
+
let relays = null;
|
|
40
|
+
let wallet = null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure relay connections are active
|
|
44
|
+
*/
|
|
45
|
+
async function ensureConnected() {
|
|
46
|
+
if (!relays || relays.length === 0) {
|
|
47
|
+
relays = await connectRelays(relayUrls, connectTimeoutMs);
|
|
48
|
+
if (relays.length === 0) {
|
|
49
|
+
throw new Error('Failed to connect to any relay');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return relays;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get or create wallet
|
|
57
|
+
*/
|
|
58
|
+
function ensureWallet() {
|
|
59
|
+
if (!wallet && nwcUrl) {
|
|
60
|
+
wallet = createWallet(nwcUrl);
|
|
61
|
+
}
|
|
62
|
+
return wallet;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sign and publish an event
|
|
67
|
+
*/
|
|
68
|
+
async function signAndPublish(template) {
|
|
69
|
+
const r = await ensureConnected();
|
|
70
|
+
const event = signEvent(template, secretKey);
|
|
71
|
+
await publishToRelays(r, event);
|
|
72
|
+
return event;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Task operations ───────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Post a new task
|
|
79
|
+
*/
|
|
80
|
+
async function postTask(taskOpts) {
|
|
81
|
+
const template = createTaskEvent({
|
|
82
|
+
...taskOpts,
|
|
83
|
+
lightningAddress: taskOpts.lightningAddress || lightningAddress,
|
|
84
|
+
minTrust: taskOpts.minTrust ?? defaultMinTrust
|
|
85
|
+
});
|
|
86
|
+
const event = await signAndPublish(template);
|
|
87
|
+
return parseTask(event);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cancel a task (only works if status is "open")
|
|
92
|
+
*/
|
|
93
|
+
async function cancelTask(taskId) {
|
|
94
|
+
const r = await ensureConnected();
|
|
95
|
+
const task = await getTask(r, pubkey, taskId);
|
|
96
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
97
|
+
if (task.status !== TASK_STATUS.OPEN) {
|
|
98
|
+
throw new Error(`Cannot cancel task in status: ${task.status}`);
|
|
99
|
+
}
|
|
100
|
+
const template = updateTaskEvent(task.raw, { status: TASK_STATUS.CANCELLED });
|
|
101
|
+
const event = await signAndPublish(template);
|
|
102
|
+
return parseTask(event);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find open tasks on the network
|
|
107
|
+
*/
|
|
108
|
+
async function browseTasks(searchOpts = {}) {
|
|
109
|
+
const r = await ensureConnected();
|
|
110
|
+
return findTasks(r, {
|
|
111
|
+
status: searchOpts.status || TASK_STATUS.OPEN,
|
|
112
|
+
...searchOpts,
|
|
113
|
+
timeoutMs: searchOpts.timeoutMs || 8000
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get a specific task
|
|
119
|
+
*/
|
|
120
|
+
async function fetchTask(posterPubkey, taskId) {
|
|
121
|
+
const r = await ensureConnected();
|
|
122
|
+
return getTask(r, posterPubkey, taskId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Bid operations ────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Submit a bid on a task
|
|
129
|
+
*/
|
|
130
|
+
async function submitBid(bidOpts) {
|
|
131
|
+
const {
|
|
132
|
+
taskEventId,
|
|
133
|
+
posterPubkey,
|
|
134
|
+
amount,
|
|
135
|
+
eta,
|
|
136
|
+
message
|
|
137
|
+
} = bidOpts;
|
|
138
|
+
|
|
139
|
+
const template = createBidEvent({
|
|
140
|
+
taskEventId,
|
|
141
|
+
posterPubkey,
|
|
142
|
+
amount,
|
|
143
|
+
lightningAddress: bidOpts.lightningAddress || lightningAddress,
|
|
144
|
+
eta,
|
|
145
|
+
message
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const event = await signAndPublish(template);
|
|
149
|
+
return parseBid(event);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get bids for a task
|
|
154
|
+
*/
|
|
155
|
+
async function getBids(taskEventId) {
|
|
156
|
+
const r = await ensureConnected();
|
|
157
|
+
return findBids(r, taskEventId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Accept a bid — claims the task for the bidder
|
|
162
|
+
*/
|
|
163
|
+
async function acceptBid(taskId, bid) {
|
|
164
|
+
const r = await ensureConnected();
|
|
165
|
+
const task = await getTask(r, pubkey, taskId);
|
|
166
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
167
|
+
if (task.status !== TASK_STATUS.OPEN) {
|
|
168
|
+
throw new Error(`Cannot accept bid — task status is: ${task.status}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check bidder trust if threshold is set
|
|
172
|
+
if (task.minTrust && task.minTrust > 0) {
|
|
173
|
+
const meets = await meetsTrustThreshold(bid.bidder, task.minTrust, { relays: relayUrls });
|
|
174
|
+
if (!meets) {
|
|
175
|
+
throw new Error(`Bidder does not meet trust threshold (${task.minTrust})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const template = updateTaskEvent(task.raw, {
|
|
180
|
+
status: TASK_STATUS.CLAIMED,
|
|
181
|
+
workerPubkey: bid.bidder
|
|
182
|
+
});
|
|
183
|
+
const event = await signAndPublish(template);
|
|
184
|
+
return parseTask(event);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Delivery operations ───────────────────────────────
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Submit work for a task
|
|
191
|
+
*/
|
|
192
|
+
async function deliver(deliveryOpts) {
|
|
193
|
+
const { taskEventId, posterPubkey, result } = deliveryOpts;
|
|
194
|
+
|
|
195
|
+
const template = createDeliveryEvent({
|
|
196
|
+
taskEventId,
|
|
197
|
+
posterPubkey,
|
|
198
|
+
result,
|
|
199
|
+
includeHash: deliveryOpts.includeHash
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const event = await signAndPublish(template);
|
|
203
|
+
return parseDelivery(event);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get deliveries for a task
|
|
208
|
+
*/
|
|
209
|
+
async function getDeliveries(taskEventId) {
|
|
210
|
+
const r = await ensureConnected();
|
|
211
|
+
return findDeliveries(r, taskEventId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Resolution operations ─────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Approve delivery and pay the worker
|
|
218
|
+
*
|
|
219
|
+
* If wallet is configured, pays automatically.
|
|
220
|
+
* If not, returns a manual payment object for the caller to handle.
|
|
221
|
+
*/
|
|
222
|
+
async function approve(approveOpts) {
|
|
223
|
+
const {
|
|
224
|
+
taskId,
|
|
225
|
+
workerPubkey,
|
|
226
|
+
workerLightningAddress,
|
|
227
|
+
amount,
|
|
228
|
+
message
|
|
229
|
+
} = approveOpts;
|
|
230
|
+
|
|
231
|
+
let payment = null;
|
|
232
|
+
|
|
233
|
+
// Try automatic payment
|
|
234
|
+
const w = ensureWallet();
|
|
235
|
+
if (w && workerLightningAddress) {
|
|
236
|
+
try {
|
|
237
|
+
payment = await payWorker(w, workerLightningAddress, amount, `agent-escrow: ${taskId}`);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
// Payment failed — continue without payment proof
|
|
240
|
+
payment = { error: e.message, amountSats: amount };
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
payment = createManualPaymentResult(null, null, amount);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get the task event ID for the resolution
|
|
247
|
+
const r = await ensureConnected();
|
|
248
|
+
const task = await getTask(r, pubkey, taskId);
|
|
249
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
250
|
+
|
|
251
|
+
// Publish resolution
|
|
252
|
+
const resTemplate = createResolutionEvent({
|
|
253
|
+
taskEventId: task.eventId,
|
|
254
|
+
workerPubkey,
|
|
255
|
+
type: RESOLUTION_TYPES.APPROVE,
|
|
256
|
+
paymentBolt11: payment.bolt11,
|
|
257
|
+
preimage: payment.preimage,
|
|
258
|
+
message
|
|
259
|
+
});
|
|
260
|
+
const resEvent = await signAndPublish(resTemplate);
|
|
261
|
+
|
|
262
|
+
// Update task status to completed
|
|
263
|
+
const taskTemplate = updateTaskEvent(task.raw, { status: TASK_STATUS.COMPLETED });
|
|
264
|
+
await signAndPublish(taskTemplate);
|
|
265
|
+
|
|
266
|
+
// Publish ai.wot attestation (non-blocking)
|
|
267
|
+
publishWorkAttestation({
|
|
268
|
+
targetPubkey: workerPubkey,
|
|
269
|
+
comment: `Completed task "${task.title}" — ${amount} sats via agent-escrow`,
|
|
270
|
+
secretKey,
|
|
271
|
+
relayUrls
|
|
272
|
+
}).catch(() => {}); // Non-fatal
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
resolution: parseResolution(resEvent),
|
|
276
|
+
payment,
|
|
277
|
+
task: { ...task, status: TASK_STATUS.COMPLETED }
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Dispute a delivery
|
|
283
|
+
*/
|
|
284
|
+
async function dispute(disputeOpts) {
|
|
285
|
+
const { taskId, workerPubkey, reason } = disputeOpts;
|
|
286
|
+
|
|
287
|
+
const r = await ensureConnected();
|
|
288
|
+
const task = await getTask(r, pubkey, taskId);
|
|
289
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
290
|
+
|
|
291
|
+
// Publish resolution
|
|
292
|
+
const resTemplate = createResolutionEvent({
|
|
293
|
+
taskEventId: task.eventId,
|
|
294
|
+
workerPubkey,
|
|
295
|
+
type: RESOLUTION_TYPES.DISPUTE,
|
|
296
|
+
message: reason
|
|
297
|
+
});
|
|
298
|
+
const resEvent = await signAndPublish(resTemplate);
|
|
299
|
+
|
|
300
|
+
// Update task status to disputed
|
|
301
|
+
const taskTemplate = updateTaskEvent(task.raw, { status: TASK_STATUS.DISPUTED });
|
|
302
|
+
await signAndPublish(taskTemplate);
|
|
303
|
+
|
|
304
|
+
// Publish negative attestation (non-blocking)
|
|
305
|
+
publishDisputeAttestation({
|
|
306
|
+
targetPubkey: workerPubkey,
|
|
307
|
+
comment: `Disputed task "${task.title}": ${reason}`,
|
|
308
|
+
secretKey,
|
|
309
|
+
relayUrls
|
|
310
|
+
}).catch(() => {}); // Non-fatal
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
resolution: parseResolution(resEvent),
|
|
314
|
+
task: { ...task, status: TASK_STATUS.DISPUTED }
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Lifecycle ─────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Close all connections and clean up
|
|
322
|
+
*/
|
|
323
|
+
async function close() {
|
|
324
|
+
if (relays) {
|
|
325
|
+
closeRelays(relays);
|
|
326
|
+
relays = null;
|
|
327
|
+
}
|
|
328
|
+
if (wallet && wallet.close) {
|
|
329
|
+
await wallet.close();
|
|
330
|
+
wallet = null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
// Identity
|
|
336
|
+
pubkey,
|
|
337
|
+
|
|
338
|
+
// Tasks
|
|
339
|
+
postTask,
|
|
340
|
+
cancelTask,
|
|
341
|
+
browseTasks,
|
|
342
|
+
fetchTask,
|
|
343
|
+
|
|
344
|
+
// Bids
|
|
345
|
+
submitBid,
|
|
346
|
+
getBids,
|
|
347
|
+
acceptBid,
|
|
348
|
+
|
|
349
|
+
// Delivery
|
|
350
|
+
deliver,
|
|
351
|
+
getDeliveries,
|
|
352
|
+
|
|
353
|
+
// Resolution
|
|
354
|
+
approve,
|
|
355
|
+
dispute,
|
|
356
|
+
|
|
357
|
+
// Utility
|
|
358
|
+
close
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = { createMarketplace };
|
package/src/nostr.cjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { finalizeEvent, getPublicKey } = require('nostr-tools/pure');
|
|
4
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
5
|
+
|
|
6
|
+
// Use ws in Node.js
|
|
7
|
+
try {
|
|
8
|
+
const WebSocket = require('ws');
|
|
9
|
+
useWebSocketImplementation(WebSocket);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
// Browser environment — native WebSocket available
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Event kinds
|
|
15
|
+
const KINDS = {
|
|
16
|
+
TASK: 30950, // Parameterized replaceable
|
|
17
|
+
BID: 950, // Regular
|
|
18
|
+
DELIVERY: 951, // Regular
|
|
19
|
+
RESOLUTION: 952 // Regular
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TASK_STATUS = {
|
|
23
|
+
OPEN: 'open',
|
|
24
|
+
CLAIMED: 'claimed',
|
|
25
|
+
DELIVERED: 'delivered',
|
|
26
|
+
COMPLETED: 'completed',
|
|
27
|
+
CANCELLED: 'cancelled',
|
|
28
|
+
DISPUTED: 'disputed',
|
|
29
|
+
EXPIRED: 'expired'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const RESOLUTION_TYPES = {
|
|
33
|
+
APPROVE: 'approve',
|
|
34
|
+
REJECT: 'reject',
|
|
35
|
+
DISPUTE: 'dispute'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Connect to multiple relays, return first successful connections
|
|
40
|
+
*/
|
|
41
|
+
async function connectRelays(urls, timeoutMs = 5000) {
|
|
42
|
+
const connected = [];
|
|
43
|
+
const results = await Promise.allSettled(
|
|
44
|
+
urls.map(url =>
|
|
45
|
+
Promise.race([
|
|
46
|
+
Relay.connect(url).then(r => { connected.push(r); return r; }),
|
|
47
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), timeoutMs))
|
|
48
|
+
])
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
return connected;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Publish event to multiple relays
|
|
56
|
+
*/
|
|
57
|
+
async function publishToRelays(relays, event) {
|
|
58
|
+
const results = await Promise.allSettled(
|
|
59
|
+
relays.map(r => r.publish(event))
|
|
60
|
+
);
|
|
61
|
+
const successes = results.filter(r => r.status === 'fulfilled').length;
|
|
62
|
+
if (successes === 0) {
|
|
63
|
+
throw new Error('Failed to publish to any relay');
|
|
64
|
+
}
|
|
65
|
+
return successes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Query relays and collect events
|
|
70
|
+
*/
|
|
71
|
+
async function queryRelays(relays, filters, timeoutMs = 8000) {
|
|
72
|
+
const events = new Map(); // dedupe by id
|
|
73
|
+
|
|
74
|
+
await Promise.allSettled(
|
|
75
|
+
relays.map(relay =>
|
|
76
|
+
new Promise((resolve) => {
|
|
77
|
+
const timer = setTimeout(() => resolve(), timeoutMs);
|
|
78
|
+
relay.subscribe(Array.isArray(filters) ? filters : [filters], {
|
|
79
|
+
onevent(event) {
|
|
80
|
+
events.set(event.id, event);
|
|
81
|
+
},
|
|
82
|
+
oneose() {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return Array.from(events.values());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sign and finalize a Nostr event
|
|
96
|
+
*/
|
|
97
|
+
function signEvent(template, secretKey) {
|
|
98
|
+
const sk = typeof secretKey === 'string'
|
|
99
|
+
? Uint8Array.from(Buffer.from(secretKey, 'hex'))
|
|
100
|
+
: secretKey;
|
|
101
|
+
return finalizeEvent(template, sk);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get public key hex from secret key
|
|
106
|
+
*/
|
|
107
|
+
function pubkeyFromSecret(secretKey) {
|
|
108
|
+
const sk = typeof secretKey === 'string'
|
|
109
|
+
? Uint8Array.from(Buffer.from(secretKey, 'hex'))
|
|
110
|
+
: secretKey;
|
|
111
|
+
return getPublicKey(sk);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract tag value from event
|
|
116
|
+
*/
|
|
117
|
+
function getTag(event, tagName) {
|
|
118
|
+
const tag = event.tags.find(t => t[0] === tagName);
|
|
119
|
+
return tag ? tag[1] : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract all values of a repeatable tag
|
|
124
|
+
*/
|
|
125
|
+
function getTags(event, tagName) {
|
|
126
|
+
return event.tags.filter(t => t[0] === tagName).map(t => t[1]);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Close all relay connections
|
|
131
|
+
*/
|
|
132
|
+
function closeRelays(relays) {
|
|
133
|
+
for (const r of relays) {
|
|
134
|
+
try { r.close(); } catch (e) { /* ignore */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
KINDS,
|
|
140
|
+
TASK_STATUS,
|
|
141
|
+
RESOLUTION_TYPES,
|
|
142
|
+
connectRelays,
|
|
143
|
+
publishToRelays,
|
|
144
|
+
queryRelays,
|
|
145
|
+
signEvent,
|
|
146
|
+
pubkeyFromSecret,
|
|
147
|
+
getTag,
|
|
148
|
+
getTags,
|
|
149
|
+
closeRelays
|
|
150
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { KINDS, RESOLUTION_TYPES, queryRelays, getTag } = require('./nostr.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a resolution event template
|
|
7
|
+
*/
|
|
8
|
+
function createResolutionEvent(opts) {
|
|
9
|
+
const {
|
|
10
|
+
taskEventId,
|
|
11
|
+
workerPubkey,
|
|
12
|
+
type,
|
|
13
|
+
paymentBolt11,
|
|
14
|
+
preimage,
|
|
15
|
+
message
|
|
16
|
+
} = opts;
|
|
17
|
+
|
|
18
|
+
if (!taskEventId) throw new Error('taskEventId is required');
|
|
19
|
+
if (!workerPubkey) throw new Error('workerPubkey is required');
|
|
20
|
+
if (!type || !Object.values(RESOLUTION_TYPES).includes(type)) {
|
|
21
|
+
throw new Error(`type must be one of: ${Object.values(RESOLUTION_TYPES).join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Disputes/rejections must include a reason
|
|
25
|
+
if ((type === RESOLUTION_TYPES.DISPUTE || type === RESOLUTION_TYPES.REJECT) && !message) {
|
|
26
|
+
throw new Error('message is required for dispute/reject resolutions');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tags = [
|
|
30
|
+
['e', taskEventId],
|
|
31
|
+
['p', workerPubkey],
|
|
32
|
+
['type', type]
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
if (paymentBolt11) tags.push(['payment', paymentBolt11]);
|
|
36
|
+
if (preimage) tags.push(['preimage', preimage]);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
kind: KINDS.RESOLUTION,
|
|
40
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
41
|
+
tags,
|
|
42
|
+
content: message || ''
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a resolution event into a structured object
|
|
48
|
+
*/
|
|
49
|
+
function parseResolution(event) {
|
|
50
|
+
return {
|
|
51
|
+
eventId: event.id,
|
|
52
|
+
poster: event.pubkey,
|
|
53
|
+
taskEventId: getTag(event, 'e'),
|
|
54
|
+
workerPubkey: getTag(event, 'p'),
|
|
55
|
+
type: getTag(event, 'type'),
|
|
56
|
+
paymentBolt11: getTag(event, 'payment'),
|
|
57
|
+
preimage: getTag(event, 'preimage'),
|
|
58
|
+
message: event.content,
|
|
59
|
+
createdAt: event.created_at * 1000,
|
|
60
|
+
raw: event
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find resolutions for a specific task
|
|
66
|
+
*/
|
|
67
|
+
async function findResolutions(relays, taskEventId, timeoutMs = 8000) {
|
|
68
|
+
const filter = {
|
|
69
|
+
kinds: [KINDS.RESOLUTION],
|
|
70
|
+
'#e': [taskEventId]
|
|
71
|
+
};
|
|
72
|
+
const events = await queryRelays(relays, filter, timeoutMs);
|
|
73
|
+
return events.map(parseResolution);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
createResolutionEvent,
|
|
78
|
+
parseResolution,
|
|
79
|
+
findResolutions
|
|
80
|
+
};
|