@elisym/sdk 0.9.0 → 0.10.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
@@ -75,7 +75,7 @@ function getProtocolProgramId(cluster) {
75
75
  }
76
76
  var DEFAULTS = {
77
77
  SUBSCRIPTION_TIMEOUT_MS: 12e4,
78
- PING_TIMEOUT_MS: 15e3,
78
+ PING_TIMEOUT_MS: 3e3,
79
79
  PING_RETRIES: 2,
80
80
  PING_CACHE_TTL_MS: 3e4,
81
81
  PAYMENT_EXPIRY_SECS: 600,
@@ -1019,6 +1019,95 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1019
1019
  options
1020
1020
  );
1021
1021
  }
1022
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
1023
+ var verifyCache = /* @__PURE__ */ new Map();
1024
+ function clearQuickVerifyCache() {
1025
+ verifyCache.clear();
1026
+ }
1027
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
1028
+ if (!txSignature) {
1029
+ return { verified: false, txSignature: "", reason: "invalid_input" };
1030
+ }
1031
+ if (!expectedRecipient || !kit.isAddress(expectedRecipient)) {
1032
+ return { verified: false, txSignature, reason: "invalid_input" };
1033
+ }
1034
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
1035
+ const cached = verifyCache.get(cacheKey2);
1036
+ if (cached) {
1037
+ if (cached.result.verified) {
1038
+ return cached.result;
1039
+ }
1040
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
1041
+ return cached.result;
1042
+ }
1043
+ }
1044
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
1045
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
1046
+ return result;
1047
+ }
1048
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
1049
+ const sigStr = txSignature;
1050
+ if (!rpc || typeof rpc.getTransaction !== "function") {
1051
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1052
+ }
1053
+ let tx;
1054
+ try {
1055
+ tx = await rpc.getTransaction(txSignature, {
1056
+ commitment: "confirmed",
1057
+ encoding: "json",
1058
+ maxSupportedTransactionVersion: 0
1059
+ }).send();
1060
+ } catch {
1061
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1062
+ }
1063
+ if (!tx) {
1064
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
1065
+ }
1066
+ if (!tx.meta || tx.meta.err) {
1067
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
1068
+ }
1069
+ const accountKeys = tx.transaction.message.accountKeys;
1070
+ const recipientStr = expectedRecipient;
1071
+ const recipientIdx = accountKeys.indexOf(recipientStr);
1072
+ if (recipientIdx !== -1) {
1073
+ const preBalances = tx.meta.preBalances;
1074
+ const postBalances = tx.meta.postBalances;
1075
+ if (preBalances && postBalances) {
1076
+ const pre = preBalances[recipientIdx];
1077
+ const post = postBalances[recipientIdx];
1078
+ if (pre !== void 0 && post !== void 0) {
1079
+ const delta = BigInt(post) - BigInt(pre);
1080
+ if (delta > 0n) {
1081
+ return { verified: true, txSignature: sigStr };
1082
+ }
1083
+ }
1084
+ }
1085
+ }
1086
+ const postTokenBalances = tx.meta.postTokenBalances;
1087
+ const preTokenBalances = tx.meta.preTokenBalances;
1088
+ if (postTokenBalances) {
1089
+ for (const post of postTokenBalances) {
1090
+ if (post.owner !== recipientStr) {
1091
+ continue;
1092
+ }
1093
+ const pre = preTokenBalances?.find(
1094
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
1095
+ );
1096
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1097
+ const postAmount = BigInt(post.uiTokenAmount.amount);
1098
+ if (postAmount > preAmount) {
1099
+ return { verified: true, txSignature: sigStr };
1100
+ }
1101
+ }
1102
+ }
1103
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
1104
+ }
1105
+
1106
+ // src/services/discovery.ts
1107
+ var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1108
+ var RANKING_BUCKET_SIZE_SECS = 60;
1109
+ var COLD_START_BUCKET = -Infinity;
1110
+ var MAX_PAID_CANDIDATES_PER_AGENT = 5;
1022
1111
  function toDTag(name) {
1023
1112
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
1024
1113
  if (!tag) {
@@ -1026,6 +1115,28 @@ function toDTag(name) {
1026
1115
  }
1027
1116
  return tag;
1028
1117
  }
1118
+ function computeRankKey(agent) {
1119
+ const lastPaidJobAt = agent.lastPaidJobAt ?? 0;
1120
+ const total = agent.totalRatingCount ?? 0;
1121
+ const positive = agent.positiveCount ?? 0;
1122
+ const rate = total > 0 ? positive / total : 0;
1123
+ const bucket = lastPaidJobAt > 0 ? Math.floor(lastPaidJobAt / RANKING_BUCKET_SIZE_SECS) * RANKING_BUCKET_SIZE_SECS : COLD_START_BUCKET;
1124
+ return { bucket, rate, lastPaidJobAt, lastSeen: agent.lastSeen };
1125
+ }
1126
+ function compareAgentsByRank(a, b) {
1127
+ const ka = computeRankKey(a);
1128
+ const kb = computeRankKey(b);
1129
+ if (kb.bucket !== ka.bucket) {
1130
+ return kb.bucket - ka.bucket;
1131
+ }
1132
+ if (kb.rate !== ka.rate) {
1133
+ return kb.rate - ka.rate;
1134
+ }
1135
+ if (kb.lastPaidJobAt !== ka.lastPaidJobAt) {
1136
+ return kb.lastPaidJobAt - ka.lastPaidJobAt;
1137
+ }
1138
+ return kb.lastSeen - ka.lastSeen;
1139
+ }
1029
1140
  function buildAgentsFromEvents(events, network) {
1030
1141
  const latestByDTag = /* @__PURE__ */ new Map();
1031
1142
  for (const event of events) {
@@ -1120,9 +1231,37 @@ function buildAgentsFromEvents(events, network) {
1120
1231
  }
1121
1232
  return agentMap;
1122
1233
  }
1234
+ function pickSolanaAddress(agent) {
1235
+ for (const card of agent.cards) {
1236
+ const addr = card.payment?.address;
1237
+ if (card.payment?.chain === "solana" && typeof addr === "string" && addr.length > 0) {
1238
+ return addr;
1239
+ }
1240
+ }
1241
+ return null;
1242
+ }
1243
+ async function verifyNewestPaidCandidate(rpc, recipient, candidatesNewestFirst) {
1244
+ const settled = await Promise.allSettled(
1245
+ candidatesNewestFirst.map(
1246
+ (candidate) => verifyJobPaymentQuick(rpc, candidate.txSignature, recipient)
1247
+ )
1248
+ );
1249
+ for (let i = 0; i < settled.length; i++) {
1250
+ const entry = settled[i];
1251
+ if (entry?.status === "fulfilled" && entry.value.verified) {
1252
+ return candidatesNewestFirst[i] ?? null;
1253
+ }
1254
+ }
1255
+ return null;
1256
+ }
1123
1257
  var DiscoveryService = class {
1124
- constructor(pool) {
1258
+ constructor(pool, defaultRpc) {
1125
1259
  this.pool = pool;
1260
+ this.defaultRpc = defaultRpc;
1261
+ }
1262
+ /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
1263
+ setRpc(rpc) {
1264
+ this.defaultRpc = rpc;
1126
1265
  }
1127
1266
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1128
1267
  async fetchAllAgentCount() {
@@ -1210,8 +1349,21 @@ var DiscoveryService = class {
1210
1349
  }
1211
1350
  return agents;
1212
1351
  }
1213
- /** Fetch elisym agents filtered by network. */
1214
- async fetchAgents(network = "devnet", limit) {
1352
+ /**
1353
+ * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1354
+ *
1355
+ * Ranking algorithm:
1356
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
1357
+ * verified payment). Cold-start agents (no verified paid job) go into a
1358
+ * sentinel bucket below all populated buckets.
1359
+ * 2. Within a bucket, sort by positive review rate descending.
1360
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1361
+ *
1362
+ * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
1363
+ * If `rpc` is not configured, all agents fall through to cold-start and order
1364
+ * is determined by `lastSeen` only.
1365
+ */
1366
+ async fetchAgents(network = "devnet", limit, rpcOverride) {
1215
1367
  const filter = {
1216
1368
  kinds: [KIND_APP_HANDLER],
1217
1369
  "#t": ["elisym"]
@@ -1221,40 +1373,100 @@ var DiscoveryService = class {
1221
1373
  }
1222
1374
  const events = await this.pool.querySync(filter);
1223
1375
  const agentMap = buildAgentsFromEvents(events, network);
1224
- const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
1376
+ const agents = Array.from(agentMap.values());
1225
1377
  const agentPubkeys = Array.from(agentMap.keys());
1226
- if (agentPubkeys.length > 0) {
1227
- const activitySince = Math.floor(Date.now() / 1e3) - 24 * 60 * 60;
1228
- const resultKinds = /* @__PURE__ */ new Set();
1229
- for (const agent of agentMap.values()) {
1230
- for (const k of agent.supportedKinds) {
1231
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1232
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1233
- }
1378
+ if (agentPubkeys.length === 0) {
1379
+ return agents;
1380
+ }
1381
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1382
+ const resultKinds = /* @__PURE__ */ new Set();
1383
+ for (const agent of agentMap.values()) {
1384
+ for (const k of agent.supportedKinds) {
1385
+ if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1386
+ resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1234
1387
  }
1235
1388
  }
1236
- resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1237
- const [activityEvents] = await Promise.all([
1238
- this.pool.queryBatched(
1239
- {
1240
- kinds: [...resultKinds, KIND_JOB_FEEDBACK],
1241
- since: activitySince
1242
- },
1243
- agentPubkeys
1244
- ),
1245
- this.enrichWithMetadata(agents)
1246
- ]);
1247
- for (const ev of activityEvents) {
1248
- if (!nostrTools.verifyEvent(ev)) {
1249
- continue;
1389
+ }
1390
+ resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1391
+ const [resultEvents, feedbackEvents] = await Promise.all([
1392
+ this.pool.queryBatched(
1393
+ {
1394
+ kinds: [...resultKinds],
1395
+ since: activitySince
1396
+ },
1397
+ agentPubkeys
1398
+ ),
1399
+ this.pool.queryBatchedByTag(
1400
+ { kinds: [KIND_JOB_FEEDBACK], since: activitySince },
1401
+ "p",
1402
+ agentPubkeys
1403
+ ),
1404
+ this.enrichWithMetadata(agents)
1405
+ ]);
1406
+ for (const ev of resultEvents) {
1407
+ if (!nostrTools.verifyEvent(ev)) {
1408
+ continue;
1409
+ }
1410
+ const agent = agentMap.get(ev.pubkey);
1411
+ if (agent && ev.created_at > agent.lastSeen) {
1412
+ agent.lastSeen = ev.created_at;
1413
+ }
1414
+ }
1415
+ const paidCandidates = /* @__PURE__ */ new Map();
1416
+ for (const ev of feedbackEvents) {
1417
+ if (!nostrTools.verifyEvent(ev)) {
1418
+ continue;
1419
+ }
1420
+ const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1421
+ if (!targetPubkey) {
1422
+ continue;
1423
+ }
1424
+ const agent = agentMap.get(targetPubkey);
1425
+ if (!agent) {
1426
+ continue;
1427
+ }
1428
+ if (ev.created_at > agent.lastSeen) {
1429
+ agent.lastSeen = ev.created_at;
1430
+ }
1431
+ const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1432
+ if (rating === "1" || rating === "0") {
1433
+ agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1434
+ if (rating === "1") {
1435
+ agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1250
1436
  }
1251
- const agent = agentMap.get(ev.pubkey);
1252
- if (agent && ev.created_at > agent.lastSeen) {
1253
- agent.lastSeen = ev.created_at;
1437
+ }
1438
+ const status = ev.tags.find((t) => t[0] === "status")?.[1];
1439
+ const txTag = ev.tags.find((t) => t[0] === "tx");
1440
+ const txSignature = txTag?.[1];
1441
+ if (status === "payment-completed" && typeof txSignature === "string" && txSignature) {
1442
+ const list = paidCandidates.get(targetPubkey);
1443
+ const candidate = { txSignature, createdAt: ev.created_at };
1444
+ if (list) {
1445
+ list.push(candidate);
1446
+ } else {
1447
+ paidCandidates.set(targetPubkey, [candidate]);
1254
1448
  }
1255
1449
  }
1256
- agents.sort((a, b) => b.lastSeen - a.lastSeen);
1257
1450
  }
1451
+ const rpc = rpcOverride ?? this.defaultRpc;
1452
+ if (rpc && paidCandidates.size > 0) {
1453
+ const perAgent = Array.from(paidCandidates.entries()).map(([agentPubkey, list]) => {
1454
+ const agent = agentMap.get(agentPubkey);
1455
+ const recipient = agent ? pickSolanaAddress(agent) : null;
1456
+ if (!agent || !recipient) {
1457
+ return Promise.resolve();
1458
+ }
1459
+ const ordered = [...list].sort((a, b) => b.createdAt - a.createdAt).slice(0, MAX_PAID_CANDIDATES_PER_AGENT);
1460
+ return verifyNewestPaidCandidate(rpc, recipient, ordered).then((winner) => {
1461
+ if (winner) {
1462
+ agent.lastPaidJobAt = winner.createdAt;
1463
+ agent.lastPaidJobTx = winner.txSignature;
1464
+ }
1465
+ });
1466
+ });
1467
+ await Promise.all(perAgent);
1468
+ }
1469
+ agents.sort(compareAgentsByRank);
1258
1470
  return agents;
1259
1471
  }
1260
1472
  /**
@@ -2602,7 +2814,7 @@ var ElisymClient = class {
2602
2814
  payment;
2603
2815
  constructor(config = {}) {
2604
2816
  this.pool = new NostrPool(config.relays ?? RELAYS);
2605
- this.discovery = new DiscoveryService(this.pool);
2817
+ this.discovery = new DiscoveryService(this.pool, config.rpc);
2606
2818
  this.marketplace = new MarketplaceService(this.pool);
2607
2819
  this.ping = new PingService(this.pool);
2608
2820
  this.media = new MediaService(config.uploadUrl);
@@ -2972,6 +3184,9 @@ exports.buildPaymentInstructions = buildPaymentInstructions;
2972
3184
  exports.calculateProtocolFee = calculateProtocolFee;
2973
3185
  exports.clearPriorityFeeCache = clearPriorityFeeCache;
2974
3186
  exports.clearProtocolConfigCache = clearProtocolConfigCache;
3187
+ exports.clearQuickVerifyCache = clearQuickVerifyCache;
3188
+ exports.compareAgentsByRank = compareAgentsByRank;
3189
+ exports.computeRankKey = computeRankKey;
2975
3190
  exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
2976
3191
  exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
2977
3192
  exports.estimatePriorityFeeMicroLamports = estimatePriorityFeeMicroLamports;
@@ -2996,5 +3211,6 @@ exports.toDTag = toDTag;
2996
3211
  exports.truncateKey = truncateKey;
2997
3212
  exports.validateAgentName = validateAgentName;
2998
3213
  exports.validateExpiry = validateExpiry;
3214
+ exports.verifyJobPaymentQuick = verifyJobPaymentQuick;
2999
3215
  //# sourceMappingURL=index.cjs.map
3000
3216
  //# sourceMappingURL=index.cjs.map