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