@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.d.cts CHANGED
@@ -97,6 +97,13 @@ interface Job {
97
97
  amount?: number;
98
98
  txHash?: string;
99
99
  createdAt: number;
100
+ /**
101
+ * Payment asset, derived from the `payment-required` feedback's embedded
102
+ * payment request when present. Undefined means either no payment-required
103
+ * feedback was observed for this job, or the embedded request was missing
104
+ * an `asset` field (treated as native SOL by callers).
105
+ */
106
+ asset?: PaymentAssetRef;
100
107
  }
101
108
  interface SubmitJobOptions {
102
109
  /** Job input text. Sent unencrypted if providerPubkey is not set. */
@@ -349,10 +356,7 @@ declare function computeRankKey(agent: Agent): RankKey;
349
356
  declare function compareAgentsByRank(a: Agent, b: Agent): number;
350
357
  declare class DiscoveryService {
351
358
  private pool;
352
- private defaultRpc?;
353
- constructor(pool: NostrPool, defaultRpc?: Rpc<SolanaRpcApi> | undefined);
354
- /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
355
- setRpc(rpc: Rpc<SolanaRpcApi> | undefined): void;
359
+ constructor(pool: NostrPool);
356
360
  /** Count elisym agents (kind:31990 with "elisym" tag). */
357
361
  fetchAllAgentCount(): Promise<number>;
358
362
  /**
@@ -371,20 +375,26 @@ declare class DiscoveryService {
371
375
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
372
376
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
373
377
  /**
374
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
378
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
379
+ * positive-feedback rate.
375
380
  *
376
381
  * Ranking algorithm:
377
- * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
378
- * verified payment). Cold-start agents (no verified paid job) go into a
382
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
383
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
379
384
  * sentinel bucket below all populated buckets.
380
385
  * 2. Within a bucket, sort by positive review rate descending.
381
386
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
382
387
  *
383
- * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
384
- * If `rpc` is not configured, all agents fall through to cold-start and order
385
- * is determined by `lastSeen` only.
388
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
389
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
390
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
391
+ * candidates), and the resulting 429s blocked discovery entirely. This
392
+ * means a malicious customer can publish a fake `payment-completed` to lift
393
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
394
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
395
+ * provider.
386
396
  */
387
- fetchAgents(network?: Network, limit?: number, rpcOverride?: Rpc<SolanaRpcApi>): Promise<Agent[]>;
397
+ fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
388
398
  /**
389
399
  * Publish a capability card (kind:31990) as a provider.
390
400
  * Solana address is validated for Base58 format only - full decode
@@ -528,11 +538,6 @@ interface ElisymClientFullConfig extends ElisymClientConfig {
528
538
  payment?: PaymentStrategy;
529
539
  /** Custom upload URL for file uploads (defaults to nostr.build). */
530
540
  uploadUrl?: string;
531
- /**
532
- * Solana RPC used by `discovery.fetchAgents` for on-chain payment verification.
533
- * If omitted, ranking falls back to NIP-89 freshness only (no paid-job promotion).
534
- */
535
- rpc?: Rpc<SolanaRpcApi>;
536
541
  }
537
542
  declare class ElisymClient {
538
543
  readonly pool: NostrPool;
package/dist/index.d.ts CHANGED
@@ -97,6 +97,13 @@ interface Job {
97
97
  amount?: number;
98
98
  txHash?: string;
99
99
  createdAt: number;
100
+ /**
101
+ * Payment asset, derived from the `payment-required` feedback's embedded
102
+ * payment request when present. Undefined means either no payment-required
103
+ * feedback was observed for this job, or the embedded request was missing
104
+ * an `asset` field (treated as native SOL by callers).
105
+ */
106
+ asset?: PaymentAssetRef;
100
107
  }
101
108
  interface SubmitJobOptions {
102
109
  /** Job input text. Sent unencrypted if providerPubkey is not set. */
@@ -349,10 +356,7 @@ declare function computeRankKey(agent: Agent): RankKey;
349
356
  declare function compareAgentsByRank(a: Agent, b: Agent): number;
350
357
  declare class DiscoveryService {
351
358
  private pool;
352
- private defaultRpc?;
353
- constructor(pool: NostrPool, defaultRpc?: Rpc<SolanaRpcApi> | undefined);
354
- /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
355
- setRpc(rpc: Rpc<SolanaRpcApi> | undefined): void;
359
+ constructor(pool: NostrPool);
356
360
  /** Count elisym agents (kind:31990 with "elisym" tag). */
357
361
  fetchAllAgentCount(): Promise<number>;
358
362
  /**
@@ -371,20 +375,26 @@ declare class DiscoveryService {
371
375
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
372
376
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
373
377
  /**
374
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
378
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
379
+ * positive-feedback rate.
375
380
  *
376
381
  * Ranking algorithm:
377
- * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
378
- * verified payment). Cold-start agents (no verified paid job) go into a
382
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
383
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
379
384
  * sentinel bucket below all populated buckets.
380
385
  * 2. Within a bucket, sort by positive review rate descending.
381
386
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
382
387
  *
383
- * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
384
- * If `rpc` is not configured, all agents fall through to cold-start and order
385
- * is determined by `lastSeen` only.
388
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
389
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
390
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
391
+ * candidates), and the resulting 429s blocked discovery entirely. This
392
+ * means a malicious customer can publish a fake `payment-completed` to lift
393
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
394
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
395
+ * provider.
386
396
  */
387
- fetchAgents(network?: Network, limit?: number, rpcOverride?: Rpc<SolanaRpcApi>): Promise<Agent[]>;
397
+ fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
388
398
  /**
389
399
  * Publish a capability card (kind:31990) as a provider.
390
400
  * Solana address is validated for Base58 format only - full decode
@@ -528,11 +538,6 @@ interface ElisymClientFullConfig extends ElisymClientConfig {
528
538
  payment?: PaymentStrategy;
529
539
  /** Custom upload URL for file uploads (defaults to nostr.build). */
530
540
  uploadUrl?: string;
531
- /**
532
- * Solana RPC used by `discovery.fetchAgents` for on-chain payment verification.
533
- * If omitted, ranking falls back to NIP-89 freshness only (no paid-job promotion).
534
- */
535
- rpc?: Rpc<SolanaRpcApi>;
536
541
  }
537
542
  declare class ElisymClient {
538
543
  readonly pool: NostrPool;
package/dist/index.js CHANGED
@@ -994,95 +994,9 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
994
994
  options
995
995
  );
996
996
  }
997
- var NEGATIVE_CACHE_TTL_MS = 6e4;
998
- var verifyCache = /* @__PURE__ */ new Map();
999
- function clearQuickVerifyCache() {
1000
- verifyCache.clear();
1001
- }
1002
- async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
1003
- if (!txSignature) {
1004
- return { verified: false, txSignature: "", reason: "invalid_input" };
1005
- }
1006
- if (!expectedRecipient || !isAddress(expectedRecipient)) {
1007
- return { verified: false, txSignature, reason: "invalid_input" };
1008
- }
1009
- const cacheKey2 = `${txSignature}:${expectedRecipient}`;
1010
- const cached = verifyCache.get(cacheKey2);
1011
- if (cached) {
1012
- if (cached.result.verified) {
1013
- return cached.result;
1014
- }
1015
- if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
1016
- return cached.result;
1017
- }
1018
- }
1019
- const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
1020
- verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
1021
- return result;
1022
- }
1023
- async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
1024
- const sigStr = txSignature;
1025
- if (!rpc || typeof rpc.getTransaction !== "function") {
1026
- return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1027
- }
1028
- let tx;
1029
- try {
1030
- tx = await rpc.getTransaction(txSignature, {
1031
- commitment: "confirmed",
1032
- encoding: "json",
1033
- maxSupportedTransactionVersion: 0
1034
- }).send();
1035
- } catch {
1036
- return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1037
- }
1038
- if (!tx) {
1039
- return { verified: false, txSignature: sigStr, reason: "not_found" };
1040
- }
1041
- if (!tx.meta || tx.meta.err) {
1042
- return { verified: false, txSignature: sigStr, reason: "tx_failed" };
1043
- }
1044
- const accountKeys = tx.transaction.message.accountKeys;
1045
- const recipientStr = expectedRecipient;
1046
- const recipientIdx = accountKeys.indexOf(recipientStr);
1047
- if (recipientIdx !== -1) {
1048
- const preBalances = tx.meta.preBalances;
1049
- const postBalances = tx.meta.postBalances;
1050
- if (preBalances && postBalances) {
1051
- const pre = preBalances[recipientIdx];
1052
- const post = postBalances[recipientIdx];
1053
- if (pre !== void 0 && post !== void 0) {
1054
- const delta = BigInt(post) - BigInt(pre);
1055
- if (delta > 0n) {
1056
- return { verified: true, txSignature: sigStr };
1057
- }
1058
- }
1059
- }
1060
- }
1061
- const postTokenBalances = tx.meta.postTokenBalances;
1062
- const preTokenBalances = tx.meta.preTokenBalances;
1063
- if (postTokenBalances) {
1064
- for (const post of postTokenBalances) {
1065
- if (post.owner !== recipientStr) {
1066
- continue;
1067
- }
1068
- const pre = preTokenBalances?.find(
1069
- (entry) => entry.owner === recipientStr && entry.mint === post.mint
1070
- );
1071
- const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1072
- const postAmount = BigInt(post.uiTokenAmount.amount);
1073
- if (postAmount > preAmount) {
1074
- return { verified: true, txSignature: sigStr };
1075
- }
1076
- }
1077
- }
1078
- return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
1079
- }
1080
-
1081
- // src/services/discovery.ts
1082
997
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1083
998
  var RANKING_BUCKET_SIZE_SECS = 60;
