@elisym/sdk 0.10.0 → 0.10.2

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
@@ -1,7 +1,7 @@
1
1
  import { Address, TransactionSigner, Rpc, SolanaRpcApi } from '@solana/kit';
2
2
  import { Filter, Event } from 'nostr-tools';
3
- import { A as Asset } from './assets-CMf-v55Z.cjs';
4
- export { C as Chain, K as KNOWN_ASSETS, N as NATIVE_SOL, U as USDC_SOLANA_DEVNET, a as assetByKey, b as assetKey, f as formatAssetAmount, p as parseAssetAmount, r as resolveAssetFromPaymentRequest, c as resolveKnownAsset } from './assets-CMf-v55Z.cjs';
3
+ import { A as Asset } from './assets-C-nzSYD4.cjs';
4
+ export { C as Chain, K as KNOWN_ASSETS, N as NATIVE_SOL, U as USDC_SOLANA_DEVNET, a as assetByKey, b as assetKey, f as formatAssetAmount, p as parseAssetAmount, r as resolveAssetFromPaymentRequest, c as resolveKnownAsset } from './assets-C-nzSYD4.cjs';
5
5
  import { z } from 'zod';
6
6
  export { G as GlobalConfig, a as GlobalConfigSchema, S as SessionSpendLimitEntry, b as SessionSpendLimitEntrySchema } from './global-schema-CddHP2nk.cjs';
7
7
 
@@ -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
@@ -1,7 +1,7 @@
1
1
  import { Address, TransactionSigner, Rpc, SolanaRpcApi } from '@solana/kit';
2
2
  import { Filter, Event } from 'nostr-tools';
3
- import { A as Asset } from './assets-CMf-v55Z.js';
4
- export { C as Chain, K as KNOWN_ASSETS, N as NATIVE_SOL, U as USDC_SOLANA_DEVNET, a as assetByKey, b as assetKey, f as formatAssetAmount, p as parseAssetAmount, r as resolveAssetFromPaymentRequest, c as resolveKnownAsset } from './assets-CMf-v55Z.js';
3
+ import { A as Asset } from './assets-C-nzSYD4.js';
4
+ export { C as Chain, K as KNOWN_ASSETS, N as NATIVE_SOL, U as USDC_SOLANA_DEVNET, a as assetByKey, b as assetKey, f as formatAssetAmount, p as parseAssetAmount, r as resolveAssetFromPaymentRequest, c as resolveKnownAsset } from './assets-C-nzSYD4.js';
5
5
  import { z } from 'zod';
6
6
  export { G as GlobalConfig, a as GlobalConfigSchema, S as SessionSpendLimitEntry, b as SessionSpendLimitEntrySchema } from './global-schema-CddHP2nk.js';
7
7
 
@@ -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
@@ -1,7 +1,7 @@
1
1
  import { getTransferSolInstruction } from '@solana-program/system';
2
2
  import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
3
3
  import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, isAddress, getProgramDerivedAddress, assertAccountExists, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
4
- import Decimal2 from 'decimal.js-light';
4
+ import Decimal3 from 'decimal.js-light';
5
5
  import { z } from 'zod';
6
6
  import { verifyEvent, finalizeEvent, getPublicKey, nip19, generateSecretKey, SimplePool } from 'nostr-tools';
7
7
  import * as nip44 from 'nostr-tools/nip44';
@@ -151,8 +151,6 @@ async function getProtocolConfig(rpc, programId, options) {
151
151
  );
152
152
  }
153
153
  }
154
-
155
- // src/payment/assets.ts
156
154
  var NATIVE_SOL = {
157
155
  chain: "solana",
158
156
  token: "sol",
@@ -233,16 +231,10 @@ function parseAssetAmount(asset, human) {
233
231
  }
234
232
  return raw;
235
233
  }
234
+ var FormatDecimal = Decimal3.clone({ toExpNeg: -100, toExpPos: 100, precision: 50 });
236
235
  function formatAssetAmount(asset, raw) {
237
- const sign = raw < 0n ? "-" : "";
238
- const abs = raw < 0n ? -raw : raw;
239
- const unit = 10n ** BigInt(asset.decimals);
240
- const whole = abs / unit;
241
- const frac = abs % unit;
242
- if (asset.decimals === 0) {
243
- return `${sign}${whole} ${asset.symbol}`;
244
- }
245
- return `${sign}${whole}.${frac.toString().padStart(asset.decimals, "0")} ${asset.symbol}`;
236
+ const value = new FormatDecimal(raw.toString()).div(new FormatDecimal(10).pow(asset.decimals));
237
+ return `${value.toString()} ${asset.symbol}`;
246
238
  }
247
239
  var BPS_DENOMINATOR = 1e4;
