@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.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
|
-
|
|
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
|
|
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` (
|
|
378
|
-
*
|
|
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
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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` (
|
|
378
|
-
*
|
|
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
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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` (
|
|
1332
|
-
*
|
|
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
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
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
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
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
|
|
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"),
|