1084
999
  var COLD_START_BUCKET = -Infinity;
1085
- var MAX_PAID_CANDIDATES_PER_AGENT = 5;
1086
1000
  function toDTag(name) {
1087
1001
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
1088
1002
  if (!tag) {
@@ -1206,37 +1120,9 @@ function buildAgentsFromEvents(events, network) {
1206
1120
  }
1207
1121
  return agentMap;
1208
1122
  }
1209
- function pickSolanaAddress(agent) {
1210
- for (const card of agent.cards) {
1211
- const addr = card.payment?.address;
1212
- if (card.payment?.chain === "solana" && typeof addr === "string" && addr.length > 0) {
1213
- return addr;
1214
- }
1215
- }
1216
- return null;
1217
- }
1218
- async function verifyNewestPaidCandidate(rpc, recipient, candidatesNewestFirst) {
1219
- const settled = await Promise.allSettled(
1220
- candidatesNewestFirst.map(
1221
- (candidate) => verifyJobPaymentQuick(rpc, candidate.txSignature, recipient)
1222
- )
1223
- );
1224
- for (let i = 0; i < settled.length; i++) {
1225
- const entry = settled[i];
1226
- if (entry?.status === "fulfilled" && entry.value.verified) {
1227
- return candidatesNewestFirst[i] ?? null;
1228
- }
1229
- }
1230
- return null;
1231
- }
1232
1123
  var DiscoveryService = class {
1233
- constructor(pool, defaultRpc) {
1124
+ constructor(pool) {
1234
1125
  this.pool = pool;
1235
- this.defaultRpc = defaultRpc;
1236
- }
1237
- /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
1238
- setRpc(rpc) {
1239
- this.defaultRpc = rpc;
1240
1126
  }
1241
1127
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1242
1128
  async fetchAllAgentCount() {
@@ -1325,20 +1211,26 @@ var DiscoveryService = class {
1325
1211
  return agents;
1326
1212
  }
1327
1213
  /**
1328
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1214
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1215
+ * positive-feedback rate.
1329
1216
  *
1330
1217
  * Ranking algorithm:
1331
- * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
1332
- * verified payment). Cold-start agents (no verified paid job) go into a
1218
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1219
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1333
1220
  * sentinel bucket below all populated buckets.
1334
1221
  * 2. Within a bucket, sort by positive review rate descending.
1335
1222
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1336
1223
  *
1337
- * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
1338
- * If `rpc` is not configured, all agents fall through to cold-start and order
1339
- * is determined by `lastSeen` only.
1224
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
1225
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
1226
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
1227
+ * candidates), and the resulting 429s blocked discovery entirely. This
1228
+ * means a malicious customer can publish a fake `payment-completed` to lift
1229
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
1230
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
1231
+ * provider.
1340
1232
  */
1341
- async fetchAgents(network = "devnet", limit, rpcOverride) {
1233
+ async fetchAgents(network = "devnet", limit) {
1342
1234
  const filter = {
1343
1235
  kinds: [KIND_APP_HANDLER],
1344
1236
  "#t": ["elisym"]
@@ -1387,7 +1279,6 @@ var DiscoveryService = class {
1387
1279
  agent.lastSeen = ev.created_at;
1388
1280
  }
1389
1281
  }
1390
- const paidCandidates = /* @__PURE__ */ new Map();
1391
1282
  for (const ev of feedbackEvents) {
1392
1283
  if (!verifyEvent(ev)) {
1393
1284
  continue;
@@ -1414,33 +1305,12 @@ var DiscoveryService = class {
1414
1305
  const txTag = ev.tags.find((t) => t[0] === "tx");
1415
1306
  const txSignature = txTag?.[1];
1416
1307
  if (status === "payment-completed" && typeof txSignature === "string" && txSignature) {
1417
- const list = paidCandidates.get(targetPubkey);
1418
- const candidate = { txSignature, createdAt: ev.created_at };
1419
- if (list) {
1420
- list.push(candidate);
1421
- } else {
1422
- paidCandidates.set(targetPubkey, [candidate]);
1308
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1309
+ agent.lastPaidJobAt = ev.created_at;
1310
+ agent.lastPaidJobTx = txSignature;
1423
1311
  }
1424
1312
  }
1425
1313
  }
1426
- const rpc = rpcOverride ?? this.defaultRpc;
1427
- if (rpc && paidCandidates.size > 0) {
1428
- const perAgent = Array.from(paidCandidates.entries()).map(([agentPubkey, list]) => {
1429
- const agent = agentMap.get(agentPubkey);
1430
- const recipient = agent ? pickSolanaAddress(agent) : null;
1431
- if (!agent || !recipient) {
1432
- return Promise.resolve();
1433
- }
1434
- const ordered = [...list].sort((a, b) => b.createdAt - a.createdAt).slice(0, MAX_PAID_CANDIDATES_PER_AGENT);
1435
- return verifyNewestPaidCandidate(rpc, recipient, ordered).then((winner) => {
1436
- if (winner) {
1437
- agent.lastPaidJobAt = winner.createdAt;
1438
- agent.lastPaidJobTx = winner.txSignature;
1439
- }
1440
- });
1441
- });
1442
- await Promise.all(perAgent);
1443
- }
1444
1314
  agents.sort(compareAgentsByRank);
1445
1315
  return agents;
1446
1316
  }
@@ -2140,6 +2010,7 @@ var MarketplaceService = class {
2140
2010
  let status = "processing";
2141
2011
  let amount;
2142
2012
  let txHash;
2013
+ let asset;
2143
2014
  if (result) {
2144
2015
  status = "success";
2145
2016
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -2148,9 +2019,18 @@ var MarketplaceService = class {
2148
2019
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
2149
2020
  for (const fb of allFeedbacksForReq) {
2150
2021
  const txTag = fb.tags.find((t) => t[0] === "tx");
2151
- if (txTag?.[1]) {
2022
+ if (txTag?.[1] && !txHash) {
2152
2023
  txHash = txTag[1];
2153
- break;
2024
+ }
2025
+ if (!asset) {
2026
+ const amtTag = fb.tags.find((t) => t[0] === "amount");
2027
+ const requestJson = amtTag?.[2];
2028
+ if (requestJson) {
2029
+ const parsed = parsePaymentRequest(requestJson);
2030
+ if (parsed.ok && parsed.data.asset) {
2031
+ asset = parsed.data.asset;
2032
+ }
2033
+ }
2154
2034
  }
2155
2035
  }
2156
2036
  if (feedback) {
@@ -2179,6 +2059,7 @@ var MarketplaceService = class {
2179
2059
  resultEventId: result?.id,
2180
2060
  amount,
2181
2061
  txHash,
2062
+ asset,
2182
2063
  createdAt: req.created_at
2183
2064
  });
2184
2065
  }
@@ -2789,7 +2670,7 @@ var ElisymClient = class {
2789
2670
  payment;
2790
2671
  constructor(config = {}) {
2791
2672
  this.pool = new NostrPool(config.relays ?? RELAYS);
2792
- this.discovery = new DiscoveryService(this.pool, config.rpc);
2673
+ this.discovery = new DiscoveryService(this.pool);
2793
2674
  this.marketplace = new MarketplaceService(this.pool);
2794
2675
  this.ping = new PingService(this.pool);
2795
2676
  this.media = new MediaService(config.uploadUrl);
@@ -2917,6 +2798,89 @@ function lamportsToSol(lamports) {
2917
2798
  const frac = lamports % LAMPORTS_PER_SOL2;
2918
2799
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2919
2800
  }
2801
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
2802
+ var verifyCache = /* @__PURE__ */ new Map();
2803
+ function clearQuickVerifyCache() {
2804
+ verifyCache.clear();
2805
+ }
2806
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
2807
+ if (!txSignature) {
2808
+ return { verified: false, txSignature: "", reason: "invalid_input" };
2809
+ }
2810
+ if (!expectedRecipient || !isAddress(expectedRecipient)) {
2811
+ return { verified: false, txSignature, reason: "invalid_input" };
2812
+ }
2813
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
2814
+ const cached = verifyCache.get(cacheKey2);
2815
+ if (cached) {
2816
+ if (cached.result.verified) {
2817
+ return cached.result;
2818
+ }
2819
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
2820
+ return cached.result;
2821
+ }
2822
+ }
2823
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
2824
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
2825
+ return result;
2826
+ }
2827
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2828
+ const sigStr = txSignature;
2829
+ if (!rpc || typeof rpc.getTransaction !== "function") {
2830
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2831
+ }
2832
+ let tx;
2833
+ try {
2834
+ tx = await rpc.getTransaction(txSignature, {
2835
+ commitment: "confirmed",
2836
+ encoding: "json",
2837
+ maxSupportedTransactionVersion: 0
2838
+ }).send();
2839
+ } catch {
2840
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2841
+ }
2842
+ if (!tx) {
2843
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
2844
+ }
2845
+ if (!tx.meta || tx.meta.err) {
2846
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
2847
+ }
2848
+ const accountKeys = tx.transaction.message.accountKeys;
2849
+ const recipientStr = expectedRecipient;
2850
+ const recipientIdx = accountKeys.indexOf(recipientStr);
2851
+ if (recipientIdx !== -1) {
2852
+ const preBalances = tx.meta.preBalances;
2853
+ const postBalances = tx.meta.postBalances;
2854
+ if (preBalances && postBalances) {
2855
+ const pre = preBalances[recipientIdx];
2856
+ const post = postBalances[recipientIdx];
2857
+ if (pre !== void 0 && post !== void 0) {
2858
+ const delta = BigInt(post) - BigInt(pre);
2859
+ if (delta > 0n) {
2860
+ return { verified: true, txSignature: sigStr };
2861
+ }
2862
+ }
2863
+ }
2864
+ }
2865
+ const postTokenBalances = tx.meta.postTokenBalances;
2866
+ const preTokenBalances = tx.meta.preTokenBalances;
2867
+ if (postTokenBalances) {
2868
+ for (const post of postTokenBalances) {
2869
+ if (post.owner !== recipientStr) {
2870
+ continue;
2871
+ }
2872
+ const pre = preTokenBalances?.find(
2873
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
2874
+ );
2875
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
2876
+ const postAmount = BigInt(post.uiTokenAmount.amount);
2877
+ if (postAmount > preAmount) {
2878
+ return { verified: true, txSignature: sigStr };
2879
+ }
2880
+ }
2881
+ }
2882
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2883
+ }
2920
2884
  var SessionSpendLimitEntrySchema = z.object({
2921
2885
  chain: z.enum(["solana"]),
2922
2886
  token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),