248
240
  function assertLamports(value, field) {
@@ -260,7 +252,7 @@ function calculateProtocolFee(amount, feeBps) {
260
252
  if (amount === 0 || feeBps === 0) {
261
253
  return 0;
262
254
  }
263
- return new Decimal2(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal2.ROUND_CEIL).toNumber();
255
+ return new Decimal3(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal3.ROUND_CEIL).toNumber();
264
256
  }
265
257
  function validateExpiry(createdAt, expirySecs) {
266
258
  if (!Number.isInteger(createdAt) || createdAt <= 0) {
@@ -994,95 +986,9 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
994
986
  options
995
987
  );
996
988
  }
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
989
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1083
990
  var RANKING_BUCKET_SIZE_SECS = 60;
1084
991
  var COLD_START_BUCKET = -Infinity;
1085
- var MAX_PAID_CANDIDATES_PER_AGENT = 5;
1086
992
  function toDTag(name) {
1087
993
  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
994
  if (!tag) {
@@ -1206,37 +1112,9 @@ function buildAgentsFromEvents(events, network) {
1206
1112
  }
1207
1113
  return agentMap;
1208
1114
  }
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
1115
  var DiscoveryService = class {
1233
- constructor(pool, defaultRpc) {
1116
+ constructor(pool) {
1234
1117
  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
1118
  }
1241
1119
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1242
1120
  async fetchAllAgentCount() {
@@ -1325,20 +1203,26 @@ var DiscoveryService = class {
1325
1203
  return agents;
1326
1204
  }
1327
1205
  /**
1328
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1206
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1207
+ * positive-feedback rate.
1329
1208
  *
1330
1209
  * 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
1210
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1211
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1333
1212
  * sentinel bucket below all populated buckets.
1334
1213
  * 2. Within a bucket, sort by positive review rate descending.
1335
1214
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1336
1215
  *
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.
1216
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
1217
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
1218
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
1219
+ * candidates), and the resulting 429s blocked discovery entirely. This
1220
+ * means a malicious customer can publish a fake `payment-completed` to lift
1221
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
1222
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
1223
+ * provider.
1340
1224
  */
1341
- async fetchAgents(network = "devnet", limit, rpcOverride) {
1225
+ async fetchAgents(network = "devnet", limit) {
1342
1226
  const filter = {
1343
1227
  kinds: [KIND_APP_HANDLER],
1344
1228
  "#t": ["elisym"]
@@ -1387,7 +1271,6 @@ var DiscoveryService = class {
1387
1271
  agent.lastSeen = ev.created_at;
1388
1272
  }
1389
1273
  }
1390
- const paidCandidates = /* @__PURE__ */ new Map();
1391
1274
  for (const ev of feedbackEvents) {
1392
1275
  if (!verifyEvent(ev)) {
1393
1276
  continue;
@@ -1414,33 +1297,12 @@ var DiscoveryService = class {
1414
1297
  const txTag = ev.tags.find((t) => t[0] === "tx");
1415
1298
  const txSignature = txTag?.[1];
1416
1299
  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]);
1300
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1301
+ agent.lastPaidJobAt = ev.created_at;
1302
+ agent.lastPaidJobTx = txSignature;
1423
1303
  }
1424
1304
  }
1425
1305
  }
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
1306
  agents.sort(compareAgentsByRank);
1445
1307
  return agents;
1446
1308
  }
@@ -2140,6 +2002,7 @@ var MarketplaceService = class {
2140
2002
  let status = "processing";
2141
2003
  let amount;
2142
2004
  let txHash;
2005
+ let asset;
2143
2006
  if (result) {
2144
2007
  status = "success";
2145
2008
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -2148,9 +2011,18 @@ var MarketplaceService = class {
2148
2011
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
2149
2012
  for (const fb of allFeedbacksForReq) {
2150
2013
  const txTag = fb.tags.find((t) => t[0] === "tx");
2151
- if (txTag?.[1]) {
2014
+ if (txTag?.[1] && !txHash) {
2152
2015
  txHash = txTag[1];
2153
- break;
2016
+ }
2017
+ if (!asset) {
2018
+ const amtTag = fb.tags.find((t) => t[0] === "amount");
2019
+ const requestJson = amtTag?.[2];
2020
+ if (requestJson) {
2021
+ const parsed = parsePaymentRequest(requestJson);
2022
+ if (parsed.ok && parsed.data.asset) {
2023
+ asset = parsed.data.asset;
2024
+ }
2025
+ }
2154
2026
  }
2155
2027
  }
2156
2028
  if (feedback) {
@@ -2179,6 +2051,7 @@ var MarketplaceService = class {
2179
2051
  resultEventId: result?.id,
2180
2052
  amount,
2181
2053
  txHash,
2054
+ asset,
2182
2055
  createdAt: req.created_at
2183
2056
  });
2184
2057
  }
@@ -2789,7 +2662,7 @@ var ElisymClient = class {
2789
2662
  payment;
2790
2663
  constructor(config = {}) {
2791
2664
  this.pool = new NostrPool(config.relays ?? RELAYS);
2792
- this.discovery = new DiscoveryService(this.pool, config.rpc);
2665
+ this.discovery = new DiscoveryService(this.pool);
2793
2666
  this.marketplace = new MarketplaceService(this.pool);
2794
2667
  this.ping = new PingService(this.pool);
2795
2668
  this.media = new MediaService(config.uploadUrl);
@@ -2917,6 +2790,89 @@ function lamportsToSol(lamports) {
2917
2790
  const frac = lamports % LAMPORTS_PER_SOL2;
2918
2791
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2919
2792
  }
2793
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
2794
+ var verifyCache = /* @__PURE__ */ new Map();
2795
+ function clearQuickVerifyCache() {
2796
+ verifyCache.clear();
2797
+ }
2798
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
2799
+ if (!txSignature) {
2800
+ return { verified: false, txSignature: "", reason: "invalid_input" };
2801
+ }
2802
+ if (!expectedRecipient || !isAddress(expectedRecipient)) {
2803
+ return { verified: false, txSignature, reason: "invalid_input" };
2804
+ }
2805
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
2806
+ const cached = verifyCache.get(cacheKey2);
2807
+ if (cached) {
2808
+ if (cached.result.verified) {
2809
+ return cached.result;
2810
+ }
2811
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
2812
+ return cached.result;
2813
+ }
2814
+ }
2815
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
2816
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
2817
+ return result;
2818
+ }
2819
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2820
+ const sigStr = txSignature;
2821
+ if (!rpc || typeof rpc.getTransaction !== "function") {
2822
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2823
+ }
2824
+ let tx;
2825
+ try {
2826
+ tx = await rpc.getTransaction(txSignature, {
2827
+ commitment: "confirmed",
2828
+ encoding: "json",
2829
+ maxSupportedTransactionVersion: 0
2830
+ }).send();
2831
+ } catch {
2832
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2833
+ }
2834
+ if (!tx) {
2835
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
2836
+ }
2837
+ if (!tx.meta || tx.meta.err) {
2838
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
2839
+ }
2840
+ const accountKeys = tx.transaction.message.accountKeys;
2841
+ const recipientStr = expectedRecipient;
2842
+ const recipientIdx = accountKeys.indexOf(recipientStr);
2843
+ if (recipientIdx !== -1) {
2844
+ const preBalances = tx.meta.preBalances;
2845
+ const postBalances = tx.meta.postBalances;
2846
+ if (preBalances && postBalances) {
2847
+ const pre = preBalances[recipientIdx];
2848
+ const post = postBalances[recipientIdx];
2849
+ if (pre !== void 0 && post !== void 0) {
2850
+ const delta = BigInt(post) - BigInt(pre);
2851
+ if (delta > 0n) {
2852
+ return { verified: true, txSignature: sigStr };
2853
+ }
2854
+ }
2855
+ }
2856
+ }
2857
+ const postTokenBalances = tx.meta.postTokenBalances;
2858
+ const preTokenBalances = tx.meta.preTokenBalances;
2859
+ if (postTokenBalances) {
2860
+ for (const post of postTokenBalances) {
2861
+ if (post.owner !== recipientStr) {
2862
+ continue;
2863
+ }
2864
+ const pre = preTokenBalances?.find(
2865
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
2866
+ );
2867
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
2868
+ const postAmount = BigInt(post.uiTokenAmount.amount);
2869
+ if (postAmount > preAmount) {
2870
+ return { verified: true, txSignature: sigStr };
2871
+ }
2872
+ }
2873
+ }
2874
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2875
+ }
2920
2876
  var SessionSpendLimitEntrySchema = z.object({
2921
2877
  chain: z.enum(["solana"]),
2922
2878
  token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -2927,7 +2883,7 @@ var GlobalConfigSchema = z.object({
2927
2883
  session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional()
2928
2884
  }).strict();
2929
2885
  function formatSol(lamports) {
2930
- const sol = new Decimal2(lamports).div(LAMPORTS_PER_SOL);
2886
+ const sol = new Decimal3(lamports).div(LAMPORTS_PER_SOL);
2931
2887
  if (sol.gte(1e6)) {
2932
2888
  return `${sol.idiv(1e6)}m SOL`;
2933
2889
  }
@@ -2941,12 +2897,12 @@ function compactSol(sol) {
2941
2897
  return "0";
2942
2898
  }
2943
2899
  if (sol.gte(1e3)) {
2944
- return sol.toDecimalPlaces(0, Decimal2.ROUND_FLOOR).toString();
2900
+ return sol.toDecimalPlaces(0, Decimal3.ROUND_FLOOR).toString();
2945
2901
  }
2946
2902
  const maxFrac = 9;
2947
2903
  for (let d = 1; d <= maxFrac; d++) {
2948
2904
  const s = sol.toFixed(d);
2949
- if (new Decimal2(s).eq(sol)) {
2905
+ if (new Decimal3(s).eq(sol)) {
2950
2906
  return s.replace(/0+$/, "").replace(/\.$/, "");
2951
2907
  }
2952
2908
  }