@elisym/sdk 0.10.4 → 0.12.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/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var memo = require('@solana-program/memo');
3
4
  var system = require('@solana-program/system');
4
5
  var token = require('@solana-program/token');
5
6
  var kit = require('@solana/kit');
@@ -64,6 +65,7 @@ var LAMPORTS_PER_SOL = 1e9;
64
65
  var PROTOCOL_FEE_BPS = 300;
65
66
  var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
66
67
  var PROTOCOL_PROGRAM_ID_DEVNET = "BrX1CRkSgvcjxBvc2bgc3QqgWjinusofDmeP7ZVxvwrE";
68
+ var ELISYM_PROTOCOL_TAG = "ELiZksgwDt41LaeuPDLkUfWgFXhGgVayTMP7L5nTSEL8";
67
69
  function getProtocolProgramId(cluster) {
68
70
  switch (cluster) {
69
71
  case "devnet":
@@ -119,13 +121,13 @@ function decodeConfig(encodedAccount) {
119
121
  getConfigDecoder()
120
122
  );
121
123
  }
122
- async function fetchConfig(rpc, address3, config) {
123
- const maybeAccount = await fetchMaybeConfig(rpc, address3, config);
124
+ async function fetchConfig(rpc, address4, config) {
125
+ const maybeAccount = await fetchMaybeConfig(rpc, address4, config);
124
126
  kit.assertAccountExists(maybeAccount);
125
127
  return maybeAccount;
126
128
  }
127
- async function fetchMaybeConfig(rpc, address3, config) {
128
- const maybeAccount = await kit.fetchEncodedAccount(rpc, address3, config);
129
+ async function fetchMaybeConfig(rpc, address4, config) {
130
+ const maybeAccount = await kit.fetchEncodedAccount(rpc, address4, config);
129
131
  return decodeConfig(maybeAccount);
130
132
  }
131
133
  if (process.env.NODE_ENV !== "production") ;
@@ -597,7 +599,9 @@ var SolanaPaymentStrategy = class {
597
599
  if (!Number.isInteger(computeUnitLimit) || computeUnitLimit <= 0) {
598
600
  throw new Error(`Invalid computeUnitLimit: ${computeUnitLimit}. Must be a positive integer.`);
599
601
  }
600
- const paymentInstructions = await buildPaymentInstructions(paymentRequest, payerSigner);
602
+ const paymentInstructions = await buildPaymentInstructions(paymentRequest, payerSigner, {
603
+ jobEventId: options?.jobEventId
604
+ });
601
605
  const priorityFeeMicroLamports = options?.priorityFeeMicroLamports ?? await estimatePriorityFeeMicroLamports(rpc, {
602
606
  percentile: options?.priorityFeePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE
603
607
  });
@@ -894,9 +898,10 @@ function bigIntDelta(post, pre) {
894
898
  function waitMs(ms) {
895
899
  return new Promise((resolve) => setTimeout(resolve, ms));
896
900
  }
897
- async function buildPaymentInstructions(paymentRequest, payerSigner) {
901
+ async function buildPaymentInstructions(paymentRequest, payerSigner, options) {
898
902
  const recipient = kit.address(paymentRequest.recipient);
899
903
  const reference = kit.address(paymentRequest.reference);
904
+ const protocolTag = kit.address(ELISYM_PROTOCOL_TAG);
900
905
  const feeAmount = paymentRequest.fee_amount ?? 0;
901
906
  const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
902
907
  if (providerAmount <= 0) {
@@ -904,6 +909,7 @@ async function buildPaymentInstructions(paymentRequest, payerSigner) {
904
909
  `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
905
910
  );
906
911
  }
912
+ const memoInstruction = options?.jobEventId ? memo.getAddMemoInstruction({ memo: `elisym:v1:${options.jobEventId}` }) : null;
907
913
  const asset = resolveAssetFromPaymentRequest(paymentRequest);
908
914
  if (!asset.mint) {
909
915
  const providerTransferIx2 = system.getTransferSolInstruction({
@@ -911,14 +917,19 @@ async function buildPaymentInstructions(paymentRequest, payerSigner) {
911
917
  destination: recipient,
912
918
  amount: BigInt(providerAmount)
913
919
  });
914
- const providerTransferIxWithReference2 = {
920
+ const providerTransferIxWithMarkers2 = {
915
921
  ...providerTransferIx2,
916
922
  accounts: [
917
923
  ...providerTransferIx2.accounts,
918
- { address: reference, role: kit.AccountRole.READONLY }
924
+ { address: reference, role: kit.AccountRole.READONLY },
925
+ { address: protocolTag, role: kit.AccountRole.READONLY }
919
926
  ]
920
927
  };
921
- const instructions2 = [providerTransferIxWithReference2];
928
+ const instructions2 = [];
929
+ if (memoInstruction) {
930
+ instructions2.push(memoInstruction);
931
+ }
932
+ instructions2.push(providerTransferIxWithMarkers2);
922
933
  if (paymentRequest.fee_address && feeAmount > 0) {
923
934
  instructions2.push(
924
935
  system.getTransferSolInstruction({
@@ -943,6 +954,9 @@ async function buildPaymentInstructions(paymentRequest, payerSigner) {
943
954
  mint
944
955
  });
945
956
  const instructions = [];
957
+ if (memoInstruction) {
958
+ instructions.push(memoInstruction);
959
+ }
946
960
  instructions.push(
947
961
  token.getCreateAssociatedTokenIdempotentInstruction(
948
962
  {
@@ -982,11 +996,15 @@ async function buildPaymentInstructions(paymentRequest, payerSigner) {
982
996
  amount: BigInt(providerAmount),
983
997
  decimals: asset.decimals
984
998
  });
985
- const providerTransferIxWithReference = {
999
+ const providerTransferIxWithMarkers = {
986
1000
  ...providerTransferIx,
987
- accounts: [...providerTransferIx.accounts, { address: reference, role: kit.AccountRole.READONLY }]
1001
+ accounts: [
1002
+ ...providerTransferIx.accounts,
1003
+ { address: reference, role: kit.AccountRole.READONLY },
1004
+ { address: protocolTag, role: kit.AccountRole.READONLY }
1005
+ ]
988
1006
  };
989
- instructions.push(providerTransferIxWithReference);
1007
+ instructions.push(providerTransferIxWithMarkers);
990
1008
  if (treasuryAta && paymentRequest.fee_address && feeAmount > 0) {
991
1009
  instructions.push(
992
1010
  token.getTransferCheckedInstruction({
@@ -1014,6 +1032,7 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1014
1032
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1015
1033
  var RANKING_BUCKET_SIZE_SECS = 60;
1016
1034
  var COLD_START_BUCKET = -Infinity;
1035
+ var NEVER_ABORTED_SIGNAL = new AbortController().signal;
1017
1036
  function toDTag(name) {
1018
1037
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
1019
1038
  if (!tag) {
@@ -1043,13 +1062,63 @@ function compareAgentsByRank(a, b) {
1043
1062
  }
1044
1063
  return kb.lastSeen - ka.lastSeen;
1045
1064
  }
1065
+ function parseCapabilityEvent(event, network) {
1066
+ if (!nostrTools.verifyEvent(event)) {
1067
+ return null;
1068
+ }
1069
+ if (!event.content) {
1070
+ return null;
1071
+ }
1072
+ let raw;
1073
+ try {
1074
+ raw = JSON.parse(event.content);
1075
+ } catch {
1076
+ return null;
1077
+ }
1078
+ if (!raw || typeof raw !== "object") {
1079
+ return null;
1080
+ }
1081
+ const candidate = raw;
1082
+ if (typeof candidate.name !== "string" || !candidate.name) {
1083
+ return null;
1084
+ }
1085
+ if (typeof candidate.description !== "string") {
1086
+ return null;
1087
+ }
1088
+ if (!Array.isArray(candidate.capabilities) || !candidate.capabilities.every((cap) => typeof cap === "string")) {
1089
+ return null;
1090
+ }
1091
+ if (candidate.deleted) {
1092
+ return null;
1093
+ }
1094
+ const card = candidate;
1095
+ if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1096
+ return null;
1097
+ }
1098
+ if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1099
+ return null;
1100
+ }
1101
+ const agentNetwork = card.payment?.network ?? "devnet";
1102
+ if (agentNetwork !== network) {
1103
+ return null;
1104
+ }
1105
+ const kTags = event.tags.filter((tag) => tag[0] === "k").map((tag) => parseInt(tag[1] ?? "", 10)).filter((kind) => !isNaN(kind));
1106
+ return {
1107
+ pubkey: event.pubkey,
1108
+ npub: nostrTools.nip19.npubEncode(event.pubkey),
1109
+ cards: [card],
1110
+ eventId: event.id,
1111
+ supportedKinds: kTags,
1112
+ lastSeen: event.created_at
1113
+ };
1114
+ }
1046
1115
  function buildAgentsFromEvents(events, network) {
1047
1116
  const latestByDTag = /* @__PURE__ */ new Map();
1048
1117
  for (const event of events) {
1049
1118
  if (!nostrTools.verifyEvent(event)) {
1050
1119
  continue;
1051
1120
  }
1052
- const dTag = event.tags.find((t) => t[0] === "d")?.[1] ?? "";
1121
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1053
1122
  const key = `${event.pubkey}:${dTag}`;
1054
1123
  const prev = latestByDTag.get(key);
1055
1124
  if (!prev || event.created_at > prev.created_at) {
@@ -1058,82 +1127,51 @@ function buildAgentsFromEvents(events, network) {
1058
1127
  }
1059
1128
  const accumMap = /* @__PURE__ */ new Map();
1060
1129
  for (const event of latestByDTag.values()) {
1061
- try {
1062
- if (!event.content) {
1063
- continue;
1064
- }
1065
- const raw = JSON.parse(event.content);
1066
- if (!raw || typeof raw !== "object") {
1067
- continue;
1068
- }
1069
- if (typeof raw.name !== "string" || !raw.name) {
1070
- continue;
1071
- }
1072
- if (typeof raw.description !== "string") {
1073
- continue;
1074
- }
1075
- if (!Array.isArray(raw.capabilities) || !raw.capabilities.every((c) => typeof c === "string")) {
1076
- continue;
1077
- }
1078
- if (raw.deleted) {
1079
- continue;
1080
- }
1081
- const card = raw;
1082
- if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1083
- continue;
1084
- }
1085
- if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1086
- continue;
1087
- }
1088
- const agentNetwork = card.payment?.network ?? "devnet";
1089
- if (agentNetwork !== network) {
1090
- continue;
1091
- }
1092
- const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
1093
- const entry = { card, kTags, createdAt: event.created_at };
1094
- const existing = accumMap.get(event.pubkey);
1095
- if (existing) {
1096
- const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
1097
- if (dupIndex >= 0) {
1098
- if (entry.createdAt >= existing.entries[dupIndex].createdAt) {
1099
- existing.entries[dupIndex] = entry;
1130
+ const parsed = parseCapabilityEvent(event, network);
1131
+ if (!parsed) {
1132
+ continue;
1133
+ }
1134
+ const card = parsed.cards[0];
1135
+ const cardKinds = parsed.supportedKinds;
1136
+ const createdAt = parsed.lastSeen;
1137
+ const existing = accumMap.get(parsed.pubkey);
1138
+ if (existing) {
1139
+ const prevForName = existing.perCard.get(card.name);
1140
+ if (prevForName) {
1141
+ if (createdAt >= prevForName.createdAt) {
1142
+ const idx = existing.agent.cards.findIndex(
1143
+ (existingCard) => existingCard.name === card.name
1144
+ );
1145
+ if (idx >= 0) {
1146
+ existing.agent.cards[idx] = card;
1100
1147
  }
1101
- } else {
1102
- existing.entries.push(entry);
1103
- }
1104
- if (event.created_at > existing.lastSeen) {
1105
- existing.lastSeen = event.created_at;
1106
- existing.eventId = event.id;
1148
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1107
1149
  }
1108
1150
  } else {
1109
- accumMap.set(event.pubkey, {
1110
- pubkey: event.pubkey,
1111
- npub: nostrTools.nip19.npubEncode(event.pubkey),
1112
- entries: [entry],
1113
- eventId: event.id,
1114
- lastSeen: event.created_at
1115
- });
1151
+ existing.agent.cards.push(card);
1152
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1116
1153
  }
1117
- } catch {
1154
+ if (createdAt > existing.agent.lastSeen) {
1155
+ existing.agent.lastSeen = createdAt;
1156
+ existing.agent.eventId = parsed.eventId;
1157
+ }
1158
+ } else {
1159
+ accumMap.set(parsed.pubkey, {
1160
+ agent: parsed,
1161
+ perCard: /* @__PURE__ */ new Map([[card.name, { createdAt, kTags: cardKinds }]])
1162
+ });
1118
1163
  }
1119
1164
  }
1120
1165
  const agentMap = /* @__PURE__ */ new Map();
1121
1166
  for (const [pubkey, acc] of accumMap) {
1122
1167
  const kindsSet = /* @__PURE__ */ new Set();
1123
- for (const e of acc.entries) {
1124
- for (const k of e.kTags) {
1125
- kindsSet.add(k);
1126
- }
1127
- }
1128
- const supportedKinds = [...kindsSet];
1129
- agentMap.set(pubkey, {
1130
- pubkey: acc.pubkey,
1131
- npub: acc.npub,
1132
- cards: acc.entries.map((e) => e.card),
1133
- eventId: acc.eventId,
1134
- supportedKinds,
1135
- lastSeen: acc.lastSeen
1136
- });
1168
+ for (const { kTags } of acc.perCard.values()) {
1169
+ for (const kind of kTags) {
1170
+ kindsSet.add(kind);
1171
+ }
1172
+ }
1173
+ acc.agent.supportedKinds = [...kindsSet];
1174
+ agentMap.set(pubkey, acc.agent);
1137
1175
  }
1138
1176
  return agentMap;
1139
1177
  }
@@ -1141,21 +1179,6 @@ var DiscoveryService = class {
1141
1179
  constructor(pool) {
1142
1180
  this.pool = pool;
1143
1181
  }
1144
- /** Count elisym agents (kind:31990 with "elisym" tag). */
1145
- async fetchAllAgentCount() {
1146
- const events = await this.pool.querySync({
1147
- kinds: [KIND_APP_HANDLER],
1148
- "#t": ["elisym"]
1149
- });
1150
- const uniquePubkeys = /* @__PURE__ */ new Set();
1151
- for (const event of events) {
1152
- if (!nostrTools.verifyEvent(event)) {
1153
- continue;
1154
- }
1155
- uniquePubkeys.add(event.pubkey);
1156
- }
1157
- return uniquePubkeys.size;
1158
- }
1159
1182
  /**
1160
1183
  * Fetch a single page of elisym agents with relay-side pagination.
1161
1184
  * Uses `until` cursor for Nostr cursor-based pagination.
@@ -1263,6 +1286,19 @@ var DiscoveryService = class {
1263
1286
  const events = await this.pool.querySync(filter);
1264
1287
  const agentMap = buildAgentsFromEvents(events, network);
1265
1288
  const agents = Array.from(agentMap.values());
1289
+ return this.runEnrichment(agents, agentMap, NEVER_ABORTED_SIGNAL);
1290
+ }
1291
+ /**
1292
+ * Enrich an agent map with paid-job stats, feedback counters, and kind:0
1293
+ * metadata, then return them sorted by `compareAgentsByRank`. Mutates the
1294
+ * passed-in `Agent` objects in place.
1295
+ *
1296
+ * Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
1297
+ * second pass). The `signal` short-circuits the post-query work; in-flight
1298
+ * pool queries are not cancellable today (they fall through to the standard
1299
+ * timeout) and the caller drops the resolved value.
1300
+ */
1301
+ async runEnrichment(agents, agentMap, signal) {
1266
1302
  const agentPubkeys = Array.from(agentMap.keys());
1267
1303
  if (agentPubkeys.length === 0) {
1268
1304
  return agents;
@@ -1270,9 +1306,9 @@ var DiscoveryService = class {
1270
1306
  const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1271
1307
  const resultKinds = /* @__PURE__ */ new Set();
1272
1308
  for (const agent of agentMap.values()) {
1273
- for (const k of agent.supportedKinds) {
1274
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1275
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1309
+ for (const supportedKind of agent.supportedKinds) {
1310
+ if (supportedKind >= KIND_JOB_REQUEST_BASE && supportedKind < KIND_JOB_RESULT_BASE) {
1311
+ resultKinds.add(KIND_JOB_RESULT_BASE + (supportedKind - KIND_JOB_REQUEST_BASE));
1276
1312
  }
1277
1313
  }
1278
1314
  }
@@ -1292,6 +1328,9 @@ var DiscoveryService = class {
1292
1328
  ),
1293
1329
  this.enrichWithMetadata(agents)
1294
1330
  ]);
1331
+ if (signal.aborted) {
1332
+ return agents;
1333
+ }
1295
1334
  const deliveredJobsByProvider = /* @__PURE__ */ new Map();
1296
1335
  for (const ev of resultEvents) {
1297
1336
  if (!nostrTools.verifyEvent(ev)) {
@@ -1304,7 +1343,7 @@ var DiscoveryService = class {
1304
1343
  if (ev.created_at > agent.lastSeen) {
1305
1344
  agent.lastSeen = ev.created_at;
1306
1345
  }
1307
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1346
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1308
1347
  if (jobEventId) {
1309
1348
  let delivered = deliveredJobsByProvider.get(ev.pubkey);
1310
1349
  if (!delivered) {
@@ -1318,7 +1357,7 @@ var DiscoveryService = class {
1318
1357
  if (!nostrTools.verifyEvent(ev)) {
1319
1358
  continue;
1320
1359
  }
1321
- const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1360
+ const targetPubkey = ev.tags.find((tag) => tag[0] === "p")?.[1];
1322
1361
  if (!targetPubkey) {
1323
1362
  continue;
1324
1363
  }
@@ -1329,17 +1368,17 @@ var DiscoveryService = class {
1329
1368
  if (ev.created_at > agent.lastSeen) {
1330
1369
  agent.lastSeen = ev.created_at;
1331
1370
  }
1332
- const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1371
+ const rating = ev.tags.find((tag) => tag[0] === "rating")?.[1];
1333
1372
  if (rating === "1" || rating === "0") {
1334
1373
  agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1335
1374
  if (rating === "1") {
1336
1375
  agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1337
1376
  }
1338
1377
  }
1339
- const status = ev.tags.find((t) => t[0] === "status")?.[1];
1340
- const txTag = ev.tags.find((t) => t[0] === "tx");
1378
+ const status = ev.tags.find((tag) => tag[0] === "status")?.[1];
1379
+ const txTag = ev.tags.find((tag) => tag[0] === "tx");
1341
1380
  const txSignature = txTag?.[1];
1342
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1381
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1343
1382
  const hasDeliveredResult = jobEventId !== void 0 && deliveredJobsByProvider.get(targetPubkey)?.has(jobEventId) === true;
1344
1383
  if (status === "payment-completed" && typeof txSignature === "string" && txSignature && hasDeliveredResult) {
1345
1384
  if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
@@ -1351,6 +1390,135 @@ var DiscoveryService = class {
1351
1390
  agents.sort(compareAgentsByRank);
1352
1391
  return agents;
1353
1392
  }
1393
+ /**
1394
+ * Stream elisym agents progressively as relays deliver events.
1395
+ *
1396
+ * Two live subscriptions:
1397
+ * - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
1398
+ * updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
1399
+ * all surviving cards for that author.
1400
+ * - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
1401
+ * emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
1402
+ * results (offset != 100) are not on this stream; they enter the final
1403
+ * ranking via the post-EOSE enrichment pass.
1404
+ *
1405
+ * After capabilities EOSE, an enrichment pass runs in parallel to the live
1406
+ * subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
1407
+ * is a clone, so further live updates do not mutate it.
1408
+ *
1409
+ * `closer.close()` tears down both subscriptions and aborts an in-flight
1410
+ * enrichment. If `opts.signal` is provided, aborting it does the same.
1411
+ */
1412
+ streamAgents(network, opts) {
1413
+ const eventsByPubkey = /* @__PURE__ */ new Map();
1414
+ const agentByPubkey = /* @__PURE__ */ new Map();
1415
+ const eoseSeen = { caps: false, results: false };
1416
+ let enrichmentStarted = false;
1417
+ const enrichmentAbort = new AbortController();
1418
+ const onExternalAbort = () => enrichmentAbort.abort();
1419
+ if (opts.signal) {
1420
+ if (opts.signal.aborted) {
1421
+ enrichmentAbort.abort();
1422
+ } else {
1423
+ opts.signal.addEventListener("abort", onExternalAbort, { once: true });
1424
+ }
1425
+ }
1426
+ const checkEose = () => {
1427
+ if (eoseSeen.caps && eoseSeen.results) {
1428
+ opts.onEose?.();
1429
+ }
1430
+ };
1431
+ const startEnrichment = () => {
1432
+ if (enrichmentStarted) {
1433
+ return;
1434
+ }
1435
+ enrichmentStarted = true;
1436
+ const snapshotAgents = Array.from(agentByPubkey.values()).map((agent) => ({ ...agent }));
1437
+ const snapshotMap = new Map(snapshotAgents.map((agent) => [agent.pubkey, agent]));
1438
+ void this.runEnrichment(snapshotAgents, snapshotMap, enrichmentAbort.signal).then(
1439
+ (sorted) => {
1440
+ if (enrichmentAbort.signal.aborted) {
1441
+ return;
1442
+ }
1443
+ opts.onComplete?.(sorted);
1444
+ },
1445
+ () => {
1446
+ }
1447
+ );
1448
+ };
1449
+ const capSub = this.pool.subscribe(
1450
+ { kinds: [KIND_APP_HANDLER], "#t": ["elisym"] },
1451
+ (event) => {
1452
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1453
+ let perDTag = eventsByPubkey.get(event.pubkey);
1454
+ const prev = perDTag?.get(dTag);
1455
+ if (prev && event.created_at <= prev.created_at) {
1456
+ return;
1457
+ }
1458
+ if (!nostrTools.verifyEvent(event)) {
1459
+ return;
1460
+ }
1461
+ if (!event.content) {
1462
+ return;
1463
+ }
1464
+ let payload;
1465
+ try {
1466
+ payload = JSON.parse(event.content);
1467
+ } catch {
1468
+ return;
1469
+ }
1470
+ const isTombstone = payload !== null && typeof payload === "object" && Boolean(payload.deleted);
1471
+ if (!isTombstone && !parseCapabilityEvent(event, network)) {
1472
+ return;
1473
+ }
1474
+ if (!perDTag) {
1475
+ perDTag = /* @__PURE__ */ new Map();
1476
+ eventsByPubkey.set(event.pubkey, perDTag);
1477
+ }
1478
+ perDTag.set(dTag, event);
1479
+ const merged = buildAgentsFromEvents(Array.from(perDTag.values()), network).get(
1480
+ event.pubkey
1481
+ );
1482
+ if (!merged) {
1483
+ agentByPubkey.delete(event.pubkey);
1484
+ return;
1485
+ }
1486
+ agentByPubkey.set(event.pubkey, merged);
1487
+ opts.onAgent(merged);
1488
+ },
1489
+ {
1490
+ oneose: () => {
1491
+ eoseSeen.caps = true;
1492
+ startEnrichment();
1493
+ checkEose();
1494
+ }
1495
+ }
1496
+ );
1497
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1498
+ const resultsSub = this.pool.subscribe(
1499
+ { kinds: [KIND_JOB_RESULT], "#t": ["elisym"], since: activitySince },
1500
+ (event) => {
1501
+ if (!nostrTools.verifyEvent(event)) {
1502
+ return;
1503
+ }
1504
+ opts.onPaidJob?.(event.pubkey, event.created_at);
1505
+ },
1506
+ {
1507
+ oneose: () => {
1508
+ eoseSeen.results = true;
1509
+ checkEose();
1510
+ }
1511
+ }
1512
+ );
1513
+ return {
1514
+ close: (reason) => {
1515
+ capSub.close(reason);
1516
+ resultsSub.close(reason);
1517
+ enrichmentAbort.abort();
1518
+ opts.signal?.removeEventListener("abort", onExternalAbort);
1519
+ }
1520
+ };
1521
+ }
1354
1522
  /**
1355
1523
  * Publish a capability card (kind:31990) as a provider.
1356
1524
  * Solana address is validated for Base58 format only - full decode
@@ -2566,8 +2734,11 @@ var NostrPool = class {
2566
2734
  throw new Error(`Failed to publish to all ${this.relays.length} relays`);
2567
2735
  }
2568
2736
  }
2569
- subscribe(filter, onEvent) {
2570
- const rawSub = this.pool.subscribeMany(this.relays, filter, { onevent: onEvent });
2737
+ subscribe(filter, onEvent, opts) {
2738
+ const rawSub = this.pool.subscribeMany(this.relays, filter, {
2739
+ onevent: onEvent,
2740
+ oneose: opts?.oneose
2741
+ });
2571
2742
  const tracked = {
2572
2743
  close: (reason) => {
2573
2744
  this.activeSubscriptions.delete(tracked);
@@ -2918,6 +3089,83 @@ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2918
3089
  }
2919
3090
  return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2920
3091
  }
3092
+ var DEFAULT_LIMIT = 1e3;
3093
+ var NATIVE_KEY = "native";
3094
+ async function aggregateNetworkStats(rpc, options) {
3095
+ const limit = options?.limit ?? DEFAULT_LIMIT;
3096
+ const concurrency = options?.concurrency ?? DEFAULTS.QUERY_MAX_CONCURRENCY;
3097
+ const tag = kit.address(ELISYM_PROTOCOL_TAG);
3098
+ const signatures = await rpc.getSignaturesForAddress(tag, { limit, before: options?.before }).send();
3099
+ const validSigs = signatures.filter((entry) => entry.err === null);
3100
+ if (validSigs.length === 0) {
3101
+ return { jobCount: 0, volumeByAsset: {} };
3102
+ }
3103
+ const volumeByAsset = {};
3104
+ let jobCount = 0;
3105
+ for (let start = 0; start < validSigs.length; start += concurrency) {
3106
+ const batch = validSigs.slice(start, start + concurrency);
3107
+ const txResults = await Promise.all(
3108
+ batch.map(
3109
+ (entry) => rpc.getTransaction(entry.signature, {
3110
+ commitment: "confirmed",
3111
+ encoding: "json",
3112
+ maxSupportedTransactionVersion: 0
3113
+ }).send().catch(() => null)
3114
+ )
3115
+ );
3116
+ for (const tx of txResults) {
3117
+ if (!tx?.meta || tx.meta.err) {
3118
+ continue;
3119
+ }
3120
+ jobCount += 1;
3121
+ accumulateTransfers(tx, volumeByAsset);
3122
+ }
3123
+ }
3124
+ const latest = validSigs[0]?.signature;
3125
+ const oldest = validSigs.at(-1)?.signature;
3126
+ return {
3127
+ jobCount,
3128
+ volumeByAsset,
3129
+ latestSignature: latest,
3130
+ oldestSignature: oldest
3131
+ };
3132
+ }
3133
+ function accumulateTransfers(tx, volumeByAsset) {
3134
+ const raw = tx;
3135
+ const meta = raw.meta;
3136
+ if (!meta) {
3137
+ return;
3138
+ }
3139
+ const preTokens = meta.preTokenBalances ?? [];
3140
+ const postTokens = meta.postTokenBalances ?? [];
3141
+ const isSpl = postTokens.length > 0 || preTokens.length > 0;
3142
+ if (isSpl) {
3143
+ accumulateSplDeltas(preTokens, postTokens, volumeByAsset);
3144
+ return;
3145
+ }
3146
+ accumulateNativeDeltas(meta.preBalances, meta.postBalances, volumeByAsset);
3147
+ }
3148
+ function accumulateSplDeltas(pre, post, volumeByAsset) {
3149
+ for (const postEntry of post) {
3150
+ const preEntry = pre.find((entry) => entry.accountIndex === postEntry.accountIndex);
3151
+ const preAmount = preEntry ? BigInt(preEntry.uiTokenAmount.amount) : 0n;
3152
+ const postAmount = BigInt(postEntry.uiTokenAmount.amount);
3153
+ const delta = postAmount - preAmount;
3154
+ if (delta > 0n) {
3155
+ volumeByAsset[postEntry.mint] = (volumeByAsset[postEntry.mint] ?? 0n) + delta;
3156
+ }
3157
+ }
3158
+ }
3159
+ function accumulateNativeDeltas(pre, post, volumeByAsset) {
3160
+ for (let i = 1; i < post.length; i++) {
3161
+ const preValue = pre[i] ?? 0n;
3162
+ const postValue = post[i] ?? 0n;
3163
+ const delta = BigInt(postValue) - BigInt(preValue);
3164
+ if (delta > 0n) {
3165
+ volumeByAsset[NATIVE_KEY] = (volumeByAsset[NATIVE_KEY] ?? 0n) + delta;
3166
+ }
3167
+ }
3168
+ }
2921
3169
  var SessionSpendLimitEntrySchema = zod.z.object({
2922
3170
  chain: zod.z.enum(["solana"]),
2923
3171
  token: zod.z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -3123,6 +3371,7 @@ exports.DEFAULTS = DEFAULTS;
3123
3371
  exports.DEFAULT_KIND_OFFSET = DEFAULT_KIND_OFFSET;
3124
3372
  exports.DEFAULT_REDACT_PATHS = DEFAULT_REDACT_PATHS;
3125
3373
  exports.DiscoveryService = DiscoveryService;
3374
+ exports.ELISYM_PROTOCOL_TAG = ELISYM_PROTOCOL_TAG;
3126
3375
  exports.ElisymClient = ElisymClient;
3127
3376
  exports.ElisymIdentity = ElisymIdentity;
3128
3377
  exports.GlobalConfigSchema = GlobalConfigSchema;
@@ -3152,6 +3401,7 @@ exports.SECRET_REDACT_PATHS = SECRET_REDACT_PATHS;
3152
3401
  exports.SessionSpendLimitEntrySchema = SessionSpendLimitEntrySchema;
3153
3402
  exports.SolanaPaymentStrategy = SolanaPaymentStrategy;
3154
3403
  exports.USDC_SOLANA_DEVNET = USDC_SOLANA_DEVNET;
3404
+ exports.aggregateNetworkStats = aggregateNetworkStats;
3155
3405
  exports.assertExpiry = assertExpiry;
3156
3406
  exports.assertLamports = assertLamports;
3157
3407
  exports.assetByKey = assetByKey;