@elisym/sdk 0.1.2
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 +327 -0
- package/dist/index.cjs +1199 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +290 -0
- package/dist/index.d.ts +290 -0
- package/dist/index.js +1145 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var nostrTools = require('nostr-tools');
|
|
4
|
+
var nip44 = require('nostr-tools/nip44');
|
|
5
|
+
var nip17 = require('nostr-tools/nip17');
|
|
6
|
+
var nip59 = require('nostr-tools/nip59');
|
|
7
|
+
var web3_js = require('@solana/web3.js');
|
|
8
|
+
var Decimal = require('decimal.js-light');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var nip44__namespace = /*#__PURE__*/_interopNamespace(nip44);
|
|
31
|
+
var nip17__namespace = /*#__PURE__*/_interopNamespace(nip17);
|
|
32
|
+
var nip59__namespace = /*#__PURE__*/_interopNamespace(nip59);
|
|
33
|
+
var Decimal__default = /*#__PURE__*/_interopDefault(Decimal);
|
|
34
|
+
|
|
35
|
+
// src/core/pool.ts
|
|
36
|
+
|
|
37
|
+
// src/constants.ts
|
|
38
|
+
var RELAYS = [
|
|
39
|
+
"wss://relay.damus.io",
|
|
40
|
+
"wss://nos.lol",
|
|
41
|
+
"wss://relay.nostr.band",
|
|
42
|
+
"wss://relay.primal.net",
|
|
43
|
+
"wss://relay.snort.social"
|
|
44
|
+
];
|
|
45
|
+
var KIND_APP_HANDLER = 31990;
|
|
46
|
+
var KIND_JOB_REQUEST_BASE = 5e3;
|
|
47
|
+
var KIND_JOB_RESULT_BASE = 6e3;
|
|
48
|
+
var KIND_JOB_FEEDBACK = 7e3;
|
|
49
|
+
var DEFAULT_KIND_OFFSET = 100;
|
|
50
|
+
var KIND_GIFT_WRAP = 1059;
|
|
51
|
+
var KIND_JOB_REQUEST = KIND_JOB_REQUEST_BASE + DEFAULT_KIND_OFFSET;
|
|
52
|
+
var KIND_JOB_RESULT = KIND_JOB_RESULT_BASE + DEFAULT_KIND_OFFSET;
|
|
53
|
+
function jobRequestKind(offset) {
|
|
54
|
+
return KIND_JOB_REQUEST_BASE + offset;
|
|
55
|
+
}
|
|
56
|
+
function jobResultKind(offset) {
|
|
57
|
+
return KIND_JOB_RESULT_BASE + offset;
|
|
58
|
+
}
|
|
59
|
+
var KIND_PING = 20200;
|
|
60
|
+
var KIND_PONG = 20201;
|
|
61
|
+
var LAMPORTS_PER_SOL = 1e9;
|
|
62
|
+
var PROTOCOL_FEE_BPS = 300;
|
|
63
|
+
var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
|
|
64
|
+
|
|
65
|
+
// src/core/pool.ts
|
|
66
|
+
var NostrPool = class {
|
|
67
|
+
pool;
|
|
68
|
+
relays;
|
|
69
|
+
constructor(relays = RELAYS) {
|
|
70
|
+
this.pool = new nostrTools.SimplePool();
|
|
71
|
+
this.relays = relays;
|
|
72
|
+
}
|
|
73
|
+
async querySync(filter) {
|
|
74
|
+
return Promise.race([
|
|
75
|
+
this.pool.querySync(this.relays, filter),
|
|
76
|
+
new Promise(
|
|
77
|
+
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
78
|
+
)
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
async queryBatched(filter, keys, batchSize = 250) {
|
|
82
|
+
const batches = [];
|
|
83
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
84
|
+
const batch = keys.slice(i, i + batchSize);
|
|
85
|
+
batches.push(
|
|
86
|
+
Promise.race([
|
|
87
|
+
this.pool.querySync(this.relays, {
|
|
88
|
+
...filter,
|
|
89
|
+
authors: batch
|
|
90
|
+
}),
|
|
91
|
+
new Promise(
|
|
92
|
+
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
93
|
+
)
|
|
94
|
+
])
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return (await Promise.all(batches)).flat();
|
|
98
|
+
}
|
|
99
|
+
async queryBatchedByTag(filter, tagName, values, batchSize = 250) {
|
|
100
|
+
const batches = [];
|
|
101
|
+
for (let i = 0; i < values.length; i += batchSize) {
|
|
102
|
+
const batch = values.slice(i, i + batchSize);
|
|
103
|
+
batches.push(
|
|
104
|
+
Promise.race([
|
|
105
|
+
this.pool.querySync(this.relays, {
|
|
106
|
+
...filter,
|
|
107
|
+
[`#${tagName}`]: batch
|
|
108
|
+
}),
|
|
109
|
+
new Promise(
|
|
110
|
+
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
111
|
+
)
|
|
112
|
+
])
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return (await Promise.all(batches)).flat();
|
|
116
|
+
}
|
|
117
|
+
async publish(event) {
|
|
118
|
+
try {
|
|
119
|
+
await Promise.any(this.pool.publish(this.relays, event));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
if (err instanceof AggregateError) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Failed to publish to all ${this.relays.length} relays: ${err.errors.map((e) => e.message).join(", ")}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Publish to all relays and wait for all to settle. Throws if none accepted. */
|
|
130
|
+
async publishAll(event) {
|
|
131
|
+
const results = await Promise.allSettled(this.pool.publish(this.relays, event));
|
|
132
|
+
const anyOk = results.some((r) => r.status === "fulfilled");
|
|
133
|
+
if (!anyOk) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Failed to publish to all ${this.relays.length} relays`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
subscribe(filter, onEvent) {
|
|
140
|
+
return this.pool.subscribeMany(
|
|
141
|
+
this.relays,
|
|
142
|
+
filter,
|
|
143
|
+
{ onevent: onEvent }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Subscribe and wait until at least one relay confirms the subscription
|
|
148
|
+
* is active (EOSE). Resolves on the first relay that responds.
|
|
149
|
+
* Essential for ephemeral events where the subscription must be live
|
|
150
|
+
* before publishing.
|
|
151
|
+
*/
|
|
152
|
+
subscribeAndWait(filter, onEvent, timeoutMs = 3e3) {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
let resolved = false;
|
|
155
|
+
const done = () => {
|
|
156
|
+
if (resolved) return;
|
|
157
|
+
resolved = true;
|
|
158
|
+
resolve(combinedSub);
|
|
159
|
+
};
|
|
160
|
+
const subs = [];
|
|
161
|
+
for (const relay of this.relays) {
|
|
162
|
+
const sub = this.pool.subscribeMany(
|
|
163
|
+
[relay],
|
|
164
|
+
filter,
|
|
165
|
+
{
|
|
166
|
+
onevent: onEvent,
|
|
167
|
+
oneose: done
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
subs.push(sub);
|
|
171
|
+
}
|
|
172
|
+
const combinedSub = {
|
|
173
|
+
close: (reason) => {
|
|
174
|
+
for (const s of subs) s.close(reason);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
setTimeout(done, timeoutMs);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Tear down pool and create a fresh one.
|
|
182
|
+
* Works around nostr-tools `onerror → skipReconnection = true` bug
|
|
183
|
+
* that permanently kills subscriptions. Callers must re-subscribe.
|
|
184
|
+
*/
|
|
185
|
+
reset() {
|
|
186
|
+
try {
|
|
187
|
+
this.pool.close(this.relays);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
this.pool = new nostrTools.SimplePool();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Lightweight connectivity probe. Returns true if at least one relay responds.
|
|
194
|
+
*/
|
|
195
|
+
async probe(timeoutMs = 3e3) {
|
|
196
|
+
let timer;
|
|
197
|
+
try {
|
|
198
|
+
await Promise.race([
|
|
199
|
+
this.pool.querySync(this.relays, { kinds: [0], limit: 1 }),
|
|
200
|
+
new Promise((_, reject) => {
|
|
201
|
+
timer = setTimeout(() => reject(new Error("probe timeout")), timeoutMs);
|
|
202
|
+
})
|
|
203
|
+
]);
|
|
204
|
+
return true;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
} finally {
|
|
208
|
+
clearTimeout(timer);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
getRelays() {
|
|
212
|
+
return this.relays;
|
|
213
|
+
}
|
|
214
|
+
close() {
|
|
215
|
+
this.pool.close(this.relays);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
var ElisymIdentity = class _ElisymIdentity {
|
|
219
|
+
secretKey;
|
|
220
|
+
publicKey;
|
|
221
|
+
npub;
|
|
222
|
+
constructor(secretKey) {
|
|
223
|
+
this.secretKey = secretKey;
|
|
224
|
+
this.publicKey = nostrTools.getPublicKey(secretKey);
|
|
225
|
+
this.npub = nostrTools.nip19.npubEncode(this.publicKey);
|
|
226
|
+
}
|
|
227
|
+
static generate() {
|
|
228
|
+
return new _ElisymIdentity(nostrTools.generateSecretKey());
|
|
229
|
+
}
|
|
230
|
+
static fromSecretKey(sk) {
|
|
231
|
+
return new _ElisymIdentity(sk);
|
|
232
|
+
}
|
|
233
|
+
static fromHex(hex) {
|
|
234
|
+
if (hex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
235
|
+
throw new Error("Invalid secret key hex: expected 64 hex characters (32 bytes).");
|
|
236
|
+
}
|
|
237
|
+
const bytes = new Uint8Array(32);
|
|
238
|
+
for (let i = 0; i < 64; i += 2) {
|
|
239
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
240
|
+
}
|
|
241
|
+
return new _ElisymIdentity(bytes);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function toDTag(name) {
|
|
245
|
+
return name.toLowerCase().replace(/\s+/g, "-");
|
|
246
|
+
}
|
|
247
|
+
function buildAgentsFromEvents(events, network) {
|
|
248
|
+
const latestByDTag = /* @__PURE__ */ new Map();
|
|
249
|
+
for (const event of events) {
|
|
250
|
+
const dTag = event.tags.find((t) => t[0] === "d")?.[1] ?? "";
|
|
251
|
+
const key = `${event.pubkey}:${dTag}`;
|
|
252
|
+
const prev = latestByDTag.get(key);
|
|
253
|
+
if (!prev || event.created_at > prev.created_at) {
|
|
254
|
+
latestByDTag.set(key, event);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const accumMap = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const event of latestByDTag.values()) {
|
|
259
|
+
try {
|
|
260
|
+
const card = JSON.parse(event.content);
|
|
261
|
+
if (!card.name || card.deleted) continue;
|
|
262
|
+
const agentNetwork = card.payment?.network ?? "devnet";
|
|
263
|
+
if (agentNetwork !== network) continue;
|
|
264
|
+
const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
|
|
265
|
+
const entry = { card, kTags, createdAt: event.created_at };
|
|
266
|
+
const existing = accumMap.get(event.pubkey);
|
|
267
|
+
if (existing) {
|
|
268
|
+
const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
|
|
269
|
+
if (dupIndex >= 0) {
|
|
270
|
+
existing.entries[dupIndex] = entry;
|
|
271
|
+
} else {
|
|
272
|
+
existing.entries.push(entry);
|
|
273
|
+
}
|
|
274
|
+
if (event.created_at > existing.lastSeen) {
|
|
275
|
+
existing.lastSeen = event.created_at;
|
|
276
|
+
existing.eventId = event.id;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
accumMap.set(event.pubkey, {
|
|
280
|
+
pubkey: event.pubkey,
|
|
281
|
+
npub: nostrTools.nip19.npubEncode(event.pubkey),
|
|
282
|
+
entries: [entry],
|
|
283
|
+
eventId: event.id,
|
|
284
|
+
lastSeen: event.created_at
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const agentMap = /* @__PURE__ */ new Map();
|
|
291
|
+
for (const [pubkey, acc] of accumMap) {
|
|
292
|
+
const supportedKinds = [];
|
|
293
|
+
for (const e of acc.entries) {
|
|
294
|
+
for (const k of e.kTags) {
|
|
295
|
+
if (!supportedKinds.includes(k)) supportedKinds.push(k);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
agentMap.set(pubkey, {
|
|
299
|
+
pubkey: acc.pubkey,
|
|
300
|
+
npub: acc.npub,
|
|
301
|
+
cards: acc.entries.map((e) => e.card),
|
|
302
|
+
eventId: acc.eventId,
|
|
303
|
+
supportedKinds,
|
|
304
|
+
lastSeen: acc.lastSeen
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return agentMap;
|
|
308
|
+
}
|
|
309
|
+
var DiscoveryService = class {
|
|
310
|
+
constructor(pool) {
|
|
311
|
+
this.pool = pool;
|
|
312
|
+
}
|
|
313
|
+
// Instance-level set — avoids module-level state leak across clients
|
|
314
|
+
allSeenAgents = /* @__PURE__ */ new Set();
|
|
315
|
+
/** Count elisym agents (kind:31990 with "elisym" tag). */
|
|
316
|
+
async fetchAllAgentCount() {
|
|
317
|
+
const events = await this.pool.querySync({
|
|
318
|
+
kinds: [KIND_APP_HANDLER],
|
|
319
|
+
"#t": ["elisym"]
|
|
320
|
+
});
|
|
321
|
+
for (const event of events) {
|
|
322
|
+
this.allSeenAgents.add(event.pubkey);
|
|
323
|
+
}
|
|
324
|
+
return this.allSeenAgents.size;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Fetch a single page of elisym agents with relay-side pagination.
|
|
328
|
+
* Uses `until` cursor for Nostr cursor-based pagination.
|
|
329
|
+
* Does NOT fetch activity (faster than fetchAgents).
|
|
330
|
+
*/
|
|
331
|
+
async fetchAgentsPage(network = "devnet", limit = 20, until) {
|
|
332
|
+
const filter = {
|
|
333
|
+
kinds: [KIND_APP_HANDLER],
|
|
334
|
+
"#t": ["elisym"],
|
|
335
|
+
limit
|
|
336
|
+
};
|
|
337
|
+
if (until !== void 0) {
|
|
338
|
+
filter.until = until;
|
|
339
|
+
}
|
|
340
|
+
const events = await this.pool.querySync(filter);
|
|
341
|
+
const rawEventCount = events.length;
|
|
342
|
+
let oldestCreatedAt = null;
|
|
343
|
+
for (const event of events) {
|
|
344
|
+
if (oldestCreatedAt === null || event.created_at < oldestCreatedAt) {
|
|
345
|
+
oldestCreatedAt = event.created_at;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const agentMap = buildAgentsFromEvents(events, network);
|
|
349
|
+
const agents = Array.from(agentMap.values()).sort(
|
|
350
|
+
(a, b) => b.lastSeen - a.lastSeen
|
|
351
|
+
);
|
|
352
|
+
return { agents, oldestCreatedAt, rawEventCount };
|
|
353
|
+
}
|
|
354
|
+
/** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
|
|
355
|
+
async enrichWithMetadata(agents) {
|
|
356
|
+
const pubkeys = agents.map((a) => a.pubkey);
|
|
357
|
+
if (pubkeys.length === 0) return agents;
|
|
358
|
+
const metaEvents = await this.pool.queryBatched(
|
|
359
|
+
{ kinds: [0] },
|
|
360
|
+
pubkeys
|
|
361
|
+
);
|
|
362
|
+
const latestMeta = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const ev of metaEvents) {
|
|
364
|
+
const prev = latestMeta.get(ev.pubkey);
|
|
365
|
+
if (!prev || ev.created_at > prev.created_at) {
|
|
366
|
+
latestMeta.set(ev.pubkey, ev);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const agentLookup = new Map(agents.map((a) => [a.pubkey, a]));
|
|
370
|
+
for (const [pubkey, ev] of latestMeta) {
|
|
371
|
+
const agent = agentLookup.get(pubkey);
|
|
372
|
+
if (!agent) continue;
|
|
373
|
+
try {
|
|
374
|
+
const meta = JSON.parse(ev.content);
|
|
375
|
+
if (meta.picture) agent.picture = meta.picture;
|
|
376
|
+
if (meta.name) agent.name = meta.name;
|
|
377
|
+
if (meta.about) agent.about = meta.about;
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return agents;
|
|
382
|
+
}
|
|
383
|
+
/** Fetch elisym agents filtered by network. */
|
|
384
|
+
async fetchAgents(network = "devnet", limit) {
|
|
385
|
+
const filter = {
|
|
386
|
+
kinds: [KIND_APP_HANDLER],
|
|
387
|
+
"#t": ["elisym"]
|
|
388
|
+
};
|
|
389
|
+
if (limit !== void 0) filter.limit = limit;
|
|
390
|
+
const events = await this.pool.querySync(filter);
|
|
391
|
+
const agentMap = buildAgentsFromEvents(events, network);
|
|
392
|
+
const agentPubkeys = Array.from(agentMap.keys());
|
|
393
|
+
if (agentPubkeys.length > 0) {
|
|
394
|
+
const activitySince = Math.floor(Date.now() / 1e3) - 24 * 60 * 60;
|
|
395
|
+
const resultKinds = /* @__PURE__ */ new Set();
|
|
396
|
+
for (const agent of agentMap.values()) {
|
|
397
|
+
for (const k of agent.supportedKinds) {
|
|
398
|
+
if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
|
|
399
|
+
resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
|
|
404
|
+
const activityEvents = await this.pool.queryBatched(
|
|
405
|
+
{
|
|
406
|
+
kinds: [...resultKinds, KIND_JOB_FEEDBACK],
|
|
407
|
+
since: activitySince
|
|
408
|
+
},
|
|
409
|
+
agentPubkeys
|
|
410
|
+
);
|
|
411
|
+
for (const ev of activityEvents) {
|
|
412
|
+
const agent = agentMap.get(ev.pubkey);
|
|
413
|
+
if (agent && ev.created_at > agent.lastSeen) {
|
|
414
|
+
agent.lastSeen = ev.created_at;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const agents = Array.from(agentMap.values()).sort(
|
|
419
|
+
(a, b) => b.lastSeen - a.lastSeen
|
|
420
|
+
);
|
|
421
|
+
await this.enrichWithMetadata(agents);
|
|
422
|
+
return agents;
|
|
423
|
+
}
|
|
424
|
+
/** Publish a capability card (kind:31990) as a provider. */
|
|
425
|
+
async publishCapability(identity, card, kinds = [KIND_JOB_REQUEST]) {
|
|
426
|
+
if (!card.payment?.address) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
"Cannot publish capability without a payment address. Connect a wallet before publishing."
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
const tags = [
|
|
432
|
+
["d", toDTag(card.name)],
|
|
433
|
+
["t", "elisym"],
|
|
434
|
+
...card.capabilities.map((c) => ["t", c]),
|
|
435
|
+
...kinds.map((k) => ["k", String(k)])
|
|
436
|
+
];
|
|
437
|
+
const event = nostrTools.finalizeEvent(
|
|
438
|
+
{
|
|
439
|
+
kind: KIND_APP_HANDLER,
|
|
440
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
441
|
+
tags,
|
|
442
|
+
content: JSON.stringify(card)
|
|
443
|
+
},
|
|
444
|
+
identity.secretKey
|
|
445
|
+
);
|
|
446
|
+
await this.pool.publish(event);
|
|
447
|
+
return event.id;
|
|
448
|
+
}
|
|
449
|
+
/** Publish a Nostr profile (kind:0) as a provider. */
|
|
450
|
+
async publishProfile(identity, name, about, picture) {
|
|
451
|
+
const content = { name, about };
|
|
452
|
+
if (picture) content.picture = picture;
|
|
453
|
+
const event = nostrTools.finalizeEvent(
|
|
454
|
+
{
|
|
455
|
+
kind: 0,
|
|
456
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
457
|
+
tags: [],
|
|
458
|
+
content: JSON.stringify(content)
|
|
459
|
+
},
|
|
460
|
+
identity.secretKey
|
|
461
|
+
);
|
|
462
|
+
await this.pool.publish(event);
|
|
463
|
+
return event.id;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Delete a capability by publishing a tombstone replacement.
|
|
467
|
+
* Since kind:31990 is a parameterized replaceable event,
|
|
468
|
+
* publishing a new event with the same `d` tag and `"deleted":true`
|
|
469
|
+
* content replaces the old one on all relays.
|
|
470
|
+
*/
|
|
471
|
+
async deleteCapability(identity, capabilityName) {
|
|
472
|
+
const dTag = toDTag(capabilityName);
|
|
473
|
+
const event = nostrTools.finalizeEvent(
|
|
474
|
+
{
|
|
475
|
+
kind: KIND_APP_HANDLER,
|
|
476
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
477
|
+
tags: [
|
|
478
|
+
["d", dTag],
|
|
479
|
+
["t", "elisym"]
|
|
480
|
+
],
|
|
481
|
+
content: JSON.stringify({ deleted: true })
|
|
482
|
+
},
|
|
483
|
+
identity.secretKey
|
|
484
|
+
);
|
|
485
|
+
await this.pool.publishAll(event);
|
|
486
|
+
return event.id;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function isEncrypted(event) {
|
|
490
|
+
return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
|
|
491
|
+
}
|
|
492
|
+
function nip44Encrypt(plaintext, secretKey, recipientPubkey) {
|
|
493
|
+
const conversationKey = nip44__namespace.v2.utils.getConversationKey(secretKey, recipientPubkey);
|
|
494
|
+
return nip44__namespace.v2.encrypt(plaintext, conversationKey);
|
|
495
|
+
}
|
|
496
|
+
function nip44Decrypt(ciphertext, secretKey, senderPubkey) {
|
|
497
|
+
const conversationKey = nip44__namespace.v2.utils.getConversationKey(secretKey, senderPubkey);
|
|
498
|
+
return nip44__namespace.v2.decrypt(ciphertext, conversationKey);
|
|
499
|
+
}
|
|
500
|
+
function resolveRequestId(event) {
|
|
501
|
+
return event.tags.find((t) => t[0] === "e")?.[1];
|
|
502
|
+
}
|
|
503
|
+
var MarketplaceService = class {
|
|
504
|
+
constructor(pool) {
|
|
505
|
+
this.pool = pool;
|
|
506
|
+
}
|
|
507
|
+
/** Submit a job request with NIP-44 encrypted input. Returns the event ID. */
|
|
508
|
+
async submitJobRequest(identity, options) {
|
|
509
|
+
if (!options.input) {
|
|
510
|
+
throw new Error("Job input must not be empty.");
|
|
511
|
+
}
|
|
512
|
+
const plaintext = options.input;
|
|
513
|
+
const encrypted = options.providerPubkey ? nip44Encrypt(plaintext, identity.secretKey, options.providerPubkey) : plaintext;
|
|
514
|
+
const iValue = options.providerPubkey ? "encrypted" : "";
|
|
515
|
+
const tags = [
|
|
516
|
+
["i", iValue, "text"],
|
|
517
|
+
["t", options.capability],
|
|
518
|
+
["t", "elisym"],
|
|
519
|
+
["output", "text/plain"]
|
|
520
|
+
];
|
|
521
|
+
if (options.providerPubkey) {
|
|
522
|
+
tags.push(["p", options.providerPubkey]);
|
|
523
|
+
tags.push(["encrypted", "nip44"]);
|
|
524
|
+
}
|
|
525
|
+
const kind = jobRequestKind(options.kindOffset ?? DEFAULT_KIND_OFFSET);
|
|
526
|
+
const event = nostrTools.finalizeEvent(
|
|
527
|
+
{
|
|
528
|
+
kind,
|
|
529
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
530
|
+
tags,
|
|
531
|
+
content: encrypted
|
|
532
|
+
},
|
|
533
|
+
identity.secretKey
|
|
534
|
+
);
|
|
535
|
+
await this.pool.publish(event);
|
|
536
|
+
return event.id;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Subscribe to job updates (feedback + results) for a given job.
|
|
540
|
+
* Returns a cleanup function.
|
|
541
|
+
*/
|
|
542
|
+
subscribeToJobUpdates(jobEventId, providerPubkey, customerPublicKey, callbacks, timeoutMs = 12e4, customerSecretKey, kindOffsets) {
|
|
543
|
+
const offsets = kindOffsets ?? [DEFAULT_KIND_OFFSET];
|
|
544
|
+
const resultKinds = offsets.map(jobResultKind);
|
|
545
|
+
const since = Math.floor(Date.now() / 1e3) - 5;
|
|
546
|
+
const subs = [];
|
|
547
|
+
let resolved = false;
|
|
548
|
+
let resultDelivered = false;
|
|
549
|
+
let timer;
|
|
550
|
+
const done = () => {
|
|
551
|
+
resolved = true;
|
|
552
|
+
if (timer) clearTimeout(timer);
|
|
553
|
+
for (const s of subs) s.close();
|
|
554
|
+
};
|
|
555
|
+
const decryptResult = (ev) => {
|
|
556
|
+
if (customerSecretKey && isEncrypted(ev)) {
|
|
557
|
+
try {
|
|
558
|
+
return nip44Decrypt(ev.content, customerSecretKey, ev.pubkey);
|
|
559
|
+
} catch {
|
|
560
|
+
return ev.content;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return ev.content;
|
|
564
|
+
};
|
|
565
|
+
const handleResult = (ev) => {
|
|
566
|
+
if (resolved || resultDelivered) return;
|
|
567
|
+
if (providerPubkey && ev.pubkey !== providerPubkey) return;
|
|
568
|
+
resultDelivered = true;
|
|
569
|
+
callbacks.onResult?.(decryptResult(ev), ev.id);
|
|
570
|
+
done();
|
|
571
|
+
};
|
|
572
|
+
const feedbackSub = this.pool.subscribe(
|
|
573
|
+
{
|
|
574
|
+
kinds: [KIND_JOB_FEEDBACK],
|
|
575
|
+
"#e": [jobEventId],
|
|
576
|
+
since
|
|
577
|
+
},
|
|
578
|
+
(ev) => {
|
|
579
|
+
if (resolved) return;
|
|
580
|
+
if (providerPubkey && ev.pubkey !== providerPubkey) return;
|
|
581
|
+
const statusTag = ev.tags.find((t) => t[0] === "status");
|
|
582
|
+
if (statusTag?.[1] === "payment-required") {
|
|
583
|
+
const amtTag = ev.tags.find((t) => t[0] === "amount");
|
|
584
|
+
const amt = amtTag?.[1] ? parseInt(amtTag[1], 10) : 0;
|
|
585
|
+
const paymentReq = amtTag?.[2];
|
|
586
|
+
callbacks.onFeedback?.("payment-required", amt, paymentReq);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
subs.push(feedbackSub);
|
|
591
|
+
const resultSub = this.pool.subscribe(
|
|
592
|
+
{
|
|
593
|
+
kinds: resultKinds,
|
|
594
|
+
"#e": [jobEventId],
|
|
595
|
+
since
|
|
596
|
+
},
|
|
597
|
+
handleResult
|
|
598
|
+
);
|
|
599
|
+
subs.push(resultSub);
|
|
600
|
+
const resultSub2 = this.pool.subscribe(
|
|
601
|
+
{
|
|
602
|
+
kinds: resultKinds,
|
|
603
|
+
"#p": [customerPublicKey],
|
|
604
|
+
"#e": [jobEventId],
|
|
605
|
+
since
|
|
606
|
+
},
|
|
607
|
+
handleResult
|
|
608
|
+
);
|
|
609
|
+
subs.push(resultSub2);
|
|
610
|
+
timer = setTimeout(() => {
|
|
611
|
+
if (!resolved) {
|
|
612
|
+
done();
|
|
613
|
+
callbacks.onError?.(`Timed out waiting for response (${timeoutMs / 1e3}s).`);
|
|
614
|
+
}
|
|
615
|
+
}, timeoutMs);
|
|
616
|
+
return done;
|
|
617
|
+
}
|
|
618
|
+
/** Submit payment confirmation feedback. */
|
|
619
|
+
async submitPaymentConfirmation(identity, jobEventId, providerPubkey, txSignature) {
|
|
620
|
+
const event = nostrTools.finalizeEvent(
|
|
621
|
+
{
|
|
622
|
+
kind: KIND_JOB_FEEDBACK,
|
|
623
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
624
|
+
tags: [
|
|
625
|
+
["e", jobEventId],
|
|
626
|
+
["p", providerPubkey],
|
|
627
|
+
["status", "payment-completed"],
|
|
628
|
+
["tx", txSignature, "solana"],
|
|
629
|
+
["t", "elisym"]
|
|
630
|
+
],
|
|
631
|
+
content: ""
|
|
632
|
+
},
|
|
633
|
+
identity.secretKey
|
|
634
|
+
);
|
|
635
|
+
await this.pool.publish(event);
|
|
636
|
+
}
|
|
637
|
+
/** Submit rating feedback for a job. */
|
|
638
|
+
async submitFeedback(identity, jobEventId, providerPubkey, positive, capability) {
|
|
639
|
+
const tags = [
|
|
640
|
+
["e", jobEventId],
|
|
641
|
+
["p", providerPubkey],
|
|
642
|
+
["status", "success"],
|
|
643
|
+
["rating", positive ? "1" : "0"],
|
|
644
|
+
["t", "elisym"]
|
|
645
|
+
];
|
|
646
|
+
if (capability) tags.push(["t", capability]);
|
|
647
|
+
const event = nostrTools.finalizeEvent(
|
|
648
|
+
{
|
|
649
|
+
kind: KIND_JOB_FEEDBACK,
|
|
650
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
651
|
+
tags,
|
|
652
|
+
content: positive ? "Good result" : "Poor result"
|
|
653
|
+
},
|
|
654
|
+
identity.secretKey
|
|
655
|
+
);
|
|
656
|
+
await this.pool.publish(event);
|
|
657
|
+
}
|
|
658
|
+
// --- Provider methods ---
|
|
659
|
+
/** Subscribe to incoming job requests for specific kinds. */
|
|
660
|
+
subscribeToJobRequests(identity, kinds, onRequest) {
|
|
661
|
+
return this.pool.subscribe(
|
|
662
|
+
{
|
|
663
|
+
kinds,
|
|
664
|
+
"#p": [identity.publicKey],
|
|
665
|
+
since: Math.floor(Date.now() / 1e3)
|
|
666
|
+
},
|
|
667
|
+
onRequest
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
/** Submit a job result with NIP-44 encrypted content. Result kind is derived from the request kind. */
|
|
671
|
+
async submitJobResult(identity, requestEvent, content, amount) {
|
|
672
|
+
const encrypted = nip44Encrypt(content, identity.secretKey, requestEvent.pubkey);
|
|
673
|
+
const resultKind = KIND_JOB_RESULT_BASE + (requestEvent.kind - KIND_JOB_REQUEST_BASE);
|
|
674
|
+
const tags = [
|
|
675
|
+
["e", requestEvent.id],
|
|
676
|
+
["p", requestEvent.pubkey],
|
|
677
|
+
["t", "elisym"],
|
|
678
|
+
["encrypted", "nip44"]
|
|
679
|
+
];
|
|
680
|
+
if (amount != null) {
|
|
681
|
+
tags.push(["amount", String(amount)]);
|
|
682
|
+
}
|
|
683
|
+
const event = nostrTools.finalizeEvent(
|
|
684
|
+
{
|
|
685
|
+
kind: resultKind,
|
|
686
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
687
|
+
tags,
|
|
688
|
+
content: encrypted
|
|
689
|
+
},
|
|
690
|
+
identity.secretKey
|
|
691
|
+
);
|
|
692
|
+
await this.pool.publish(event);
|
|
693
|
+
return event.id;
|
|
694
|
+
}
|
|
695
|
+
/** Submit payment-required feedback with a payment request. */
|
|
696
|
+
async submitPaymentRequiredFeedback(identity, requestEvent, amount, paymentRequestJson) {
|
|
697
|
+
const event = nostrTools.finalizeEvent(
|
|
698
|
+
{
|
|
699
|
+
kind: KIND_JOB_FEEDBACK,
|
|
700
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
701
|
+
tags: [
|
|
702
|
+
["e", requestEvent.id],
|
|
703
|
+
["p", requestEvent.pubkey],
|
|
704
|
+
["status", "payment-required"],
|
|
705
|
+
["amount", String(amount), paymentRequestJson, "solana"],
|
|
706
|
+
["t", "elisym"]
|
|
707
|
+
],
|
|
708
|
+
content: ""
|
|
709
|
+
},
|
|
710
|
+
identity.secretKey
|
|
711
|
+
);
|
|
712
|
+
await this.pool.publish(event);
|
|
713
|
+
}
|
|
714
|
+
// --- Query methods ---
|
|
715
|
+
/** Fetch recent jobs from the network. */
|
|
716
|
+
async fetchRecentJobs(agentPubkeys, limit, since, kindOffsets) {
|
|
717
|
+
const offsets = kindOffsets ?? [DEFAULT_KIND_OFFSET];
|
|
718
|
+
const requestKinds = offsets.map(jobRequestKind);
|
|
719
|
+
const resultKinds = offsets.map(jobResultKind);
|
|
720
|
+
const reqFilter = {
|
|
721
|
+
kinds: requestKinds,
|
|
722
|
+
"#t": ["elisym"],
|
|
723
|
+
...limit != null && { limit },
|
|
724
|
+
...since != null && { since }
|
|
725
|
+
};
|
|
726
|
+
const requests = await this.pool.querySync(reqFilter);
|
|
727
|
+
const requestIds = requests.map((r) => r.id);
|
|
728
|
+
let results = [];
|
|
729
|
+
let feedbacks = [];
|
|
730
|
+
if (requestIds.length > 0) {
|
|
731
|
+
const [resultArrays, feedbackArrays] = await Promise.all([
|
|
732
|
+
this.pool.queryBatchedByTag(
|
|
733
|
+
{ kinds: resultKinds },
|
|
734
|
+
"e",
|
|
735
|
+
requestIds
|
|
736
|
+
),
|
|
737
|
+
this.pool.queryBatchedByTag(
|
|
738
|
+
{ kinds: [KIND_JOB_FEEDBACK] },
|
|
739
|
+
"e",
|
|
740
|
+
requestIds
|
|
741
|
+
)
|
|
742
|
+
]);
|
|
743
|
+
results = resultArrays;
|
|
744
|
+
feedbacks = feedbackArrays;
|
|
745
|
+
}
|
|
746
|
+
const targetedAgentByRequest = /* @__PURE__ */ new Map();
|
|
747
|
+
for (const req of requests) {
|
|
748
|
+
const pTag = req.tags.find((t) => t[0] === "p");
|
|
749
|
+
if (pTag?.[1]) targetedAgentByRequest.set(req.id, pTag[1]);
|
|
750
|
+
}
|
|
751
|
+
const resultsByRequest = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const r of results) {
|
|
753
|
+
const reqId = resolveRequestId(r);
|
|
754
|
+
if (!reqId) continue;
|
|
755
|
+
const targeted = targetedAgentByRequest.get(reqId);
|
|
756
|
+
if (targeted && r.pubkey !== targeted) continue;
|
|
757
|
+
const existing = resultsByRequest.get(reqId);
|
|
758
|
+
if (!existing || targeted && r.pubkey === targeted) {
|
|
759
|
+
resultsByRequest.set(reqId, r);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const feedbackByRequest = /* @__PURE__ */ new Map();
|
|
763
|
+
for (const f of feedbacks) {
|
|
764
|
+
const reqId = resolveRequestId(f);
|
|
765
|
+
if (!reqId) continue;
|
|
766
|
+
const targeted = targetedAgentByRequest.get(reqId);
|
|
767
|
+
if (targeted && f.pubkey !== targeted) continue;
|
|
768
|
+
const existing = feedbackByRequest.get(reqId);
|
|
769
|
+
if (!existing || targeted && f.pubkey === targeted) {
|
|
770
|
+
feedbackByRequest.set(reqId, f);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const jobs = [];
|
|
774
|
+
for (const req of requests) {
|
|
775
|
+
const result = resultsByRequest.get(req.id);
|
|
776
|
+
const feedback = feedbackByRequest.get(req.id);
|
|
777
|
+
const jobAgentPubkey = result?.pubkey ?? feedback?.pubkey;
|
|
778
|
+
if (agentPubkeys && agentPubkeys.size > 0 && jobAgentPubkey) {
|
|
779
|
+
if (!agentPubkeys.has(jobAgentPubkey)) continue;
|
|
780
|
+
}
|
|
781
|
+
const capability = req.tags.find((t) => t[0] === "t" && t[1] !== "elisym")?.[1];
|
|
782
|
+
const bid = req.tags.find((t) => t[0] === "bid")?.[1];
|
|
783
|
+
let status = "processing";
|
|
784
|
+
let amount;
|
|
785
|
+
let txHash;
|
|
786
|
+
if (result) {
|
|
787
|
+
status = "success";
|
|
788
|
+
const amtTag = result.tags.find((t) => t[0] === "amount");
|
|
789
|
+
if (amtTag?.[1]) amount = parseInt(amtTag[1], 10);
|
|
790
|
+
}
|
|
791
|
+
const allFeedbacksForReq = feedbacks.filter(
|
|
792
|
+
(f) => resolveRequestId(f) === req.id
|
|
793
|
+
);
|
|
794
|
+
for (const fb of allFeedbacksForReq) {
|
|
795
|
+
const txTag = fb.tags.find((t) => t[0] === "tx");
|
|
796
|
+
if (txTag?.[1]) {
|
|
797
|
+
txHash = txTag[1];
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (feedback) {
|
|
802
|
+
if (!result) {
|
|
803
|
+
const statusTag = feedback.tags.find((t) => t[0] === "status");
|
|
804
|
+
if (statusTag?.[1]) {
|
|
805
|
+
const isTargeted = targetedAgentByRequest.has(req.id);
|
|
806
|
+
if (statusTag[1] === "payment-required" && !bid && !isTargeted) ; else {
|
|
807
|
+
status = statusTag[1];
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (!amount) {
|
|
812
|
+
const amtTag = feedback.tags.find((t) => t[0] === "amount");
|
|
813
|
+
if (amtTag?.[1]) amount = parseInt(amtTag[1], 10);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
jobs.push({
|
|
817
|
+
eventId: req.id,
|
|
818
|
+
customer: req.pubkey,
|
|
819
|
+
agentPubkey: jobAgentPubkey,
|
|
820
|
+
capability,
|
|
821
|
+
bid: bid ? parseInt(bid, 10) : void 0,
|
|
822
|
+
status,
|
|
823
|
+
result: result?.content,
|
|
824
|
+
resultEventId: result?.id,
|
|
825
|
+
amount,
|
|
826
|
+
txHash,
|
|
827
|
+
createdAt: req.created_at
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return jobs.sort((a, b) => b.createdAt - a.createdAt);
|
|
831
|
+
}
|
|
832
|
+
/** Subscribe to live elisym events (requests, results, feedback). */
|
|
833
|
+
subscribeToEvents(kinds, onEvent) {
|
|
834
|
+
return this.pool.subscribe(
|
|
835
|
+
{
|
|
836
|
+
kinds,
|
|
837
|
+
"#t": ["elisym"],
|
|
838
|
+
since: Math.floor(Date.now() / 1e3)
|
|
839
|
+
},
|
|
840
|
+
onEvent
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
var MessagingService = class _MessagingService {
|
|
845
|
+
// 30s
|
|
846
|
+
constructor(pool) {
|
|
847
|
+
this.pool = pool;
|
|
848
|
+
this.sessionIdentity = ElisymIdentity.generate();
|
|
849
|
+
}
|
|
850
|
+
sessionIdentity;
|
|
851
|
+
pingCache = /* @__PURE__ */ new Map();
|
|
852
|
+
// pubkey → timestamp of last online result
|
|
853
|
+
pendingPings = /* @__PURE__ */ new Map();
|
|
854
|
+
// dedup in-flight pings
|
|
855
|
+
static PING_CACHE_TTL = 3e4;
|
|
856
|
+
/**
|
|
857
|
+
* Ping an agent via ephemeral Nostr events (kind 20200/20201).
|
|
858
|
+
* Uses a persistent session identity to avoid relay rate-limiting.
|
|
859
|
+
* Publishes to ALL relays for maximum delivery reliability.
|
|
860
|
+
* Caches results for 30s to prevent redundant publishes.
|
|
861
|
+
*/
|
|
862
|
+
async pingAgent(agentPubkey, timeoutMs = 15e3, signal) {
|
|
863
|
+
const cachedAt = this.pingCache.get(agentPubkey);
|
|
864
|
+
if (cachedAt && Date.now() - cachedAt < _MessagingService.PING_CACHE_TTL) {
|
|
865
|
+
console.log(`[ping] cache hit for ${agentPubkey.slice(0, 8)}: online`);
|
|
866
|
+
return { online: true, identity: this.sessionIdentity };
|
|
867
|
+
}
|
|
868
|
+
const pending = this.pendingPings.get(agentPubkey);
|
|
869
|
+
if (pending) {
|
|
870
|
+
console.log(`[ping] dedup: reusing in-flight ping for ${agentPubkey.slice(0, 8)}`);
|
|
871
|
+
return pending;
|
|
872
|
+
}
|
|
873
|
+
const promise = this._doPing(agentPubkey, timeoutMs, signal);
|
|
874
|
+
this.pendingPings.set(agentPubkey, promise);
|
|
875
|
+
promise.finally(() => this.pendingPings.delete(agentPubkey));
|
|
876
|
+
return promise;
|
|
877
|
+
}
|
|
878
|
+
async _doPing(agentPubkey, timeoutMs, signal) {
|
|
879
|
+
const sk = this.sessionIdentity.secretKey;
|
|
880
|
+
const pk = this.sessionIdentity.publicKey;
|
|
881
|
+
const nonce = crypto.getRandomValues(new Uint8Array(16)).reduce((s, b) => s + b.toString(16).padStart(2, "0"), "");
|
|
882
|
+
const shortNonce = nonce.slice(0, 8);
|
|
883
|
+
const shortAgent = agentPubkey.slice(0, 8);
|
|
884
|
+
console.log(`[ping] \u2192 ping ${shortAgent} nonce=${shortNonce}`);
|
|
885
|
+
if (signal?.aborted) {
|
|
886
|
+
return { online: false, identity: null };
|
|
887
|
+
}
|
|
888
|
+
return new Promise(async (resolve) => {
|
|
889
|
+
let resolved = false;
|
|
890
|
+
const done = (online, reason) => {
|
|
891
|
+
if (resolved) return;
|
|
892
|
+
resolved = true;
|
|
893
|
+
clearTimeout(timer);
|
|
894
|
+
sub.close();
|
|
895
|
+
signal?.removeEventListener("abort", onAbort);
|
|
896
|
+
console.log(`[ping] ${online ? "\u2713 ONLINE" : "\u2717 OFFLINE"} agent=${shortAgent} nonce=${shortNonce}${reason ? ` (${reason})` : ""}`);
|
|
897
|
+
if (online) this.pingCache.set(agentPubkey, Date.now());
|
|
898
|
+
resolve({ online, identity: online ? this.sessionIdentity : null });
|
|
899
|
+
};
|
|
900
|
+
const onAbort = () => done(false, "aborted");
|
|
901
|
+
signal?.addEventListener("abort", onAbort);
|
|
902
|
+
const sub = this.pool.subscribe(
|
|
903
|
+
{ kinds: [KIND_PONG], "#p": [pk] },
|
|
904
|
+
(ev) => {
|
|
905
|
+
try {
|
|
906
|
+
const msg = JSON.parse(ev.content);
|
|
907
|
+
if (msg.type === "elisym_pong" && msg.nonce === nonce) {
|
|
908
|
+
console.log(`[ping] \u2190 pong from ${ev.pubkey.slice(0, 8)} nonce=${shortNonce}`);
|
|
909
|
+
done(true, "pong matched");
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
const pingEvent = nostrTools.finalizeEvent(
|
|
916
|
+
{
|
|
917
|
+
kind: KIND_PING,
|
|
918
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
919
|
+
tags: [["p", agentPubkey]],
|
|
920
|
+
content: JSON.stringify({ type: "elisym_ping", nonce })
|
|
921
|
+
},
|
|
922
|
+
sk
|
|
923
|
+
);
|
|
924
|
+
this.pool.publishAll(pingEvent).then(() => console.log(`[ping] \u2713 published nonce=${shortNonce}`)).catch((err) => {
|
|
925
|
+
console.error(`[ping] \u2717 publish failed nonce=${shortNonce}`, err);
|
|
926
|
+
done(false, "publish failed");
|
|
927
|
+
});
|
|
928
|
+
const timer = setTimeout(() => done(false, "timeout"), timeoutMs);
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Subscribe to incoming ephemeral ping events (kind 20200).
|
|
933
|
+
* No `since` filter needed - ephemeral events are never stored.
|
|
934
|
+
*/
|
|
935
|
+
subscribeToPings(identity, onPing) {
|
|
936
|
+
return this.pool.subscribe(
|
|
937
|
+
{ kinds: [KIND_PING], "#p": [identity.publicKey] },
|
|
938
|
+
(ev) => {
|
|
939
|
+
try {
|
|
940
|
+
const msg = JSON.parse(ev.content);
|
|
941
|
+
if (msg.type === "elisym_ping" && msg.nonce) {
|
|
942
|
+
onPing(ev.pubkey, msg.nonce);
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
/** Send an ephemeral pong response to ALL relays. */
|
|
950
|
+
async sendPong(identity, recipientPubkey, nonce) {
|
|
951
|
+
const pongEvent = nostrTools.finalizeEvent(
|
|
952
|
+
{
|
|
953
|
+
kind: KIND_PONG,
|
|
954
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
955
|
+
tags: [["p", recipientPubkey]],
|
|
956
|
+
content: JSON.stringify({ type: "elisym_pong", nonce })
|
|
957
|
+
},
|
|
958
|
+
identity.secretKey
|
|
959
|
+
);
|
|
960
|
+
await this.pool.publishAll(pongEvent);
|
|
961
|
+
}
|
|
962
|
+
/** Send a NIP-17 DM. */
|
|
963
|
+
async sendMessage(identity, recipientPubkey, content) {
|
|
964
|
+
const wrap = nip17__namespace.wrapEvent(
|
|
965
|
+
identity.secretKey,
|
|
966
|
+
{ publicKey: recipientPubkey },
|
|
967
|
+
content
|
|
968
|
+
);
|
|
969
|
+
await this.pool.publish(wrap);
|
|
970
|
+
}
|
|
971
|
+
/** Fetch historical NIP-17 DMs from relays. Returns decrypted messages sorted by time. */
|
|
972
|
+
async fetchMessageHistory(identity, since) {
|
|
973
|
+
const events = await this.pool.querySync({
|
|
974
|
+
kinds: [KIND_GIFT_WRAP],
|
|
975
|
+
"#p": [identity.publicKey],
|
|
976
|
+
since
|
|
977
|
+
});
|
|
978
|
+
const seen = /* @__PURE__ */ new Set();
|
|
979
|
+
const messages = [];
|
|
980
|
+
for (const ev of events) {
|
|
981
|
+
try {
|
|
982
|
+
const rumor = nip59__namespace.unwrapEvent(ev, identity.secretKey);
|
|
983
|
+
if (seen.has(rumor.id)) continue;
|
|
984
|
+
seen.add(rumor.id);
|
|
985
|
+
messages.push({
|
|
986
|
+
senderPubkey: rumor.pubkey,
|
|
987
|
+
content: rumor.content,
|
|
988
|
+
createdAt: rumor.created_at,
|
|
989
|
+
rumorId: rumor.id
|
|
990
|
+
});
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return messages.sort((a, b) => a.createdAt - b.createdAt);
|
|
995
|
+
}
|
|
996
|
+
/** Subscribe to incoming NIP-17 DMs. */
|
|
997
|
+
subscribeToMessages(identity, onMessage, since) {
|
|
998
|
+
const seen = /* @__PURE__ */ new Set();
|
|
999
|
+
const filter = {
|
|
1000
|
+
kinds: [KIND_GIFT_WRAP],
|
|
1001
|
+
"#p": [identity.publicKey]
|
|
1002
|
+
};
|
|
1003
|
+
if (since !== void 0) filter.since = since;
|
|
1004
|
+
return this.pool.subscribe(
|
|
1005
|
+
filter,
|
|
1006
|
+
(ev) => {
|
|
1007
|
+
try {
|
|
1008
|
+
const rumor = nip59__namespace.unwrapEvent(ev, identity.secretKey);
|
|
1009
|
+
if (seen.has(rumor.id)) return;
|
|
1010
|
+
seen.add(rumor.id);
|
|
1011
|
+
onMessage(rumor.pubkey, rumor.content, rumor.created_at, rumor.id);
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
var PaymentService = class _PaymentService {
|
|
1019
|
+
/**
|
|
1020
|
+
* Calculate protocol fee using Decimal basis-point math (no floats).
|
|
1021
|
+
* Returns ceil(amount * PROTOCOL_FEE_BPS / 10000).
|
|
1022
|
+
*/
|
|
1023
|
+
static calculateProtocolFee(amount) {
|
|
1024
|
+
if (amount === 0) return 0;
|
|
1025
|
+
return new Decimal__default.default(amount).mul(PROTOCOL_FEE_BPS).div(1e4).toDecimalPlaces(0, Decimal__default.default.ROUND_CEIL).toNumber();
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Validate that a payment request has the correct recipient and protocol fee.
|
|
1029
|
+
* Returns an error message if invalid, null if OK.
|
|
1030
|
+
*/
|
|
1031
|
+
static validatePaymentFee(requestJson, expectedRecipient) {
|
|
1032
|
+
let data;
|
|
1033
|
+
try {
|
|
1034
|
+
data = JSON.parse(requestJson);
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
return `Invalid payment request JSON: ${e}`;
|
|
1037
|
+
}
|
|
1038
|
+
if (expectedRecipient && data.recipient !== expectedRecipient) {
|
|
1039
|
+
return `Recipient mismatch: expected ${expectedRecipient}, got ${data.recipient}. Provider may be attempting to redirect payment.`;
|
|
1040
|
+
}
|
|
1041
|
+
if (data.created_at > 0 && data.expiry_secs > 0) {
|
|
1042
|
+
const elapsed = Math.floor(Date.now() / 1e3) - data.created_at;
|
|
1043
|
+
if (elapsed > data.expiry_secs) {
|
|
1044
|
+
return `Payment request expired (created ${data.created_at}, expiry ${data.expiry_secs}s).`;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const expectedFee = _PaymentService.calculateProtocolFee(data.amount);
|
|
1048
|
+
const { fee_address, fee_amount } = data;
|
|
1049
|
+
if (fee_address && fee_amount && fee_amount > 0) {
|
|
1050
|
+
if (fee_address !== PROTOCOL_TREASURY) {
|
|
1051
|
+
return `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`;
|
|
1052
|
+
}
|
|
1053
|
+
if (fee_amount !== expectedFee) {
|
|
1054
|
+
return `Fee amount mismatch: expected ${expectedFee} lamports (${PROTOCOL_FEE_BPS}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`;
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
if (!fee_address && !fee_amount) {
|
|
1059
|
+
return `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`;
|
|
1060
|
+
}
|
|
1061
|
+
return `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Build a Solana transaction from a payment request.
|
|
1065
|
+
* The caller must sign and send via wallet adapter.
|
|
1066
|
+
*/
|
|
1067
|
+
static buildPaymentTransaction(payerPubkey, paymentRequest) {
|
|
1068
|
+
const recipient = new web3_js.PublicKey(paymentRequest.recipient);
|
|
1069
|
+
const reference = new web3_js.PublicKey(paymentRequest.reference);
|
|
1070
|
+
const feeAddress = paymentRequest.fee_address ? new web3_js.PublicKey(paymentRequest.fee_address) : null;
|
|
1071
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
1072
|
+
const providerAmount = feeAddress && feeAmount > 0 ? new Decimal__default.default(paymentRequest.amount).minus(feeAmount).toNumber() : paymentRequest.amount;
|
|
1073
|
+
const transferIx = web3_js.SystemProgram.transfer({
|
|
1074
|
+
fromPubkey: payerPubkey,
|
|
1075
|
+
toPubkey: recipient,
|
|
1076
|
+
lamports: providerAmount
|
|
1077
|
+
});
|
|
1078
|
+
transferIx.keys.push({
|
|
1079
|
+
pubkey: reference,
|
|
1080
|
+
isSigner: false,
|
|
1081
|
+
isWritable: false
|
|
1082
|
+
});
|
|
1083
|
+
const tx = new web3_js.Transaction().add(transferIx);
|
|
1084
|
+
if (feeAddress && feeAmount > 0) {
|
|
1085
|
+
tx.add(
|
|
1086
|
+
web3_js.SystemProgram.transfer({
|
|
1087
|
+
fromPubkey: payerPubkey,
|
|
1088
|
+
toPubkey: feeAddress,
|
|
1089
|
+
lamports: feeAmount
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return tx;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Create a payment request with auto-calculated protocol fee.
|
|
1097
|
+
* Used by providers to generate payment requests for customers.
|
|
1098
|
+
*/
|
|
1099
|
+
static createPaymentRequest(recipientAddress, amount, expirySecs = 600) {
|
|
1100
|
+
const feeAmount = _PaymentService.calculateProtocolFee(amount);
|
|
1101
|
+
const reference = web3_js.PublicKey.unique().toBase58();
|
|
1102
|
+
return {
|
|
1103
|
+
recipient: recipientAddress,
|
|
1104
|
+
amount,
|
|
1105
|
+
reference,
|
|
1106
|
+
fee_address: PROTOCOL_TREASURY,
|
|
1107
|
+
fee_amount: feeAmount,
|
|
1108
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1109
|
+
expiry_secs: expirySecs
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
function formatSol(lamports) {
|
|
1114
|
+
const sol = new Decimal__default.default(lamports).div(LAMPORTS_PER_SOL);
|
|
1115
|
+
if (sol.gte(1e6)) return `${sol.idiv(1e6)}m SOL`;
|
|
1116
|
+
if (sol.gte(1e4)) return `${sol.idiv(1e3)}k SOL`;
|
|
1117
|
+
return `${compactSol(sol)} SOL`;
|
|
1118
|
+
}
|
|
1119
|
+
function compactSol(sol) {
|
|
1120
|
+
if (sol.isZero()) return "0";
|
|
1121
|
+
if (sol.gte(1e3)) return sol.toDecimalPlaces(0, Decimal__default.default.ROUND_FLOOR).toString();
|
|
1122
|
+
const maxFrac = 9;
|
|
1123
|
+
for (let d = 1; d <= maxFrac; d++) {
|
|
1124
|
+
const s = sol.toFixed(d);
|
|
1125
|
+
if (new Decimal__default.default(s).eq(sol)) {
|
|
1126
|
+
return s.replace(/0+$/, "").replace(/\.$/, "");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return sol.toFixed(maxFrac).replace(/0+$/, "").replace(/\.$/, "");
|
|
1130
|
+
}
|
|
1131
|
+
function timeAgo(unix) {
|
|
1132
|
+
const seconds = Math.max(0, Math.floor(Date.now() / 1e3 - unix));
|
|
1133
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
1134
|
+
const minutes = Math.floor(seconds / 60);
|
|
1135
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
1136
|
+
const hours = Math.floor(minutes / 60);
|
|
1137
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1138
|
+
const days = Math.floor(hours / 24);
|
|
1139
|
+
return `${days}d ago`;
|
|
1140
|
+
}
|
|
1141
|
+
function truncateKey(hex, chars = 6) {
|
|
1142
|
+
if (hex.length <= chars * 2) return hex;
|
|
1143
|
+
return `${hex.slice(0, chars)}...${hex.slice(-chars)}`;
|
|
1144
|
+
}
|
|
1145
|
+
function makeNjumpUrl(eventId, relays = RELAYS) {
|
|
1146
|
+
const nevent = nostrTools.nip19.neventEncode({
|
|
1147
|
+
id: eventId,
|
|
1148
|
+
relays: relays.slice(0, 2)
|
|
1149
|
+
});
|
|
1150
|
+
return `https://njump.me/${nevent}`;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/index.ts
|
|
1154
|
+
var ElisymClient = class {
|
|
1155
|
+
pool;
|
|
1156
|
+
discovery;
|
|
1157
|
+
marketplace;
|
|
1158
|
+
messaging;
|
|
1159
|
+
constructor(config = {}) {
|
|
1160
|
+
this.pool = new NostrPool(config.relays ?? RELAYS);
|
|
1161
|
+
this.discovery = new DiscoveryService(this.pool);
|
|
1162
|
+
this.marketplace = new MarketplaceService(this.pool);
|
|
1163
|
+
this.messaging = new MessagingService(this.pool);
|
|
1164
|
+
}
|
|
1165
|
+
close() {
|
|
1166
|
+
this.pool.close();
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
exports.DEFAULT_KIND_OFFSET = DEFAULT_KIND_OFFSET;
|
|
1171
|
+
exports.DiscoveryService = DiscoveryService;
|
|
1172
|
+
exports.ElisymClient = ElisymClient;
|
|
1173
|
+
exports.ElisymIdentity = ElisymIdentity;
|
|
1174
|
+
exports.KIND_APP_HANDLER = KIND_APP_HANDLER;
|
|
1175
|
+
exports.KIND_GIFT_WRAP = KIND_GIFT_WRAP;
|
|
1176
|
+
exports.KIND_JOB_FEEDBACK = KIND_JOB_FEEDBACK;
|
|
1177
|
+
exports.KIND_JOB_REQUEST = KIND_JOB_REQUEST;
|
|
1178
|
+
exports.KIND_JOB_REQUEST_BASE = KIND_JOB_REQUEST_BASE;
|
|
1179
|
+
exports.KIND_JOB_RESULT = KIND_JOB_RESULT;
|
|
1180
|
+
exports.KIND_JOB_RESULT_BASE = KIND_JOB_RESULT_BASE;
|
|
1181
|
+
exports.KIND_PING = KIND_PING;
|
|
1182
|
+
exports.KIND_PONG = KIND_PONG;
|
|
1183
|
+
exports.LAMPORTS_PER_SOL = LAMPORTS_PER_SOL;
|
|
1184
|
+
exports.MarketplaceService = MarketplaceService;
|
|
1185
|
+
exports.MessagingService = MessagingService;
|
|
1186
|
+
exports.NostrPool = NostrPool;
|
|
1187
|
+
exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
|
|
1188
|
+
exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
|
|
1189
|
+
exports.PaymentService = PaymentService;
|
|
1190
|
+
exports.RELAYS = RELAYS;
|
|
1191
|
+
exports.formatSol = formatSol;
|
|
1192
|
+
exports.jobRequestKind = jobRequestKind;
|
|
1193
|
+
exports.jobResultKind = jobResultKind;
|
|
1194
|
+
exports.makeNjumpUrl = makeNjumpUrl;
|
|
1195
|
+
exports.timeAgo = timeAgo;
|
|
1196
|
+
exports.toDTag = toDTag;
|
|
1197
|
+
exports.truncateKey = truncateKey;
|
|
1198
|
+
//# sourceMappingURL=index.cjs.map
|
|
1199
|
+
//# sourceMappingURL=index.cjs.map
|