@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.
@@ -52,7 +52,11 @@ declare function resolveAssetFromPaymentRequest(request: {
52
52
  * call-sites safe).
53
53
  */
54
54
  declare function parseAssetAmount(asset: Asset, human: string): bigint;
55
- /** Format raw subunits back to `"<whole>.<frac> <SYMBOL>"`. Keeps all `decimals` digits. */
55
+ /**
56
+ * Format raw subunits back to `"<value> <SYMBOL>"`. Trailing zeros and a bare
57
+ * trailing dot are stripped, so 0.01 USDC renders as `"0.01 USDC"` rather than
58
+ * `"0.010000 USDC"`.
59
+ */
56
60
  declare function formatAssetAmount(asset: Asset, raw: bigint): string;
57
61
 
58
62
  export { type Asset as A, type Chain as C, KNOWN_ASSETS as K, NATIVE_SOL as N, USDC_SOLANA_DEVNET as U, assetByKey as a, assetKey as b, resolveKnownAsset as c, formatAssetAmount as f, parseAssetAmount as p, resolveAssetFromPaymentRequest as r };
@@ -52,7 +52,11 @@ declare function resolveAssetFromPaymentRequest(request: {
52
52
  * call-sites safe).
53
53
  */
54
54
  declare function parseAssetAmount(asset: Asset, human: string): bigint;
55
- /** Format raw subunits back to `"<whole>.<frac> <SYMBOL>"`. Keeps all `decimals` digits. */
55
+ /**
56
+ * Format raw subunits back to `"<value> <SYMBOL>"`. Trailing zeros and a bare
57
+ * trailing dot are stripped, so 0.01 USDC renders as `"0.01 USDC"` rather than
58
+ * `"0.010000 USDC"`.
59
+ */
56
60
  declare function formatAssetAmount(asset: Asset, raw: bigint): string;
57
61
 
58
62
  export { type Asset as A, type Chain as C, KNOWN_ASSETS as K, NATIVE_SOL as N, USDC_SOLANA_DEVNET as U, assetByKey as a, assetKey as b, resolveKnownAsset as c, formatAssetAmount as f, parseAssetAmount as p, resolveAssetFromPaymentRequest as r };
package/dist/index.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
  var system = require('@solana-program/system');
4
4
  var token = require('@solana-program/token');
5
5
  var kit = require('@solana/kit');
6
- var Decimal2 = require('decimal.js-light');
6
+ var Decimal3 = require('decimal.js-light');
7
7
  var zod = require('zod');
8
8
  var nostrTools = require('nostr-tools');
9
9
  var nip44 = require('nostr-tools/nip44');
@@ -28,7 +28,7 @@ function _interopNamespace(e) {
28
28
  return Object.freeze(n);
29
29
  }
30
30
 
31
- var Decimal2__default = /*#__PURE__*/_interopDefault(Decimal2);
31
+ var Decimal3__default = /*#__PURE__*/_interopDefault(Decimal3);
32
32
  var nip44__namespace = /*#__PURE__*/_interopNamespace(nip44);
33
33
 
34
34
  // src/constants.ts
@@ -176,8 +176,6 @@ async function getProtocolConfig(rpc, programId, options) {
176
176
  );
177
177
  }
178
178
  }
179
-
180
- // src/payment/assets.ts
181
179
  var NATIVE_SOL = {
182
180
  chain: "solana",
183
181
  token: "sol",
@@ -258,16 +256,10 @@ function parseAssetAmount(asset, human) {
258
256
  }
259
257
  return raw;
260
258
  }
259
+ var FormatDecimal = Decimal3__default.default.clone({ toExpNeg: -100, toExpPos: 100, precision: 50 });
261
260
  function formatAssetAmount(asset, raw) {
262
- const sign = raw < 0n ? "-" : "";
263
- const abs = raw < 0n ? -raw : raw;
264
- const unit = 10n ** BigInt(asset.decimals);
265
- const whole = abs / unit;
266
- const frac = abs % unit;
267
- if (asset.decimals === 0) {
268
- return `${sign}${whole} ${asset.symbol}`;
269
- }
270
- return `${sign}${whole}.${frac.toString().padStart(asset.decimals, "0")} ${asset.symbol}`;
261
+ const value = new FormatDecimal(raw.toString()).div(new FormatDecimal(10).pow(asset.decimals));
262
+ return `${value.toString()} ${asset.symbol}`;
271
263
  }
272
264
  var BPS_DENOMINATOR = 1e4;
