@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/{assets-CMf-v55Z.d.cts → assets-C-nzSYD4.d.cts} +5 -1
- package/dist/{assets-CMf-v55Z.d.ts → assets-C-nzSYD4.d.ts} +5 -1
- package/dist/index.cjs +123 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -18
- package/dist/index.d.ts +23 -18
- package/dist/index.js +122 -166
- package/dist/index.js.map +1 -1
- package/dist/skills.cjs +3 -2
- package/dist/skills.cjs.map +1 -1
- package/dist/skills.d.cts +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -2
- package/dist/skills.js.map +1 -1
- package/package.json +1 -1
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-
|
|
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-
|
|
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
|
-
|
|
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
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
|
238
|
-
|
|
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
|
|
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
|
|
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
|
|
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` (
|
|
1332
|
-
*
|
|
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
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
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
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
2905
|
+
if (new Decimal3(s).eq(sol)) {
|
|
2950
2906
|
return s.replace(/0+$/, "").replace(/\.$/, "");
|
|
2951
2907
|
}
|
|
2952
2908
|
}
|