@elisym/sdk 0.9.0 → 0.10.1

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,9 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1019
1019
  options
1020
1020
  );
1021
1021
  }
1022
+ var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1023
+ var RANKING_BUCKET_SIZE_SECS = 60;
1024
+ var COLD_START_BUCKET = -Infinity;
1022
1025
  function toDTag(name) {
1023
1026
  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
1027
  if (!tag) {
@@ -1026,6 +1029,28 @@ function toDTag(name) {
1026
1029
  }
1027
1030
  return tag;
1028
1031
  }
1032
+ function computeRankKey(agent) {
1033
+ const lastPaidJobAt = agent.lastPaidJobAt ?? 0;
1034
+ const total = agent.totalRatingCount ?? 0;
1035
+ const positive = agent.positiveCount ?? 0;
1036
+ const rate = total > 0 ? positive / total : 0;
1037
+ const bucket = lastPaidJobAt > 0 ? Math.floor(lastPaidJobAt / RANKING_BUCKET_SIZE_SECS) * RANKING_BUCKET_SIZE_SECS : COLD_START_BUCKET;
1038
+ return { bucket, rate, lastPaidJobAt, lastSeen: agent.lastSeen };
1039
+ }
1040
+ function compareAgentsByRank(a, b) {
1041
+ const ka = computeRankKey(a);
1042
+ const kb = computeRankKey(b);
1043
+ if (kb.bucket !== ka.bucket) {
1044
+ return kb.bucket - ka.bucket;
1045
+ }
1046
+ if (kb.rate !== ka.rate) {
1047
+ return kb.rate - ka.rate;
1048
+ }
1049
+ if (kb.lastPaidJobAt !== ka.lastPaidJobAt) {
1050
+ return kb.lastPaidJobAt - ka.lastPaidJobAt;
1051
+ }
1052
+ return kb.lastSeen - ka.lastSeen;
1053
+ }
1029
1054
  function buildAgentsFromEvents(events, network) {
1030
1055
  const latestByDTag = /* @__PURE__ */ new Map();
1031
1056
  for (const event of events) {
@@ -1210,7 +1235,26 @@ var DiscoveryService = class {
1210
1235
  }
1211
1236
  return agents;
1212
1237
  }
1213
- /** Fetch elisym agents filtered by network. */
1238
+ /**
1239
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1240
+ * positive-feedback rate.
1241
+ *
1242
+ * Ranking algorithm:
1243
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1244
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1245
+ * sentinel bucket below all populated buckets.
1246
+ * 2. Within a bucket, sort by positive review rate descending.
1247
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1248
+ *
1249
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
1250
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
1251
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
1252
+ * candidates), and the resulting 429s blocked discovery entirely. This
1253
+ * means a malicious customer can publish a fake `payment-completed` to lift
1254
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
1255
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
1256
+ * provider.
1257
+ */
1214
1258
  async fetchAgents(network = "devnet", limit) {
1215
1259
  const filter = {
1216
1260
  kinds: [KIND_APP_HANDLER],
@@ -1221,40 +1265,78 @@ var DiscoveryService = class {
1221
1265
  }
1222
1266
  const events = await this.pool.querySync(filter);
1223
1267
  const agentMap = buildAgentsFromEvents(events, network);
1224
- const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
1268
+ const agents = Array.from(agentMap.values());
1225
1269
  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
- }
1270
+ if (agentPubkeys.length === 0) {
1271
+ return agents;
1272
+ }
1273
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1274
+ const resultKinds = /* @__PURE__ */ new Set();
1275
+ for (const agent of agentMap.values()) {
1276
+ for (const k of agent.supportedKinds) {
1277
+ if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1278
+ resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1234
1279
  }
1235
1280
  }
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;
1281
+ }
1282
+ resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1283
+ const [resultEvents, feedbackEvents] = await Promise.all([
1284
+ this.pool.queryBatched(
1285
+ {
1286
+ kinds: [...resultKinds],
1287
+ since: activitySince
1288
+ },
1289
+ agentPubkeys
1290
+ ),
1291
+ this.pool.queryBatchedByTag(
1292
+ { kinds: [KIND_JOB_FEEDBACK], since: activitySince },
1293
+ "p",
1294
+ agentPubkeys
1295
+ ),
1296
+ this.enrichWithMetadata(agents)
1297
+ ]);
1298
+ for (const ev of resultEvents) {
1299
+ if (!nostrTools.verifyEvent(ev)) {
1300
+ continue;
1301
+ }
1302
+ const agent = agentMap.get(ev.pubkey);
1303
+ if (agent && ev.created_at > agent.lastSeen) {
1304
+ agent.lastSeen = ev.created_at;
1305
+ }
1306
+ }
1307
+ for (const ev of feedbackEvents) {
1308
+ if (!nostrTools.verifyEvent(ev)) {
1309
+ continue;
1310
+ }
1311
+ const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1312
+ if (!targetPubkey) {
1313
+ continue;
1314
+ }
1315
+ const agent = agentMap.get(targetPubkey);
1316
+ if (!agent) {
1317
+ continue;
1318
+ }
1319
+ if (ev.created_at > agent.lastSeen) {
1320
+ agent.lastSeen = ev.created_at;
1321
+ }
1322
+ const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1323
+ if (rating === "1" || rating === "0") {
1324
+ agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1325
+ if (rating === "1") {
1326
+ agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1250
1327
  }
1251
- const agent = agentMap.get(ev.pubkey);
1252
- if (agent && ev.created_at > agent.lastSeen) {
1253
- agent.lastSeen = ev.created_at;
1328
+ }
1329
+ const status = ev.tags.find((t) => t[0] === "status")?.[1];
1330
+ const txTag = ev.tags.find((t) => t[0] === "tx");
1331
+ const txSignature = txTag?.[1];
1332
+ if (status === "payment-completed" && typeof txSignature === "string" && txSignature) {
1333
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1334
+ agent.lastPaidJobAt = ev.created_at;
1335
+ agent.lastPaidJobTx = txSignature;
1254
1336
  }
1255
1337
  }
1256
- agents.sort((a, b) => b.lastSeen - a.lastSeen);
1257
1338
  }
1339
+ agents.sort(compareAgentsByRank);
1258
1340
  return agents;
1259
1341
  }
1260
1342
  /**
@@ -1953,6 +2035,7 @@ var MarketplaceService = class {
1953
2035
  let status = "processing";
1954
2036
  let amount;
1955
2037
  let txHash;
2038
+ let asset;
1956
2039
  if (result) {
1957
2040
  status = "success";
1958
2041
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -1961,9 +2044,18 @@ var MarketplaceService = class {
1961
2044
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
1962
2045
  for (const fb of allFeedbacksForReq) {
1963
2046
  const txTag = fb.tags.find((t) => t[0] === "tx");
1964
- if (txTag?.[1]) {
2047
+ if (txTag?.[1] && !txHash) {
1965
2048
  txHash = txTag[1];
1966
- break;
2049
+ }
2050
+ if (!asset) {
2051
+ const amtTag = fb.tags.find((t) => t[0] === "amount");
2052
+ const requestJson = amtTag?.[2];
2053
+ if (requestJson) {
2054
+ const parsed = parsePaymentRequest(requestJson);
2055
+ if (parsed.ok && parsed.data.asset) {
2056
+ asset = parsed.data.asset;
2057
+ }
2058
+ }
1967
2059
  }
1968
2060
  }
1969
2061
  if (feedback) {
@@ -1992,6 +2084,7 @@ var MarketplaceService = class {
1992
2084
  resultEventId: result?.id,
1993
2085
  amount,
1994
2086
  txHash,
2087
+ asset,
1995
2088
  createdAt: req.created_at
1996
2089
  });
1997
2090
  }
@@ -2730,6 +2823,89 @@ function lamportsToSol(lamports) {
2730
2823
  const frac = lamports % LAMPORTS_PER_SOL2;
2731
2824
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2732
2825
  }
2826
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
2827
+ var verifyCache = /* @__PURE__ */ new Map();
2828
+ function clearQuickVerifyCache() {
2829
+ verifyCache.clear();
2830
+ }
2831
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
2832
+ if (!txSignature) {
2833
+ return { verified: false, txSignature: "", reason: "invalid_input" };
2834
+ }
2835
+ if (!expectedRecipient || !kit.isAddress(expectedRecipient)) {
2836
+ return { verified: false, txSignature, reason: "invalid_input" };
2837
+ }
2838
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
2839
+ const cached = verifyCache.get(cacheKey2);
2840
+ if (cached) {
2841
+ if (cached.result.verified) {
2842
+ return cached.result;
2843
+ }
2844
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
2845
+ return cached.result;
2846
+ }
2847
+ }
2848
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
2849
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
2850
+ return result;
2851
+ }
2852
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2853
+ const sigStr = txSignature;
2854
+ if (!rpc || typeof rpc.getTransaction !== "function") {
2855
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2856
+ }
2857
+ let tx;
2858
+ try {
2859
+ tx = await rpc.getTransaction(txSignature, {
2860
+ commitment: "confirmed",
2861
+ encoding: "json",
2862
+ maxSupportedTransactionVersion: 0
2863
+ }).send();
2864
+ } catch {
2865
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2866
+ }
2867
+ if (!tx) {
2868
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
2869
+ }
2870
+ if (!tx.meta || tx.meta.err) {
2871
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
2872
+ }
2873
+ const accountKeys = tx.transaction.message.accountKeys;
2874
+ const recipientStr = expectedRecipient;
2875
+ const recipientIdx = accountKeys.indexOf(recipientStr);
2876
+ if (recipientIdx !== -1) {
2877
+ const preBalances = tx.meta.preBalances;
2878
+ const postBalances = tx.meta.postBalances;
2879
+ if (preBalances && postBalances) {
2880
+ const pre = preBalances[recipientIdx];
2881
+ const post = postBalances[recipientIdx];
2882
+ if (pre !== void 0 && post !== void 0) {
2883
+ const delta = BigInt(post) - BigInt(pre);
2884
+ if (delta > 0n) {
2885
+ return { verified: true, txSignature: sigStr };
2886
+ }
2887
+ }
2888
+ }
2889
+ }
2890
+ const postTokenBalances = tx.meta.postTokenBalances;
2891
+ const preTokenBalances = tx.meta.preTokenBalances;
2892
+ if (postTokenBalances) {
2893
+ for (const post of postTokenBalances) {
2894
+ if (post.owner !== recipientStr) {
2895
+ continue;
2896
+ }
2897
+ const pre = preTokenBalances?.find(
2898
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
2899
+ );
2900
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
2901
+ const postAmount = BigInt(post.uiTokenAmount.amount);
2902
+ if (postAmount > preAmount) {
2903
+ return { verified: true, txSignature: sigStr };
2904
+ }
2905
+ }
2906
+ }
2907
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2908
+ }
2733
2909
  var SessionSpendLimitEntrySchema = zod.z.object({
2734
2910
  chain: zod.z.enum(["solana"]),
2735
2911
  token: zod.z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -2972,6 +3148,9 @@ exports.buildPaymentInstructions = buildPaymentInstructions;
2972
3148
  exports.calculateProtocolFee = calculateProtocolFee;
2973
3149
  exports.clearPriorityFeeCache = clearPriorityFeeCache;
2974
3150
  exports.clearProtocolConfigCache = clearProtocolConfigCache;
3151
+ exports.clearQuickVerifyCache = clearQuickVerifyCache;
3152
+ exports.compareAgentsByRank = compareAgentsByRank;
3153
+ exports.computeRankKey = computeRankKey;
2975
3154
  exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
2976
3155
  exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
2977
3156
  exports.estimatePriorityFeeMicroLamports = estimatePriorityFeeMicroLamports;
@@ -2996,5 +3175,6 @@ exports.toDTag = toDTag;
2996
3175
  exports.truncateKey = truncateKey;
2997
3176
  exports.validateAgentName = validateAgentName;
2998
3177
  exports.validateExpiry = validateExpiry;
3178
+ exports.verifyJobPaymentQuick = verifyJobPaymentQuick;
2999
3179
  //# sourceMappingURL=index.cjs.map
3000
3180
  //# sourceMappingURL=index.cjs.map