@elisym/sdk 0.8.0 → 0.9.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 { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
2
3
  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';
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 = [
@@ -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
  }
@@ -2335,96 +2587,124 @@ var ElisymClient = class {
2335
2587
  this.pool.close();
2336
2588
  }
2337
2589
  };
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
- );
2590
+ var DEFAULT_COMPUTE_UNIT_LIMIT2 = 2e5;
2591
+ var DEFAULT_PRIORITY_FEE_PERCENTILE2 = 75;
2592
+ var BASE_FEE_LAMPORTS_PER_SIGNATURE = 5000n;
2593
+ var FALLBACK_ATA_RENT_LAMPORTS = 2039280n;
2594
+ var SPL_TOKEN_ACCOUNT_SIZE = 165;
2595
+ async function estimateSolFeeLamports(rpc, paymentRequest, _payerAddress, options) {
2596
+ const numSignatures = options?.numSignatures ?? 1;
2597
+ const computeUnitLimit = options?.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT2;
2598
+ const priorityFeeMicroLamports = options?.priorityFeeMicroLamports ?? await estimatePriorityFeeMicroLamports(rpc, {
2599
+ percentile: options?.priorityFeePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE2
2600
+ });
2601
+ const baseFeeLamports = BASE_FEE_LAMPORTS_PER_SIGNATURE * BigInt(numSignatures);
2602
+ const priorityFeeLamports = ceilDiv(
2603
+ priorityFeeMicroLamports * BigInt(computeUnitLimit),
2604
+ 1000000n
2605
+ );
2606
+ const asset = resolveAssetFromPaymentRequest(paymentRequest);
2607
+ let rentLamports = 0n;
2608
+ let rentPerAtaLamports = 0n;
2609
+ let missingAtaCount = 0;
2610
+ if (asset.mint) {
2611
+ rentPerAtaLamports = await fetchAtaRent(rpc);
2612
+ const mint = address(asset.mint);
2613
+ const ataAccountsToCheck = [];
2614
+ const [recipientAta] = await findAssociatedTokenPda({
2615
+ owner: address(paymentRequest.recipient),
2616
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
2617
+ mint
2618
+ });
2619
+ ataAccountsToCheck.push(recipientAta);
2620
+ const feeAmount = paymentRequest.fee_amount ?? 0;
2621
+ if (paymentRequest.fee_address && feeAmount > 0) {
2622
+ const [treasuryAta] = await findAssociatedTokenPda({
2623
+ owner: address(paymentRequest.fee_address),
2624
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
2625
+ mint
2626
+ });
2627
+ ataAccountsToCheck.push(treasuryAta);
2628
+ }
2629
+ missingAtaCount = await countMissingAccounts(rpc, ataAccountsToCheck);
2630
+ rentLamports = rentPerAtaLamports * BigInt(missingAtaCount);
2397
2631
  }
2398
- return raw;
2632
+ const totalLamports = baseFeeLamports + priorityFeeLamports + rentLamports;
2633
+ return {
2634
+ baseFeeLamports,
2635
+ priorityFeeLamports,
2636
+ rentLamports,
2637
+ totalLamports,
2638
+ breakdown: {
2639
+ numSignatures,
2640
+ priorityFeeMicroLamports,
2641
+ computeUnitLimit,
2642
+ rentPerAtaLamports,
2643
+ missingAtaCount
2644
+ }
2645
+ };
2399
2646
  }
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}`;
2647
+ async function fetchAtaRent(rpc) {
2648
+ try {
2649
+ const lamports = await rpc.getMinimumBalanceForRentExemption(BigInt(SPL_TOKEN_ACCOUNT_SIZE)).send();
2650
+ if (typeof lamports === "bigint") {
2651
+ return lamports;
2652
+ }
2653
+ if (typeof lamports === "number" && Number.isFinite(lamports) && lamports > 0) {
2654
+ return BigInt(lamports);
2655
+ }
2656
+ return FALLBACK_ATA_RENT_LAMPORTS;
2657
+ } catch {
2658
+ return FALLBACK_ATA_RENT_LAMPORTS;
2408
2659
  }
2409
- return `${sign}${whole}.${frac.toString().padStart(asset.decimals, "0")} ${asset.symbol}`;
2410
2660
  }
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) {
2661
+ async function countMissingAccounts(rpc, accounts) {
2662
+ if (accounts.length === 0) {
2663
+ return 0;
2664
+ }
2665
+ let missing = 0;
2666
+ for (const acct of accounts) {
2418
2667
  try {
2419
- const { unlink } = await import('node:fs/promises');
2420
- await unlink(tmpPath);
2668
+ const res = await rpc.getAccountInfo(acct, { encoding: "base64" }).send();
2669
+ if (!res || !res.value) {
2670
+ missing++;
2671
+ }
2421
2672
  } catch {
2673
+ missing++;
2422
2674
  }
2423
- throw e;
2424
2675
  }
2676
+ return missing;
2677
+ }
2678
+ function ceilDiv(num, denom) {
2679
+ if (denom === 0n) {
2680
+ throw new Error("division by zero in ceilDiv");
2681
+ }
2682
+ const q = num / denom;
2683
+ const r = num % denom;
2684
+ return r === 0n ? q : q + 1n;
2685
+ }
2686
+ function formatFeeBreakdown(estimate) {
2687
+ const line = (label, lamports) => {
2688
+ const label16 = label.padEnd(14);
2689
+ return ` ${label16}${lamports.toString()} lamports (${lamportsToSol(lamports)} SOL)`;
2690
+ };
2691
+ const lines = [
2692
+ "Estimated SOL cost for this transaction:",
2693
+ line("Base fee:", estimate.baseFeeLamports),
2694
+ line("Priority fee:", estimate.priorityFeeLamports)
2695
+ ];
2696
+ if (estimate.rentLamports > 0n) {
2697
+ lines.push(line("ATA rent:", estimate.rentLamports));
2698
+ }
2699
+ lines.push(line("Total:", estimate.totalLamports));
2700
+ return lines.join("\n");
2701
+ }
2702
+ function lamportsToSol(lamports) {
2703
+ const LAMPORTS_PER_SOL2 = 1000000000n;
2704
+ const whole = lamports / LAMPORTS_PER_SOL2;
2705
+ const frac = lamports % LAMPORTS_PER_SOL2;
2706
+ return `${whole}.${frac.toString().padStart(9, "0")}`;
2425
2707
  }
2426
-
2427
- // src/config/global.ts
2428
2708
  var SessionSpendLimitEntrySchema = z.object({
2429
2709
  chain: z.enum(["solana"]),
2430
2710
  token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -2434,30 +2714,6 @@ var SessionSpendLimitEntrySchema = z.object({
2434
2714
  var GlobalConfigSchema = z.object({
2435
2715
  session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional()
2436
2716
  }).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
2717
  function formatSol(lamports) {
2462
2718
  const sol = new Decimal2(lamports).div(LAMPORTS_PER_SOL);
2463
2719
  if (sol.gte(1e6)) {
@@ -2649,6 +2905,6 @@ function makeCensor() {
2649
2905
  };
2650
2906
  }
2651
2907
 
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 };
2908
+ 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, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatAssetAmount, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
2653
2909
  //# sourceMappingURL=index.js.map
2654
2910
  //# sourceMappingURL=index.js.map