273
265
  function assertLamports(value, field) {
@@ -285,7 +277,7 @@ function calculateProtocolFee(amount, feeBps) {
285
277
  if (amount === 0 || feeBps === 0) {
286
278
  return 0;
287
279
  }
288
- return new Decimal2__default.default(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal2__default.default.ROUND_CEIL).toNumber();
280
+ return new Decimal3__default.default(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal3__default.default.ROUND_CEIL).toNumber();
289
281
  }
290
282
  function validateExpiry(createdAt, expirySecs) {
291
283
  if (!Number.isInteger(createdAt) || createdAt <= 0) {
@@ -1019,95 +1011,9 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1019
1011
  options
1020
1012
  );
1021
1013
  }
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
1014
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1108
1015
  var RANKING_BUCKET_SIZE_SECS = 60;
1109
1016
  var COLD_START_BUCKET = -Infinity;
1110
- var MAX_PAID_CANDIDATES_PER_AGENT = 5;
1111
1017
  function toDTag(name) {
1112
1018
  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
1019
  if (!tag) {
@@ -1231,37 +1137,9 @@ function buildAgentsFromEvents(events, network) {
1231
1137
  }
1232
1138
  return agentMap;
1233
1139
  }
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
1140
  var DiscoveryService = class {
1258
- constructor(pool, defaultRpc) {
1141
+ constructor(pool) {
1259
1142
  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
1143
  }
1266
1144
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1267
1145
  async fetchAllAgentCount() {
@@ -1350,20 +1228,26 @@ var DiscoveryService = class {
1350
1228
  return agents;
1351
1229
  }
1352
1230
  /**
1353
- * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1231
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1232
+ * positive-feedback rate.
1354
1233
  *
1355
1234
  * 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
1235
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1236
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1358
1237
  * sentinel bucket below all populated buckets.
1359
1238
  * 2. Within a bucket, sort by positive review rate descending.
1360
1239
  * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1361
1240
  *
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.
1241
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
1242
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
1243
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
1244
+ * candidates), and the resulting 429s blocked discovery entirely. This
1245
+ * means a malicious customer can publish a fake `payment-completed` to lift
1246
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
1247
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
1248
+ * provider.
1365
1249
  */
1366
- async fetchAgents(network = "devnet", limit, rpcOverride) {
1250
+ async fetchAgents(network = "devnet", limit) {
1367
1251
  const filter = {
1368
1252
  kinds: [KIND_APP_HANDLER],
1369
1253
  "#t": ["elisym"]
@@ -1412,7 +1296,6 @@ var DiscoveryService = class {
1412
1296
  agent.lastSeen = ev.created_at;
1413
1297
  }
1414
1298
  }
1415
- const paidCandidates = /* @__PURE__ */ new Map();
1416
1299
  for (const ev of feedbackEvents) {
1417
1300
  if (!nostrTools.verifyEvent(ev)) {
1418
1301
  continue;
@@ -1439,33 +1322,12 @@ var DiscoveryService = class {
1439
1322
  const txTag = ev.tags.find((t) => t[0] === "tx");
1440
1323
  const txSignature = txTag?.[1];
1441
1324
  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]);
1325
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1326
+ agent.lastPaidJobAt = ev.created_at;
1327
+ agent.lastPaidJobTx = txSignature;
1448
1328
  }
1449
1329
  }
1450
1330
  }
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
1331
  agents.sort(compareAgentsByRank);
1470
1332
  return agents;
1471
1333
  }
@@ -2165,6 +2027,7 @@ var MarketplaceService = class {
2165
2027
  let status = "processing";
2166
2028
  let amount;
2167
2029
  let txHash;
2030
+ let asset;
2168
2031
  if (result) {
2169
2032
  status = "success";
2170
2033
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -2173,9 +2036,18 @@ var MarketplaceService = class {
2173
2036
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
2174
2037
  for (const fb of allFeedbacksForReq) {
2175
2038
  const txTag = fb.tags.find((t) => t[0] === "tx");
2176
- if (txTag?.[1]) {
2039
+ if (txTag?.[1] && !txHash) {
2177
2040
  txHash = txTag[1];
2178
- break;
2041
+ }
2042
+ if (!asset) {
2043
+ const amtTag = fb.tags.find((t) => t[0] === "amount");
2044
+ const requestJson = amtTag?.[2];
2045
+ if (requestJson) {
2046
+ const parsed = parsePaymentRequest(requestJson);
2047
+ if (parsed.ok && parsed.data.asset) {
2048
+ asset = parsed.data.asset;
2049
+ }
2050
+ }
2179
2051
  }
2180
2052
  }
2181
2053
  if (feedback) {
@@ -2204,6 +2076,7 @@ var MarketplaceService = class {
2204
2076
  resultEventId: result?.id,
2205
2077
  amount,
2206
2078
  txHash,
2079
+ asset,
2207
2080
  createdAt: req.created_at
2208
2081
  });
2209
2082
  }
@@ -2814,7 +2687,7 @@ var ElisymClient = class {
2814
2687
  payment;
2815
2688
  constructor(config = {}) {
2816
2689
  this.pool = new NostrPool(config.relays ?? RELAYS);
2817
- this.discovery = new DiscoveryService(this.pool, config.rpc);
2690
+ this.discovery = new DiscoveryService(this.pool);
2818
2691
  this.marketplace = new MarketplaceService(this.pool);
2819
2692
  this.ping = new PingService(this.pool);
2820
2693
  this.media = new MediaService(config.uploadUrl);
@@ -2942,6 +2815,89 @@ function lamportsToSol(lamports) {
2942
2815
  const frac = lamports % LAMPORTS_PER_SOL2;
2943
2816
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2944
2817
  }
2818
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
2819
+ var verifyCache = /* @__PURE__ */ new Map();
2820
+ function clearQuickVerifyCache() {
2821
+ verifyCache.clear();
2822
+ }
2823
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
2824
+ if (!txSignature) {
2825
+ return { verified: false, txSignature: "", reason: "invalid_input" };
2826
+ }
2827
+ if (!expectedRecipient || !kit.isAddress(expectedRecipient)) {
2828
+ return { verified: false, txSignature, reason: "invalid_input" };
2829
+ }
2830
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
2831
+ const cached = verifyCache.get(cacheKey2);
2832
+ if (cached) {
2833
+ if (cached.result.verified) {
2834
+ return cached.result;
2835
+ }
2836
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
2837
+ return cached.result;
2838
+ }
2839
+ }
2840
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
2841
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
2842
+ return result;
2843
+ }
2844
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2845
+ const sigStr = txSignature;
2846
+ if (!rpc || typeof rpc.getTransaction !== "function") {
2847
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2848
+ }
2849
+ let tx;
2850
+ try {
2851
+ tx = await rpc.getTransaction(txSignature, {
2852
+ commitment: "confirmed",
2853
+ encoding: "json",
2854
+ maxSupportedTransactionVersion: 0
2855
+ }).send();
2856
+ } catch {
2857
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2858
+ }
2859
+ if (!tx) {
2860
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
2861
+ }
2862
+ if (!tx.meta || tx.meta.err) {
2863
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
2864
+ }
2865
+ const accountKeys = tx.transaction.message.accountKeys;
2866
+ const recipientStr = expectedRecipient;
2867
+ const recipientIdx = accountKeys.indexOf(recipientStr);
2868
+ if (recipientIdx !== -1) {
2869
+ const preBalances = tx.meta.preBalances;
2870
+ const postBalances = tx.meta.postBalances;
2871
+ if (preBalances && postBalances) {
2872
+ const pre = preBalances[recipientIdx];
2873
+ const post = postBalances[recipientIdx];
2874
+ if (pre !== void 0 && post !== void 0) {
2875
+ const delta = BigInt(post) - BigInt(pre);
2876
+ if (delta > 0n) {
2877
+ return { verified: true, txSignature: sigStr };
2878
+ }
2879
+ }
2880
+ }
2881
+ }
2882
+ const postTokenBalances = tx.meta.postTokenBalances;
2883
+ const preTokenBalances = tx.meta.preTokenBalances;
2884
+ if (postTokenBalances) {
2885
+ for (const post of postTokenBalances) {
2886
+ if (post.owner !== recipientStr) {
2887
+ continue;
2888
+ }
2889
+ const pre = preTokenBalances?.find(
2890
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
2891
+ );
2892
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
2893
+ const postAmount = BigInt(post.uiTokenAmount.amount);
2894
+ if (postAmount > preAmount) {
2895
+ return { verified: true, txSignature: sigStr };
2896
+ }
2897
+ }
2898
+ }
2899
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2900
+ }
2945
2901
  var SessionSpendLimitEntrySchema = zod.z.object({
2946
2902
  chain: zod.z.enum(["solana"]),
2947
2903
  token: zod.z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -2952,7 +2908,7 @@ var GlobalConfigSchema = zod.z.object({
2952
2908
  session_spend_limits: zod.z.array(SessionSpendLimitEntrySchema).max(16).optional()
2953
2909
  }).strict();
2954
2910
  function formatSol(lamports) {
2955
- const sol = new Decimal2__default.default(lamports).div(LAMPORTS_PER_SOL);
2911
+ const sol = new Decimal3__default.default(lamports).div(LAMPORTS_PER_SOL);
2956
2912
  if (sol.gte(1e6)) {
2957
2913
  return `${sol.idiv(1e6)}m SOL`;
2958
2914
  }
@@ -2966,12 +2922,12 @@ function compactSol(sol) {
2966
2922
  return "0";
2967
2923
  }
2968
2924
  if (sol.gte(1e3)) {
2969
- return sol.toDecimalPlaces(0, Decimal2__default.default.ROUND_FLOOR).toString();
2925
+ return sol.toDecimalPlaces(0, Decimal3__default.default.ROUND_FLOOR).toString();
2970
2926
  }
2971
2927
  const maxFrac = 9;
2972
2928
  for (let d = 1; d <= maxFrac; d++) {
2973
2929
  const s = sol.toFixed(d);
2974
- if (new Decimal2__default.default(s).eq(sol)) {
2930
+ if (new Decimal3__default.default(s).eq(sol)) {
2975
2931
  return s.replace(/0+$/, "").replace(/\.$/, "");
2976
2932
  }
2977
2933
  }