@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 +114 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -16
- package/dist/index.d.ts +21 -16
- package/dist/index.js +114 -150
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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` (
|
|
1357
|
-
*
|
|
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
|
-
*
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
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
|
|
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
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
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
|
|
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"),
|