@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/dist/index.js CHANGED
@@ -1,13 +1,10 @@
1
1
  import { getTransferSolInstruction } from '@solana-program/system';
2
- import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, getProgramDerivedAddress, assertAccountExists, isAddress, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
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: 15e3,
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, address2, config) {
101
- const maybeAccount = await fetchMaybeConfig(rpc, address2, config);
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, address2, config) {
106
- const maybeAccount = await fetchEncodedAccount(rpc, address2, config);
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 = checkBalanceDiff({
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 = checkBalanceDiff({
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 checkBalanceDiff(input) {
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 providerTransferIx = getTransferSolInstruction({
715
- source: payerSigner,
716
- destination: recipient,
717
- amount: BigInt(providerAmount)
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
- const instructions = [providerTransferIxWithReference];
724
- if (paymentRequest.fee_address && feeAmount > 0) {
972
+ instructions.push(providerTransferIxWithReference);
973
+ if (treasuryAta && paymentRequest.fee_address && feeAmount > 0) {
725
974
  instructions.push(
726
- getTransferSolInstruction({
727
- source: payerSigner,
728
- destination: address(paymentRequest.fee_address),
729
- amount: BigInt(feeAmount)
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
- /** Fetch elisym agents filtered by network. */
937
- async fetchAgents(network = "devnet", limit) {
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()).sort((a, b) => b.lastSeen - a.lastSeen);
1351
+ const agents = Array.from(agentMap.values());
948
1352
  const agentPubkeys = Array.from(agentMap.keys());
949
- if (agentPubkeys.length > 0) {
950
- const activitySince = Math.floor(Date.now() / 1e3) - 24 * 60 * 60;
951
- const resultKinds = /* @__PURE__ */ new Set();
952
- for (const agent of agentMap.values()) {
953
- for (const k of agent.supportedKinds) {
954
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
955
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
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
- resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
960
- const [activityEvents] = await Promise.all([
961
- this.pool.queryBatched(
962
- {
963
- kinds: [...resultKinds, KIND_JOB_FEEDBACK],
964
- since: activitySince
965
- },
966
- agentPubkeys
967
- ),
968
- this.enrichWithMetadata(agents)
969
- ]);
970
- for (const ev of activityEvents) {
971
- if (!verifyEvent(ev)) {
972
- continue;
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
- const agent = agentMap.get(ev.pubkey);
975
- if (agent && ev.created_at > agent.lastSeen) {
976
- agent.lastSeen = ev.created_at;
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
- // src/payment/assets.ts
2340
- var NATIVE_SOL = {
2341
- chain: "solana",
2342
- token: "sol",
2343
- decimals: 9,
2344
- symbol: "SOL"
2345
- };
2346
- var KNOWN_ASSETS = [NATIVE_SOL];
2347
- function assetKey(a) {
2348
- return a.mint ? `${a.chain}:${a.token}:${a.mint}` : `${a.chain}:${a.token}`;
2349
- }
2350
- function resolveKnownAsset(chain, token, mint) {
2351
- const key = mint ? `${chain}:${token}:${mint}` : `${chain}:${token}`;
2352
- return KNOWN_ASSETS.find((asset) => assetKey(asset) === key);
2353
- }
2354
- function assetByKey(key) {
2355
- return KNOWN_ASSETS.find((asset) => assetKey(asset) === key);
2356
- }
2357
- var DECIMAL_RE = /^(\d+\.\d*|\d*\.\d+|\d+)$/;
2358
- function parseAssetAmount(asset, human) {
2359
- const trimmed = human.trim();
2360
- if (!trimmed) {
2361
- throw new Error(`${asset.symbol} amount is empty`);
2362
- }
2363
- if (trimmed.startsWith("-")) {
2364
- throw new Error(`${asset.symbol} amount cannot be negative`);
2365
- }
2366
- if (!DECIMAL_RE.test(trimmed)) {
2367
- throw new Error(
2368
- `${asset.symbol} amount must be a non-negative decimal (e.g. "0.5", "1"); got "${human}"`
2369
- );
2370
- }
2371
- const dotPos = trimmed.indexOf(".");
2372
- let wholePart;
2373
- if (dotPos === -1) {
2374
- wholePart = trimmed;
2375
- } else if (dotPos === 0) {
2376
- wholePart = "0";
2377
- } else {
2378
- wholePart = trimmed.slice(0, dotPos);
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
- return raw;
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 formatAssetAmount(asset, raw) {
2401
- const sign = raw < 0n ? "-" : "";
2402
- const abs = raw < 0n ? -raw : raw;
2403
- const unit = 10n ** BigInt(asset.decimals);
2404
- const whole = abs / unit;
2405
- const frac = abs % unit;
2406
- if (asset.decimals === 0) {
2407
- return `${sign}${whole} ${asset.symbol}`;
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 writeFileAtomic(path, data, mode) {
2412
- await mkdir(dirname(path), { recursive: true });
2413
- const tmpPath = `${path}.tmp.${randomBytes(6).toString("hex")}`;
2414
- await writeFile(tmpPath, data, { mode });
2415
- try {
2416
- await rename(tmpPath, path);
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 { unlink } = await import('node:fs/promises');
2420
- await unlink(tmpPath);
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, loadGlobalConfig, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveKnownAsset, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry, writeGlobalConfig };
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