@elisym/sdk 0.8.0 → 0.10.0
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/README.md +31 -10
- package/dist/agent-store.cjs +1 -2
- package/dist/agent-store.cjs.map +1 -1
- package/dist/agent-store.d.cts +19 -16
- package/dist/agent-store.d.ts +19 -16
- package/dist/agent-store.js +1 -2
- package/dist/agent-store.js.map +1 -1
- package/dist/assets-CMf-v55Z.d.cts +58 -0
- package/dist/assets-CMf-v55Z.d.ts +58 -0
- package/dist/global-schema-CddHP2nk.d.cts +62 -0
- package/dist/global-schema-CddHP2nk.d.ts +62 -0
- package/dist/index.cjs +642 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -117
- package/dist/index.d.ts +214 -117
- package/dist/index.js +636 -168
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +60 -0
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +21 -1
- package/dist/node.d.ts +21 -1
- package/dist/node.js +55 -1
- package/dist/node.js.map +1 -1
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +14 -0
- package/dist/runtime.d.ts +14 -0
- package/dist/runtime.js.map +1 -1
- package/dist/skills.cjs +123 -10
- package/dist/skills.cjs.map +1 -1
- package/dist/skills.d.cts +17 -4
- package/dist/skills.d.ts +17 -4
- package/dist/skills.js +123 -10
- package/dist/skills.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { getTransferSolInstruction } from '@solana-program/system';
|
|
2
|
-
import {
|
|
2
|
+
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
|
|
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';
|
|
3
4
|
import Decimal2 from 'decimal.js-light';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { verifyEvent, finalizeEvent, getPublicKey, nip19, generateSecretKey, SimplePool } from 'nostr-tools';
|
|
6
7
|
import * as nip44 from 'nostr-tools/nip44';
|
|
7
|
-
import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
8
|
-
import YAML2 from 'yaml';
|
|
9
|
-
import { randomBytes } from 'node:crypto';
|
|
10
|
-
import { dirname } from 'node:path';
|
|
11
8
|
|
|
12
9
|
// src/constants.ts
|
|
13
10
|
var RELAYS = [
|
|
@@ -53,7 +50,7 @@ function getProtocolProgramId(cluster) {
|
|
|
53
50
|
}
|
|
54
51
|
var DEFAULTS = {
|
|
55
52
|
SUBSCRIPTION_TIMEOUT_MS: 12e4,
|
|
56
|
-
PING_TIMEOUT_MS:
|
|
53
|
+
PING_TIMEOUT_MS: 3e3,
|
|
57
54
|
PING_RETRIES: 2,
|
|
58
55
|
PING_CACHE_TTL_MS: 3e4,
|
|
59
56
|
PAYMENT_EXPIRY_SECS: 600,
|
|
@@ -97,13 +94,13 @@ function decodeConfig(encodedAccount) {
|
|
|
97
94
|
getConfigDecoder()
|
|
98
95
|
);
|
|
99
96
|
}
|
|
100
|
-
async function fetchConfig(rpc,
|
|
101
|
-
const maybeAccount = await fetchMaybeConfig(rpc,
|
|
97
|
+
async function fetchConfig(rpc, address3, config) {
|
|
98
|
+
const maybeAccount = await fetchMaybeConfig(rpc, address3, config);
|
|
102
99
|
assertAccountExists(maybeAccount);
|
|
103
100
|
return maybeAccount;
|
|
104
101
|
}
|
|
105
|
-
async function fetchMaybeConfig(rpc,
|
|
106
|
-
const maybeAccount = await fetchEncodedAccount(rpc,
|
|
102
|
+
async function fetchMaybeConfig(rpc, address3, config) {
|
|
103
|
+
const maybeAccount = await fetchEncodedAccount(rpc, address3, config);
|
|
107
104
|
return decodeConfig(maybeAccount);
|
|
108
105
|
}
|
|
109
106
|
if (process.env.NODE_ENV !== "production") ;
|
|
@@ -154,6 +151,99 @@ async function getProtocolConfig(rpc, programId, options) {
|
|
|
154
151
|
);
|
|
155
152
|
}
|
|
156
153
|
}
|
|
154
|
+
|
|
155
|
+
// src/payment/assets.ts
|
|
156
|
+
var NATIVE_SOL = {
|
|
157
|
+
chain: "solana",
|
|
158
|
+
token: "sol",
|
|
159
|
+
decimals: 9,
|
|
160
|
+
symbol: "SOL"
|
|
161
|
+
};
|
|
162
|
+
var USDC_SOLANA_DEVNET = {
|
|
163
|
+
chain: "solana",
|
|
164
|
+
token: "usdc",
|
|
165
|
+
mint: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
166
|
+
decimals: 6,
|
|
167
|
+
symbol: "USDC"
|
|
168
|
+
};
|
|
169
|
+
var KNOWN_ASSETS = [NATIVE_SOL, USDC_SOLANA_DEVNET];
|
|
170
|
+
function assetKey(a) {
|
|
171
|
+
return a.mint ? `${a.chain}:${a.token}:${a.mint}` : `${a.chain}:${a.token}`;
|
|
172
|
+
}
|
|
173
|
+
function resolveKnownAsset(chain, token, mint) {
|
|
174
|
+
const key = mint ? `${chain}:${token}:${mint}` : `${chain}:${token}`;
|
|
175
|
+
return KNOWN_ASSETS.find((asset) => assetKey(asset) === key);
|
|
176
|
+
}
|
|
177
|
+
function assetByKey(key) {
|
|
178
|
+
return KNOWN_ASSETS.find((asset) => assetKey(asset) === key);
|
|
179
|
+
}
|
|
180
|
+
function resolveAssetFromPaymentRequest(request) {
|
|
181
|
+
if (!request.asset) {
|
|
182
|
+
return NATIVE_SOL;
|
|
183
|
+
}
|
|
184
|
+
const found = resolveKnownAsset(request.asset.chain, request.asset.token, request.asset.mint);
|
|
185
|
+
if (!found) {
|
|
186
|
+
const display = request.asset.mint ? `${request.asset.chain}:${request.asset.token}:${request.asset.mint}` : `${request.asset.chain}:${request.asset.token}`;
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Unknown asset in payment request: ${display}. Known assets: ${KNOWN_ASSETS.map(assetKey).join(", ")}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
var DECIMAL_RE = /^(\d+\.\d*|\d*\.\d+|\d+)$/;
|
|
194
|
+
function parseAssetAmount(asset, human) {
|
|
195
|
+
const trimmed = human.trim();
|
|
196
|
+
if (!trimmed) {
|
|
197
|
+
throw new Error(`${asset.symbol} amount is empty`);
|
|
198
|
+
}
|
|
199
|
+
if (trimmed.startsWith("-")) {
|
|
200
|
+
throw new Error(`${asset.symbol} amount cannot be negative`);
|
|
201
|
+
}
|
|
202
|
+
if (!DECIMAL_RE.test(trimmed)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`${asset.symbol} amount must be a non-negative decimal (e.g. "0.5", "1"); got "${human}"`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const dotPos = trimmed.indexOf(".");
|
|
208
|
+
let wholePart;
|
|
209
|
+
if (dotPos === -1) {
|
|
210
|
+
wholePart = trimmed;
|
|
211
|
+
} else if (dotPos === 0) {
|
|
212
|
+
wholePart = "0";
|
|
213
|
+
} else {
|
|
214
|
+
wholePart = trimmed.slice(0, dotPos);
|
|
215
|
+
}
|
|
216
|
+
const fracPart = dotPos === -1 ? "" : trimmed.slice(dotPos + 1);
|
|
217
|
+
if (fracPart.length > asset.decimals) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`${asset.symbol} amount has too many decimals (max ${asset.decimals}); got "${human}"`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const unit = 10n ** BigInt(asset.decimals);
|
|
223
|
+
const whole = BigInt(wholePart);
|
|
224
|
+
const frac = fracPart ? BigInt(fracPart.padEnd(asset.decimals, "0")) : 0n;
|
|
225
|
+
const raw = whole * unit + frac;
|
|
226
|
+
if (raw === 0n) {
|
|
227
|
+
throw new Error(`${asset.symbol} amount must be positive; got "${human}"`);
|
|
228
|
+
}
|
|
229
|
+
if (raw > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`${asset.symbol} amount exceeds safe range (max ${Number.MAX_SAFE_INTEGER} subunits)`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return raw;
|
|
235
|
+
}
|
|
236
|
+
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}`;
|
|
246
|
+
}
|
|
157
247
|
var BPS_DENOMINATOR = 1e4;
|
|
158
248
|
function assertLamports(value, field) {
|
|
159
249
|
if (!Number.isInteger(value) || value < 0) {
|
|
@@ -263,6 +353,12 @@ var SOLANA_ADDRESS_LENGTH_RE = /^.{32,44}$/;
|
|
|
263
353
|
var lamportsSchema = z.number().int().positive().max(MAX_SAFE_LAMPORTS, `amount must be <= ${MAX_SAFE_LAMPORTS}`);
|
|
264
354
|
var feeAmountSchema = z.number().int().nonnegative().max(MAX_SAFE_LAMPORTS, `fee_amount must be <= ${MAX_SAFE_LAMPORTS}`);
|
|
265
355
|
var solanaAddressSchema = z.string().regex(BASE58_RE, "must be base58").regex(SOLANA_ADDRESS_LENGTH_RE, "must be 32-44 base58 chars");
|
|
356
|
+
var paymentAssetRefSchema = z.object({
|
|
357
|
+
chain: z.string().min(1),
|
|
358
|
+
token: z.string().min(1),
|
|
359
|
+
mint: solanaAddressSchema.optional(),
|
|
360
|
+
decimals: z.number().int().min(0).max(18)
|
|
361
|
+
});
|
|
266
362
|
var PaymentRequestSchema = z.object({
|
|
267
363
|
recipient: solanaAddressSchema,
|
|
268
364
|
amount: lamportsSchema,
|
|
@@ -271,7 +367,8 @@ var PaymentRequestSchema = z.object({
|
|
|
271
367
|
fee_address: solanaAddressSchema.optional(),
|
|
272
368
|
fee_amount: feeAmountSchema.optional(),
|
|
273
369
|
created_at: z.number().int().positive(),
|
|
274
|
-
expiry_secs: z.number().int().positive().max(MAX_EXPIRY_SECS_SCHEMA, `expiry_secs must be <= ${MAX_EXPIRY_SECS_SCHEMA}`)
|
|
370
|
+
expiry_secs: z.number().int().positive().max(MAX_EXPIRY_SECS_SCHEMA, `expiry_secs must be <= ${MAX_EXPIRY_SECS_SCHEMA}`),
|
|
371
|
+
asset: paymentAssetRefSchema.optional()
|
|
275
372
|
});
|
|
276
373
|
function parsePaymentRequest(input, options) {
|
|
277
374
|
let parsed;
|
|
@@ -353,6 +450,12 @@ var SolanaPaymentStrategy = class {
|
|
|
353
450
|
assertExpirySecs(expirySecs);
|
|
354
451
|
const feeAmount = calculateProtocolFee(amount, config.feeBps);
|
|
355
452
|
const reference = generateReference();
|
|
453
|
+
const assetRef = options?.asset && options.asset !== NATIVE_SOL ? {
|
|
454
|
+
chain: options.asset.chain,
|
|
455
|
+
token: options.asset.token,
|
|
456
|
+
mint: options.asset.mint,
|
|
457
|
+
decimals: options.asset.decimals
|
|
458
|
+
} : void 0;
|
|
356
459
|
return {
|
|
357
460
|
recipient: recipientAddress,
|
|
358
461
|
amount,
|
|
@@ -360,7 +463,8 @@ var SolanaPaymentStrategy = class {
|
|
|
360
463
|
fee_address: config.treasury,
|
|
361
464
|
fee_amount: feeAmount,
|
|
362
465
|
created_at: Math.floor(Date.now() / 1e3),
|
|
363
|
-
expiry_secs: expirySecs
|
|
466
|
+
expiry_secs: expirySecs,
|
|
467
|
+
...assetRef ? { asset: assetRef } : {}
|
|
364
468
|
};
|
|
365
469
|
}
|
|
366
470
|
validatePaymentRequest(requestJson, config, expectedRecipient, options) {
|
|
@@ -378,6 +482,16 @@ var SolanaPaymentStrategy = class {
|
|
|
378
482
|
return { code: "invalid_amount", message: parsed.error.message };
|
|
379
483
|
}
|
|
380
484
|
const data = parsed.data;
|
|
485
|
+
if (data.asset) {
|
|
486
|
+
try {
|
|
487
|
+
resolveAssetFromPaymentRequest(data);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return {
|
|
490
|
+
code: "invalid_asset",
|
|
491
|
+
message: error instanceof Error ? error.message : String(error)
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
381
495
|
if (!isValidSolanaAddress(data.recipient)) {
|
|
382
496
|
return {
|
|
383
497
|
code: "invalid_recipient_address",
|
|
@@ -466,7 +580,7 @@ var SolanaPaymentStrategy = class {
|
|
|
466
580
|
if (!Number.isInteger(computeUnitLimit) || computeUnitLimit <= 0) {
|
|
467
581
|
throw new Error(`Invalid computeUnitLimit: ${computeUnitLimit}. Must be a positive integer.`);
|
|
468
582
|
}
|
|
469
|
-
const paymentInstructions = buildPaymentInstructions(paymentRequest, payerSigner);
|
|
583
|
+
const paymentInstructions = await buildPaymentInstructions(paymentRequest, payerSigner);
|
|
470
584
|
const priorityFeeMicroLamports = options?.priorityFeeMicroLamports ?? await estimatePriorityFeeMicroLamports(rpc, {
|
|
471
585
|
percentile: options?.priorityFeePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE
|
|
472
586
|
});
|
|
@@ -528,6 +642,16 @@ var SolanaPaymentStrategy = class {
|
|
|
528
642
|
error: `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount})`
|
|
529
643
|
};
|
|
530
644
|
}
|
|
645
|
+
let asset;
|
|
646
|
+
try {
|
|
647
|
+
asset = resolveAssetFromPaymentRequest(paymentRequest);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
return {
|
|
650
|
+
verified: false,
|
|
651
|
+
error: error instanceof Error ? error.message : String(error)
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
const mint = asset.mint;
|
|
531
655
|
if (options?.txSignature) {
|
|
532
656
|
return this._verifyBySignature(
|
|
533
657
|
rpc,
|
|
@@ -537,6 +661,7 @@ var SolanaPaymentStrategy = class {
|
|
|
537
661
|
treasury,
|
|
538
662
|
expectedNet,
|
|
539
663
|
feeAmount,
|
|
664
|
+
mint,
|
|
540
665
|
options?.retries ?? DEFAULTS.VERIFY_RETRIES,
|
|
541
666
|
options?.intervalMs ?? DEFAULTS.VERIFY_INTERVAL_MS
|
|
542
667
|
);
|
|
@@ -548,11 +673,12 @@ var SolanaPaymentStrategy = class {
|
|
|
548
673
|
treasury,
|
|
549
674
|
expectedNet,
|
|
550
675
|
feeAmount,
|
|
676
|
+
mint,
|
|
551
677
|
options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
|
|
552
678
|
options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
|
|
553
679
|
);
|
|
554
680
|
}
|
|
555
|
-
async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
681
|
+
async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, mint, retries, intervalMs) {
|
|
556
682
|
let lastError;
|
|
557
683
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
558
684
|
try {
|
|
@@ -571,15 +697,18 @@ var SolanaPaymentStrategy = class {
|
|
|
571
697
|
error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
|
|
572
698
|
};
|
|
573
699
|
}
|
|
574
|
-
const verdict =
|
|
700
|
+
const verdict = checkTxDiff({
|
|
575
701
|
accountKeys: tx.transaction.message.accountKeys,
|
|
576
702
|
preBalances: tx.meta.preBalances,
|
|
577
703
|
postBalances: tx.meta.postBalances,
|
|
704
|
+
preTokenBalances: tx.meta.preTokenBalances,
|
|
705
|
+
postTokenBalances: tx.meta.postTokenBalances,
|
|
578
706
|
referenceKey,
|
|
579
707
|
recipientAddress,
|
|
580
708
|
treasuryAddress,
|
|
581
709
|
expectedNet,
|
|
582
|
-
expectedFee
|
|
710
|
+
expectedFee,
|
|
711
|
+
mint
|
|
583
712
|
});
|
|
584
713
|
if (verdict.ok) {
|
|
585
714
|
return { verified: true, txSignature };
|
|
@@ -597,7 +726,7 @@ var SolanaPaymentStrategy = class {
|
|
|
597
726
|
error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
|
|
598
727
|
};
|
|
599
728
|
}
|
|
600
|
-
async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
729
|
+
async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, mint, retries, intervalMs) {
|
|
601
730
|
let lastError;
|
|
602
731
|
const reference = address(referenceKey);
|
|
603
732
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
@@ -621,15 +750,18 @@ var SolanaPaymentStrategy = class {
|
|
|
621
750
|
if (!tx?.meta || tx.meta.err) {
|
|
622
751
|
continue;
|
|
623
752
|
}
|
|
624
|
-
const verdict =
|
|
753
|
+
const verdict = checkTxDiff({
|
|
625
754
|
accountKeys: tx.transaction.message.accountKeys,
|
|
626
755
|
preBalances: tx.meta.preBalances,
|
|
627
756
|
postBalances: tx.meta.postBalances,
|
|
757
|
+
preTokenBalances: tx.meta.preTokenBalances,
|
|
758
|
+
postTokenBalances: tx.meta.postTokenBalances,
|
|
628
759
|
referenceKey,
|
|
629
760
|
recipientAddress,
|
|
630
761
|
treasuryAddress,
|
|
631
762
|
expectedNet,
|
|
632
|
-
expectedFee
|
|
763
|
+
expectedFee,
|
|
764
|
+
mint
|
|
633
765
|
});
|
|
634
766
|
if (verdict.ok) {
|
|
635
767
|
return { verified: true, txSignature: sig };
|
|
@@ -649,7 +781,7 @@ var SolanaPaymentStrategy = class {
|
|
|
649
781
|
};
|
|
650
782
|
}
|
|
651
783
|
};
|
|
652
|
-
function
|
|
784
|
+
function checkTxDiff(input) {
|
|
653
785
|
const balanceCount = input.preBalances.length;
|
|
654
786
|
const keyToIdx = /* @__PURE__ */ new Map();
|
|
655
787
|
for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
|
|
@@ -661,6 +793,9 @@ function checkBalanceDiff(input) {
|
|
|
661
793
|
if (!keyToIdx.has(input.referenceKey)) {
|
|
662
794
|
return { ok: false, reason: "Reference key not found in transaction - possible replay" };
|
|
663
795
|
}
|
|
796
|
+
if (input.mint) {
|
|
797
|
+
return checkTokenBalanceDiff(input);
|
|
798
|
+
}
|
|
664
799
|
const recipientIdx = keyToIdx.get(input.recipientAddress);
|
|
665
800
|
if (recipientIdx === void 0) {
|
|
666
801
|
return { ok: false, reason: "Recipient not found in transaction" };
|
|
@@ -693,6 +828,47 @@ function checkBalanceDiff(input) {
|
|
|
693
828
|
}
|
|
694
829
|
return { ok: true };
|
|
695
830
|
}
|
|
831
|
+
function checkTokenBalanceDiff(input) {
|
|
832
|
+
const mint = input.mint;
|
|
833
|
+
if (!mint) {
|
|
834
|
+
return { ok: false, reason: "Expected mint for SPL verification, got none" };
|
|
835
|
+
}
|
|
836
|
+
const pre = input.preTokenBalances ?? [];
|
|
837
|
+
const post = input.postTokenBalances ?? [];
|
|
838
|
+
const tokenDelta = (ownerAddress) => {
|
|
839
|
+
const preEntry = pre.find((entry) => entry.owner === ownerAddress && entry.mint === mint);
|
|
840
|
+
const postEntry = post.find((entry) => entry.owner === ownerAddress && entry.mint === mint);
|
|
841
|
+
if (!postEntry) {
|
|
842
|
+
return -1n;
|
|
843
|
+
}
|
|
844
|
+
const preAmount = preEntry ? BigInt(preEntry.uiTokenAmount.amount) : 0n;
|
|
845
|
+
const postAmount = BigInt(postEntry.uiTokenAmount.amount);
|
|
846
|
+
return postAmount - preAmount;
|
|
847
|
+
};
|
|
848
|
+
const recipientDelta = tokenDelta(input.recipientAddress);
|
|
849
|
+
if (recipientDelta === -1n) {
|
|
850
|
+
return { ok: false, reason: "Recipient token account not found in transaction" };
|
|
851
|
+
}
|
|
852
|
+
if (recipientDelta < BigInt(input.expectedNet)) {
|
|
853
|
+
return {
|
|
854
|
+
ok: false,
|
|
855
|
+
reason: `Recipient received ${recipientDelta.toString()} tokens, expected >= ${input.expectedNet}`
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (input.expectedFee > 0) {
|
|
859
|
+
const treasuryDelta = tokenDelta(input.treasuryAddress);
|
|
860
|
+
if (treasuryDelta === -1n) {
|
|
861
|
+
return { ok: false, reason: "Treasury token account not found in transaction" };
|
|
862
|
+
}
|
|
863
|
+
if (treasuryDelta < BigInt(input.expectedFee)) {
|
|
864
|
+
return {
|
|
865
|
+
ok: false,
|
|
866
|
+
reason: `Treasury received ${treasuryDelta.toString()} tokens, expected >= ${input.expectedFee}`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { ok: true };
|
|
871
|
+
}
|
|
696
872
|
function bigIntDelta(post, pre) {
|
|
697
873
|
const postValue = post === void 0 ? 0n : BigInt(post);
|
|
698
874
|
const preValue = pre === void 0 ? 0n : BigInt(pre);
|
|
@@ -701,7 +877,7 @@ function bigIntDelta(post, pre) {
|
|
|
701
877
|
function waitMs(ms) {
|
|
702
878
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
703
879
|
}
|
|
704
|
-
function buildPaymentInstructions(paymentRequest, payerSigner) {
|
|
880
|
+
async function buildPaymentInstructions(paymentRequest, payerSigner) {
|
|
705
881
|
const recipient = address(paymentRequest.recipient);
|
|
706
882
|
const reference = address(paymentRequest.reference);
|
|
707
883
|
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
@@ -711,22 +887,98 @@ function buildPaymentInstructions(paymentRequest, payerSigner) {
|
|
|
711
887
|
`Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
|
|
712
888
|
);
|
|
713
889
|
}
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
890
|
+
const asset = resolveAssetFromPaymentRequest(paymentRequest);
|
|
891
|
+
if (!asset.mint) {
|
|
892
|
+
const providerTransferIx2 = getTransferSolInstruction({
|
|
893
|
+
source: payerSigner,
|
|
894
|
+
destination: recipient,
|
|
895
|
+
amount: BigInt(providerAmount)
|
|
896
|
+
});
|
|
897
|
+
const providerTransferIxWithReference2 = {
|
|
898
|
+
...providerTransferIx2,
|
|
899
|
+
accounts: [
|
|
900
|
+
...providerTransferIx2.accounts,
|
|
901
|
+
{ address: reference, role: AccountRole.READONLY }
|
|
902
|
+
]
|
|
903
|
+
};
|
|
904
|
+
const instructions2 = [providerTransferIxWithReference2];
|
|
905
|
+
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
906
|
+
instructions2.push(
|
|
907
|
+
getTransferSolInstruction({
|
|
908
|
+
source: payerSigner,
|
|
909
|
+
destination: address(paymentRequest.fee_address),
|
|
910
|
+
amount: BigInt(feeAmount)
|
|
911
|
+
})
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return instructions2;
|
|
915
|
+
}
|
|
916
|
+
const mint = address(asset.mint);
|
|
917
|
+
const payerAddress = payerSigner.address;
|
|
918
|
+
const [payerAta] = await findAssociatedTokenPda({
|
|
919
|
+
owner: payerAddress,
|
|
920
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
921
|
+
mint
|
|
922
|
+
});
|
|
923
|
+
const [recipientAta] = await findAssociatedTokenPda({
|
|
924
|
+
owner: recipient,
|
|
925
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
926
|
+
mint
|
|
927
|
+
});
|
|
928
|
+
const instructions = [];
|
|
929
|
+
instructions.push(
|
|
930
|
+
getCreateAssociatedTokenIdempotentInstruction(
|
|
931
|
+
{
|
|
932
|
+
payer: payerSigner,
|
|
933
|
+
ata: recipientAta,
|
|
934
|
+
owner: recipient,
|
|
935
|
+
mint
|
|
936
|
+
},
|
|
937
|
+
{ programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS }
|
|
938
|
+
)
|
|
939
|
+
);
|
|
940
|
+
let treasuryAta;
|
|
941
|
+
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
942
|
+
const treasuryOwner = address(paymentRequest.fee_address);
|
|
943
|
+
[treasuryAta] = await findAssociatedTokenPda({
|
|
944
|
+
owner: treasuryOwner,
|
|
945
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
946
|
+
mint
|
|
947
|
+
});
|
|
948
|
+
instructions.push(
|
|
949
|
+
getCreateAssociatedTokenIdempotentInstruction(
|
|
950
|
+
{
|
|
951
|
+
payer: payerSigner,
|
|
952
|
+
ata: treasuryAta,
|
|
953
|
+
owner: treasuryOwner,
|
|
954
|
+
mint
|
|
955
|
+
},
|
|
956
|
+
{ programAddress: ASSOCIATED_TOKEN_PROGRAM_ADDRESS }
|
|
957
|
+
)
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const providerTransferIx = getTransferCheckedInstruction({
|
|
961
|
+
source: payerAta,
|
|
962
|
+
mint,
|
|
963
|
+
destination: recipientAta,
|
|
964
|
+
authority: payerSigner,
|
|
965
|
+
amount: BigInt(providerAmount),
|
|
966
|
+
decimals: asset.decimals
|
|
718
967
|
});
|
|
719
968
|
const providerTransferIxWithReference = {
|
|
720
969
|
...providerTransferIx,
|
|
721
970
|
accounts: [...providerTransferIx.accounts, { address: reference, role: AccountRole.READONLY }]
|
|
722
971
|
};
|
|
723
|
-
|
|
724
|
-
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
972
|
+
instructions.push(providerTransferIxWithReference);
|
|
973
|
+
if (treasuryAta && paymentRequest.fee_address && feeAmount > 0) {
|
|
725
974
|
instructions.push(
|
|
726
|
-
|
|
727
|
-
source:
|
|
728
|
-
|
|
729
|
-
|
|
975
|
+
getTransferCheckedInstruction({
|
|
976
|
+
source: payerAta,
|
|
977
|
+
mint,
|
|
978
|
+
destination: treasuryAta,
|
|
979
|
+
authority: payerSigner,
|
|
980
|
+
amount: BigInt(feeAmount),
|
|
981
|
+
decimals: asset.decimals
|
|
730
982
|
})
|
|
731
983
|
);
|
|
732
984
|
}
|
|
@@ -742,6 +994,95 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
|
|
|
742
994
|
options
|
|
743
995
|
);
|
|
744
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
|
+
var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
|
|
1083
|
+
var RANKING_BUCKET_SIZE_SECS = 60;
|
|
1084
|
+
var COLD_START_BUCKET = -Infinity;
|
|
1085
|
+
var MAX_PAID_CANDIDATES_PER_AGENT = 5;
|
|
745
1086
|
function toDTag(name) {
|
|
746
1087
|
const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
|
|
747
1088
|
if (!tag) {
|
|
@@ -749,6 +1090,28 @@ function toDTag(name) {
|
|
|
749
1090
|
}
|
|
750
1091
|
return tag;
|
|
751
1092
|
}
|
|
1093
|
+
function computeRankKey(agent) {
|
|
1094
|
+
const lastPaidJobAt = agent.lastPaidJobAt ?? 0;
|
|
1095
|
+
const total = agent.totalRatingCount ?? 0;
|
|
1096
|
+
const positive = agent.positiveCount ?? 0;
|
|
1097
|
+
const rate = total > 0 ? positive / total : 0;
|
|
1098
|
+
const bucket = lastPaidJobAt > 0 ? Math.floor(lastPaidJobAt / RANKING_BUCKET_SIZE_SECS) * RANKING_BUCKET_SIZE_SECS : COLD_START_BUCKET;
|
|
1099
|
+
return { bucket, rate, lastPaidJobAt, lastSeen: agent.lastSeen };
|
|
1100
|
+
}
|
|
1101
|
+
function compareAgentsByRank(a, b) {
|
|
1102
|
+
const ka = computeRankKey(a);
|
|
1103
|
+
const kb = computeRankKey(b);
|
|
1104
|
+
if (kb.bucket !== ka.bucket) {
|
|
1105
|
+
return kb.bucket - ka.bucket;
|
|
1106
|
+
}
|
|
1107
|
+
if (kb.rate !== ka.rate) {
|
|
1108
|
+
return kb.rate - ka.rate;
|
|
1109
|
+
}
|
|
1110
|
+
if (kb.lastPaidJobAt !== ka.lastPaidJobAt) {
|
|
1111
|
+
return kb.lastPaidJobAt - ka.lastPaidJobAt;
|
|
1112
|
+
}
|
|
1113
|
+
return kb.lastSeen - ka.lastSeen;
|
|
1114
|
+
}
|
|
752
1115
|
function buildAgentsFromEvents(events, network) {
|
|
753
1116
|
const latestByDTag = /* @__PURE__ */ new Map();
|
|
754
1117
|
for (const event of events) {
|
|
@@ -843,9 +1206,37 @@ function buildAgentsFromEvents(events, network) {
|
|
|
843
1206
|
}
|
|
844
1207
|
return agentMap;
|
|
845
1208
|
}
|
|
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
|
+
}
|
|
846
1232
|
var DiscoveryService = class {
|
|
847
|
-
constructor(pool) {
|
|
1233
|
+
constructor(pool, defaultRpc) {
|
|
848
1234
|
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;
|
|
849
1240
|
}
|
|
850
1241
|
/** Count elisym agents (kind:31990 with "elisym" tag). */
|
|
851
1242
|
async fetchAllAgentCount() {
|
|
@@ -933,8 +1324,21 @@ var DiscoveryService = class {
|
|
|
933
1324
|
}
|
|
934
1325
|
return agents;
|
|
935
1326
|
}
|
|
936
|
-
/**
|
|
937
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* Fetch elisym agents filtered by network, ranked by verified paid-job recency.
|
|
1329
|
+
*
|
|
1330
|
+
* 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
|
|
1333
|
+
* sentinel bucket below all populated buckets.
|
|
1334
|
+
* 2. Within a bucket, sort by positive review rate descending.
|
|
1335
|
+
* 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
|
|
1336
|
+
*
|
|
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.
|
|
1340
|
+
*/
|
|
1341
|
+
async fetchAgents(network = "devnet", limit, rpcOverride) {
|
|
938
1342
|
const filter = {
|
|
939
1343
|
kinds: [KIND_APP_HANDLER],
|
|
940
1344
|
"#t": ["elisym"]
|
|
@@ -944,40 +1348,100 @@ var DiscoveryService = class {
|
|
|
944
1348
|
}
|
|
945
1349
|
const events = await this.pool.querySync(filter);
|
|
946
1350
|
const agentMap = buildAgentsFromEvents(events, network);
|
|
947
|
-
const agents = Array.from(agentMap.values())
|
|
1351
|
+
const agents = Array.from(agentMap.values());
|
|
948
1352
|
const agentPubkeys = Array.from(agentMap.keys());
|
|
949
|
-
if (agentPubkeys.length
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1353
|
+
if (agentPubkeys.length === 0) {
|
|
1354
|
+
return agents;
|
|
1355
|
+
}
|
|
1356
|
+
const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
|
|
1357
|
+
const resultKinds = /* @__PURE__ */ new Set();
|
|
1358
|
+
for (const agent of agentMap.values()) {
|
|
1359
|
+
for (const k of agent.supportedKinds) {
|
|
1360
|
+
if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
|
|
1361
|
+
resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
|
|
957
1362
|
}
|
|
958
1363
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1364
|
+
}
|
|
1365
|
+
resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
|
|
1366
|
+
const [resultEvents, feedbackEvents] = await Promise.all([
|
|
1367
|
+
this.pool.queryBatched(
|
|
1368
|
+
{
|
|
1369
|
+
kinds: [...resultKinds],
|
|
1370
|
+
since: activitySince
|
|
1371
|
+
},
|
|
1372
|
+
agentPubkeys
|
|
1373
|
+
),
|
|
1374
|
+
this.pool.queryBatchedByTag(
|
|
1375
|
+
{ kinds: [KIND_JOB_FEEDBACK], since: activitySince },
|
|
1376
|
+
"p",
|
|
1377
|
+
agentPubkeys
|
|
1378
|
+
),
|
|
1379
|
+
this.enrichWithMetadata(agents)
|
|
1380
|
+
]);
|
|
1381
|
+
for (const ev of resultEvents) {
|
|
1382
|
+
if (!verifyEvent(ev)) {
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
const agent = agentMap.get(ev.pubkey);
|
|
1386
|
+
if (agent && ev.created_at > agent.lastSeen) {
|
|
1387
|
+
agent.lastSeen = ev.created_at;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const paidCandidates = /* @__PURE__ */ new Map();
|
|
1391
|
+
for (const ev of feedbackEvents) {
|
|
1392
|
+
if (!verifyEvent(ev)) {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
|
|
1396
|
+
if (!targetPubkey) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
const agent = agentMap.get(targetPubkey);
|
|
1400
|
+
if (!agent) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
if (ev.created_at > agent.lastSeen) {
|
|
1404
|
+
agent.lastSeen = ev.created_at;
|
|
1405
|
+
}
|
|
1406
|
+
const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
|
|
1407
|
+
if (rating === "1" || rating === "0") {
|
|
1408
|
+
agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
|
|
1409
|
+
if (rating === "1") {
|
|
1410
|
+
agent.positiveCount = (agent.positiveCount ?? 0) + 1;
|
|
973
1411
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1412
|
+
}
|
|
1413
|
+
const status = ev.tags.find((t) => t[0] === "status")?.[1];
|
|
1414
|
+
const txTag = ev.tags.find((t) => t[0] === "tx");
|
|
1415
|
+
const txSignature = txTag?.[1];
|
|
1416
|
+
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]);
|
|
977
1423
|
}
|
|
978
1424
|
}
|
|
979
|
-
agents.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
980
1425
|
}
|
|
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
|
+
agents.sort(compareAgentsByRank);
|
|
981
1445
|
return agents;
|
|
982
1446
|
}
|
|
983
1447
|
/**
|
|
@@ -2325,7 +2789,7 @@ var ElisymClient = class {
|
|
|
2325
2789
|
payment;
|
|
2326
2790
|
constructor(config = {}) {
|
|
2327
2791
|
this.pool = new NostrPool(config.relays ?? RELAYS);
|
|
2328
|
-
this.discovery = new DiscoveryService(this.pool);
|
|
2792
|
+
this.discovery = new DiscoveryService(this.pool, config.rpc);
|
|
2329
2793
|
this.marketplace = new MarketplaceService(this.pool);
|
|
2330
2794
|
this.ping = new PingService(this.pool);
|
|
2331
2795
|
this.media = new MediaService(config.uploadUrl);
|
|
@@ -2335,96 +2799,124 @@ var ElisymClient = class {
|
|
|
2335
2799
|
this.pool.close();
|
|
2336
2800
|
}
|
|
2337
2801
|
};
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
var
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
)
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
}
|
|
2380
|
-
const fracPart = dotPos === -1 ? "" : trimmed.slice(dotPos + 1);
|
|
2381
|
-
if (fracPart.length > asset.decimals) {
|
|
2382
|
-
throw new Error(
|
|
2383
|
-
`${asset.symbol} amount has too many decimals (max ${asset.decimals}); got "${human}"`
|
|
2384
|
-
);
|
|
2385
|
-
}
|
|
2386
|
-
const unit = 10n ** BigInt(asset.decimals);
|
|
2387
|
-
const whole = BigInt(wholePart);
|
|
2388
|
-
const frac = fracPart ? BigInt(fracPart.padEnd(asset.decimals, "0")) : 0n;
|
|
2389
|
-
const raw = whole * unit + frac;
|
|
2390
|
-
if (raw === 0n) {
|
|
2391
|
-
throw new Error(`${asset.symbol} amount must be positive; got "${human}"`);
|
|
2392
|
-
}
|
|
2393
|
-
if (raw > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
2394
|
-
throw new Error(
|
|
2395
|
-
`${asset.symbol} amount exceeds safe range (max ${Number.MAX_SAFE_INTEGER} subunits)`
|
|
2396
|
-
);
|
|
2802
|
+
var DEFAULT_COMPUTE_UNIT_LIMIT2 = 2e5;
|
|
2803
|
+
var DEFAULT_PRIORITY_FEE_PERCENTILE2 = 75;
|
|
2804
|
+
var BASE_FEE_LAMPORTS_PER_SIGNATURE = 5000n;
|
|
2805
|
+
var FALLBACK_ATA_RENT_LAMPORTS = 2039280n;
|
|
2806
|
+
var SPL_TOKEN_ACCOUNT_SIZE = 165;
|
|
2807
|
+
async function estimateSolFeeLamports(rpc, paymentRequest, _payerAddress, options) {
|
|
2808
|
+
const numSignatures = options?.numSignatures ?? 1;
|
|
2809
|
+
const computeUnitLimit = options?.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT2;
|
|
2810
|
+
const priorityFeeMicroLamports = options?.priorityFeeMicroLamports ?? await estimatePriorityFeeMicroLamports(rpc, {
|
|
2811
|
+
percentile: options?.priorityFeePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE2
|
|
2812
|
+
});
|
|
2813
|
+
const baseFeeLamports = BASE_FEE_LAMPORTS_PER_SIGNATURE * BigInt(numSignatures);
|
|
2814
|
+
const priorityFeeLamports = ceilDiv(
|
|
2815
|
+
priorityFeeMicroLamports * BigInt(computeUnitLimit),
|
|
2816
|
+
1000000n
|
|
2817
|
+
);
|
|
2818
|
+
const asset = resolveAssetFromPaymentRequest(paymentRequest);
|
|
2819
|
+
let rentLamports = 0n;
|
|
2820
|
+
let rentPerAtaLamports = 0n;
|
|
2821
|
+
let missingAtaCount = 0;
|
|
2822
|
+
if (asset.mint) {
|
|
2823
|
+
rentPerAtaLamports = await fetchAtaRent(rpc);
|
|
2824
|
+
const mint = address(asset.mint);
|
|
2825
|
+
const ataAccountsToCheck = [];
|
|
2826
|
+
const [recipientAta] = await findAssociatedTokenPda({
|
|
2827
|
+
owner: address(paymentRequest.recipient),
|
|
2828
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
2829
|
+
mint
|
|
2830
|
+
});
|
|
2831
|
+
ataAccountsToCheck.push(recipientAta);
|
|
2832
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
2833
|
+
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
2834
|
+
const [treasuryAta] = await findAssociatedTokenPda({
|
|
2835
|
+
owner: address(paymentRequest.fee_address),
|
|
2836
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
2837
|
+
mint
|
|
2838
|
+
});
|
|
2839
|
+
ataAccountsToCheck.push(treasuryAta);
|
|
2840
|
+
}
|
|
2841
|
+
missingAtaCount = await countMissingAccounts(rpc, ataAccountsToCheck);
|
|
2842
|
+
rentLamports = rentPerAtaLamports * BigInt(missingAtaCount);
|
|
2397
2843
|
}
|
|
2398
|
-
|
|
2844
|
+
const totalLamports = baseFeeLamports + priorityFeeLamports + rentLamports;
|
|
2845
|
+
return {
|
|
2846
|
+
baseFeeLamports,
|
|
2847
|
+
priorityFeeLamports,
|
|
2848
|
+
rentLamports,
|
|
2849
|
+
totalLamports,
|
|
2850
|
+
breakdown: {
|
|
2851
|
+
numSignatures,
|
|
2852
|
+
priorityFeeMicroLamports,
|
|
2853
|
+
computeUnitLimit,
|
|
2854
|
+
rentPerAtaLamports,
|
|
2855
|
+
missingAtaCount
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2399
2858
|
}
|
|
2400
|
-
function
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2859
|
+
async function fetchAtaRent(rpc) {
|
|
2860
|
+
try {
|
|
2861
|
+
const lamports = await rpc.getMinimumBalanceForRentExemption(BigInt(SPL_TOKEN_ACCOUNT_SIZE)).send();
|
|
2862
|
+
if (typeof lamports === "bigint") {
|
|
2863
|
+
return lamports;
|
|
2864
|
+
}
|
|
2865
|
+
if (typeof lamports === "number" && Number.isFinite(lamports) && lamports > 0) {
|
|
2866
|
+
return BigInt(lamports);
|
|
2867
|
+
}
|
|
2868
|
+
return FALLBACK_ATA_RENT_LAMPORTS;
|
|
2869
|
+
} catch {
|
|
2870
|
+
return FALLBACK_ATA_RENT_LAMPORTS;
|
|
2408
2871
|
}
|
|
2409
|
-
return `${sign}${whole}.${frac.toString().padStart(asset.decimals, "0")} ${asset.symbol}`;
|
|
2410
2872
|
}
|
|
2411
|
-
async function
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
} catch (e) {
|
|
2873
|
+
async function countMissingAccounts(rpc, accounts) {
|
|
2874
|
+
if (accounts.length === 0) {
|
|
2875
|
+
return 0;
|
|
2876
|
+
}
|
|
2877
|
+
let missing = 0;
|
|
2878
|
+
for (const acct of accounts) {
|
|
2418
2879
|
try {
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2880
|
+
const res = await rpc.getAccountInfo(acct, { encoding: "base64" }).send();
|
|
2881
|
+
if (!res || !res.value) {
|
|
2882
|
+
missing++;
|
|
2883
|
+
}
|
|
2421
2884
|
} catch {
|
|
2885
|
+
missing++;
|
|
2422
2886
|
}
|
|
2423
|
-
throw e;
|
|
2424
2887
|
}
|
|
2888
|
+
return missing;
|
|
2889
|
+
}
|
|
2890
|
+
function ceilDiv(num, denom) {
|
|
2891
|
+
if (denom === 0n) {
|
|
2892
|
+
throw new Error("division by zero in ceilDiv");
|
|
2893
|
+
}
|
|
2894
|
+
const q = num / denom;
|
|
2895
|
+
const r = num % denom;
|
|
2896
|
+
return r === 0n ? q : q + 1n;
|
|
2897
|
+
}
|
|
2898
|
+
function formatFeeBreakdown(estimate) {
|
|
2899
|
+
const line = (label, lamports) => {
|
|
2900
|
+
const label16 = label.padEnd(14);
|
|
2901
|
+
return ` ${label16}${lamports.toString()} lamports (${lamportsToSol(lamports)} SOL)`;
|
|
2902
|
+
};
|
|
2903
|
+
const lines = [
|
|
2904
|
+
"Estimated SOL cost for this transaction:",
|
|
2905
|
+
line("Base fee:", estimate.baseFeeLamports),
|
|
2906
|
+
line("Priority fee:", estimate.priorityFeeLamports)
|
|
2907
|
+
];
|
|
2908
|
+
if (estimate.rentLamports > 0n) {
|
|
2909
|
+
lines.push(line("ATA rent:", estimate.rentLamports));
|
|
2910
|
+
}
|
|
2911
|
+
lines.push(line("Total:", estimate.totalLamports));
|
|
2912
|
+
return lines.join("\n");
|
|
2913
|
+
}
|
|
2914
|
+
function lamportsToSol(lamports) {
|
|
2915
|
+
const LAMPORTS_PER_SOL2 = 1000000000n;
|
|
2916
|
+
const whole = lamports / LAMPORTS_PER_SOL2;
|
|
2917
|
+
const frac = lamports % LAMPORTS_PER_SOL2;
|
|
2918
|
+
return `${whole}.${frac.toString().padStart(9, "0")}`;
|
|
2425
2919
|
}
|
|
2426
|
-
|
|
2427
|
-
// src/config/global.ts
|
|
2428
2920
|
var SessionSpendLimitEntrySchema = z.object({
|
|
2429
2921
|
chain: z.enum(["solana"]),
|
|
2430
2922
|
token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
|
|
@@ -2434,30 +2926,6 @@ var SessionSpendLimitEntrySchema = z.object({
|
|
|
2434
2926
|
var GlobalConfigSchema = z.object({
|
|
2435
2927
|
session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional()
|
|
2436
2928
|
}).strict();
|
|
2437
|
-
function isEnoent(e) {
|
|
2438
|
-
return typeof e === "object" && e !== null && "code" in e && e.code === "ENOENT";
|
|
2439
|
-
}
|
|
2440
|
-
async function loadGlobalConfig(path) {
|
|
2441
|
-
let raw;
|
|
2442
|
-
try {
|
|
2443
|
-
raw = await readFile(path, "utf-8");
|
|
2444
|
-
} catch (e) {
|
|
2445
|
-
if (isEnoent(e)) {
|
|
2446
|
-
return {};
|
|
2447
|
-
}
|
|
2448
|
-
throw e;
|
|
2449
|
-
}
|
|
2450
|
-
if (raw.trim() === "") {
|
|
2451
|
-
return {};
|
|
2452
|
-
}
|
|
2453
|
-
const parsed = YAML2.parse(raw);
|
|
2454
|
-
return GlobalConfigSchema.parse(parsed ?? {});
|
|
2455
|
-
}
|
|
2456
|
-
async function writeGlobalConfig(path, config) {
|
|
2457
|
-
const validated = GlobalConfigSchema.parse(config);
|
|
2458
|
-
const body = YAML2.stringify(validated);
|
|
2459
|
-
await writeFileAtomic(path, body, 420);
|
|
2460
|
-
}
|
|
2461
2929
|
function formatSol(lamports) {
|
|
2462
2930
|
const sol = new Decimal2(lamports).div(LAMPORTS_PER_SOL);
|
|
2463
2931
|
if (sol.gte(1e6)) {
|
|
@@ -2649,6 +3117,6 @@ function makeCensor() {
|
|
|
2649
3117
|
};
|
|
2650
3118
|
}
|
|
2651
3119
|
|
|
2652
|
-
export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PaymentRequestSchema, PingService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, assertExpiry, assertLamports, assetByKey, assetKey, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, formatAssetAmount, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind,
|
|
3120
|
+
export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PaymentRequestSchema, PingService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, USDC_SOLANA_DEVNET, assertExpiry, assertLamports, assetByKey, assetKey, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatAssetAmount, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry, verifyJobPaymentQuick };
|
|
2653
3121
|
//# sourceMappingURL=index.js.map
|
|
2654
3122
|
//# sourceMappingURL=index.js.map
|