@elisym/sdk 0.10.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
@@ -1019,95 +1019,9 @@ 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
1022
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1108
1023
  var RANKING_BUCKET_SIZE_SECS = 60;
1109
1024
  var COLD_START_BUCKET = -Infinity;
1110
- var MAX_PAID_CANDIDATES_PER_AGENT = 5;
1111
1025
  function toDTag(name) {
1112
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, "");
1113
1027
  if (!tag) {
@@ -1231,37 +1145,9 @@ function buildAgentsFromEvents(events, network) {
1231
1145
  }
1232
1146
  return agentMap;
1233
1147
  }
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
- }
1257
1148
  var DiscoveryService = class {
1258
- constructor(pool, defaultRpc) {
1149
+ constructor(pool) {
1259
1150
  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;
1265
1151
  }
1266
1152
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1267
1153
  async fetchAllAgentCount() {
@@ -1350,20 +1236,26 @@ var DiscoveryService = class {
1350
1236
  return agents;
1351
1237
  }
1352
1238
  /**
1353
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1239
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1240
+ * positive-feedback rate.
1354
1241
  *
1355
1242
  * 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
1243
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1244
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1358
1245
  * sentinel bucket below all populated buckets.
1359
1246
  * 2. Within a bucket, sort by positive review rate descending.
1360
1247
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1361
1248
  *
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.
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.
1365
1257
  */
1366
- async fetchAgents(network = "devnet", limit, rpcOverride) {
1258
+ async fetchAgents(network = "devnet", limit) {
1367
1259
  const filter = {
1368
1260
  kinds: [KIND_APP_HANDLER],
1369
1261
  "#t": ["elisym"]
@@ -1412,7 +1304,6 @@ var DiscoveryService = class {
1412
1304
  agent.lastSeen = ev.created_at;
1413
1305
  }
1414
1306
  }
1415
- const paidCandidates = /* @__PURE__ */ new Map();
1416
1307
  for (const ev of feedbackEvents) {
1417
1308
  if (!nostrTools.verifyEvent(ev)) {
1418
1309
  continue;
@@ -1439,33 +1330,12 @@ var DiscoveryService = class {
1439
1330
  const txTag = ev.tags.find((t) => t[0] === "tx");
1440
1331
  const txSignature = txTag?.[1];
1441
1332
  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]);
1333
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1334
+ agent.lastPaidJobAt = ev.created_at;
1335
+ agent.lastPaidJobTx = txSignature;
1448
1336
  }
1449
1337
  }
1450
1338
  }
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
1339
  agents.sort(compareAgentsByRank);
1470
1340
  return agents;
1471
1341
  }
@@ -2165,6 +2035,7 @@ var MarketplaceService = class {
2165
2035
  let status = "processing";
2166
2036
  let amount;
2167
2037
  let txHash;
2038
+ let asset;
2168
2039
  if (result) {
2169
2040
  status = "success";
2170
2041
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -2173,9 +2044,18 @@ var MarketplaceService = class {
2173
2044
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
2174
2045
  for (const fb of allFeedbacksForReq) {
2175
2046
  const txTag = fb.tags.find((t) => t[0] === "tx");
2176
- if (txTag?.[1]) {
2047
+ if (txTag?.[1] && !txHash) {
2177
2048
  txHash = txTag[1];
2178
- 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
+ }
2179
2059
  }
2180
2060
  }
2181
2061
  if (feedback) {
@@ -2204,6 +2084,7 @@ var MarketplaceService = class {
2204
2084
  resultEventId: result?.id,
2205
2085
  amount,
2206
2086
  txHash,
2087
+ asset,
2207
2088
  createdAt: req.created_at
2208
2089
  });
2209
2090
  }
@@ -2814,7 +2695,7 @@ var ElisymClient = class {
2814
2695
  payment;
2815
2696
  constructor(config = {}) {
2816
2697
  this.pool = new NostrPool(config.relays ?? RELAYS);
2817
- this.discovery = new DiscoveryService(this.pool, config.rpc);
2698
+ this.discovery = new DiscoveryService(this.pool);
2818
2699
  this.marketplace = new MarketplaceService(this.pool);
2819
2700
  this.ping = new PingService(this.pool);
2820
2701
  this.media = new MediaService(config.uploadUrl);
@@ -2942,6 +2823,89 @@ function lamportsToSol(lamports) {
2942
2823
  const frac = lamports % LAMPORTS_PER_SOL2;
2943
2824
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2944
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
+ }
2945
2909
  var SessionSpendLimitEntrySchema = zod.z.object({
2946
2910
  chain: zod.z.enum(["solana"]),
2947
2911
  token: zod.z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),