@circle-fin/app-kit 1.2.0 → 1.3.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/index.cjs CHANGED
@@ -690,6 +690,12 @@ const InputError = {
690
690
  name: 'INPUT_UNSUPPORTED_ACTION',
691
691
  type: 'INPUT',
692
692
  },
693
+ /** No route satisfies the slippage or minimum-output constraint */
694
+ SLIPPAGE_CONSTRAINT_NOT_MET: {
695
+ code: 1009,
696
+ name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
697
+ type: 'INPUT',
698
+ },
693
699
  /** General validation failure for complex validation rules */
694
700
  VALIDATION_FAILED: {
695
701
  code: 1098,
@@ -1720,6 +1726,40 @@ function normalizeTransactionDetails(txHash, explorerUrl) {
1720
1726
  }
1721
1727
  return normalized;
1722
1728
  }
1729
+ /**
1730
+ * Pattern that matches on-chain revert reasons caused by slippage or
1731
+ * price constraints. When a revert matches, the error is surfaced as
1732
+ * {@link InputError.SLIPPAGE_CONSTRAINT_NOT_MET} (RETRYABLE) instead of
1733
+ * a generic simulation-failed / transaction-reverted error.
1734
+ *
1735
+ * @internal
1736
+ */
1737
+ const SLIPPAGE_REVERT_PATTERN = /slippage|lower than the minimum required|less than the initial balance|minimum.?output|price.?impact|stop.?limit|InsufficientOutput|InsufficientFinalBalance/i;
1738
+ /**
1739
+ * Handle simulation / execution revert errors, distinguishing slippage
1740
+ * constraint failures from generic reverts and simulation failures.
1741
+ *
1742
+ * @internal
1743
+ */
1744
+ function handleRevertError(msg, error, context) {
1745
+ const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
1746
+ if (SLIPPAGE_REVERT_PATTERN.test(reason)) {
1747
+ return new KitError({
1748
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
1749
+ recoverability: 'RETRYABLE',
1750
+ message: `Transaction on ${context.chain} reverted: "${reason}". ` +
1751
+ 'Try increasing slippageBps or adjusting stopLimit.',
1752
+ cause: { trace: { rawError: error, chain: context.chain, reason } },
1753
+ });
1754
+ }
1755
+ if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
1756
+ return createSimulationFailedError(context.chain, reason, {
1757
+ rawError: error,
1758
+ });
1759
+ }
1760
+ const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
1761
+ return createTransactionRevertedError(context.chain, reason, { rawError: error }, txHash, explorerUrl);
1762
+ }
1723
1763
  /**
1724
1764
  * Parses raw blockchain errors into structured KitError instances.
1725
1765
  *
@@ -1800,21 +1840,7 @@ function parseBlockchainError(error, context) {
1800
1840
  // Pattern 2: Simulation and execution reverts
1801
1841
  // Matches contract revert errors and simulation failures
1802
1842
  if (/execution reverted|simulation failed|transaction reverted|transaction failed/i.test(msg)) {
1803
- const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
1804
- // Distinguish between simulation failures and transaction reverts
1805
- // "simulation failed" or "eth_call" indicates pre-flight simulation
1806
- // "transaction failed" or context.operation === 'transaction' indicates post-execution
1807
- if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
1808
- return createSimulationFailedError(context.chain, reason, {
1809
- rawError: error,
1810
- });
1811
- }
1812
- // Transaction execution failures or reverts
1813
- // Include txHash and explorerUrl if available (transaction was submitted)
1814
- const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
1815
- return createTransactionRevertedError(context.chain, reason, {
1816
- rawError: error,
1817
- }, txHash, explorerUrl);
1843
+ return handleRevertError(msg, error, context);
1818
1844
  }
1819
1845
  // Pattern 3: Gas-related errors
1820
1846
  // Matches gas estimation failures and gas exhaustion
@@ -1839,8 +1865,12 @@ function parseBlockchainError(error, context) {
1839
1865
  return createNetworkConnectionError(context.chain, { rawError: error });
1840
1866
  }
1841
1867
  // Pattern 5: RPC provider errors
1842
- // Matches RPC endpoint errors, invalid responses, and rate limits
1843
- if (/rpc|invalid response|rate limit|too many requests/i.test(msg)) {
1868
+ // Matches RPC endpoint errors, invalid responses, rate limits, and
1869
+ // transient JSON-RPC internal errors (e.g. ethers.js "could not coalesce error").
1870
+ // Note: "internal error" alone is too broad — contracts like USDT emit
1871
+ // "An internal error was received" for on-chain assertion failures.
1872
+ // We require JSON-RPC context (codes -32603/-32000) instead.
1873
+ if (/rpc|invalid response|rate limit|too many requests|could not coalesce|no response|server error|json-rpc\s+internal|internal json-rpc|-32603|-32000/i.test(msg)) {
1844
1874
  return createRpcEndpointError(context.chain, { rawError: error });
1845
1875
  }
1846
1876
  // Pattern 6: Transaction size limit errors
@@ -2937,8 +2967,19 @@ function handleClientError(statusCode, serviceName, operation, error, msg, respo
2937
2967
  trace: error,
2938
2968
  },
2939
2969
  });
2940
- // 404 - Not found - unsupported route
2970
+ // 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
2941
2971
  case 404:
2972
+ if (isSlippageConstraintFailure(responseBody)) {
2973
+ return new KitError({
2974
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
2975
+ recoverability: 'RETRYABLE',
2976
+ message: `${serviceName} ${operation} failed: ${detail}. ` +
2977
+ 'Try increasing slippageBps or adjusting stopLimit.',
2978
+ cause: {
2979
+ trace: error,
2980
+ },
2981
+ });
2982
+ }
2942
2983
  return new KitError({
2943
2984
  ...InputError.UNSUPPORTED_ROUTE,
2944
2985
  recoverability: 'FATAL',
@@ -2973,6 +3014,38 @@ function handleClientError(statusCode, serviceName, operation, error, msg, respo
2973
3014
  });
2974
3015
  }
2975
3016
  }
3017
+ /**
3018
+ * Pattern that matches proxy response body text indicating the 404 was
3019
+ * caused by a slippage / price constraint rather than a truly unsupported
3020
+ * route. Kept case-insensitive so future proxy wording changes are tolerated.
3021
+ *
3022
+ * @internal
3023
+ */
3024
+ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
3025
+ /**
3026
+ * Determine whether a 404 was caused by an unmet slippage or price
3027
+ * constraint rather than a genuinely unsupported route.
3028
+ *
3029
+ * Detection relies on the proxy response body containing slippage-related
3030
+ * language or a structured reason code. This avoids false positives that
3031
+ * would occur if we guessed based on request parameters alone (a user
3032
+ * can set `slippageBps` and still hit a truly unsupported route).
3033
+ *
3034
+ * @param responseBody - The parsed JSON body returned by the proxy
3035
+ * @returns `true` when the 404 should be treated as a slippage constraint failure
3036
+ * @internal
3037
+ */
3038
+ function isSlippageConstraintFailure(responseBody) {
3039
+ if (responseBody === undefined) {
3040
+ return false;
3041
+ }
3042
+ const textsToCheck = [
3043
+ responseBody.externalMessage,
3044
+ responseBody.message,
3045
+ extractDetailFromBody(responseBody),
3046
+ ];
3047
+ return textsToCheck.some((t) => typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
3048
+ }
2976
3049
  /**
2977
3050
  * Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
2978
3051
  *
@@ -3128,13 +3201,16 @@ function joinFieldErrors(errors) {
3128
3201
  * Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
3129
3202
  *
3130
3203
  * Resolution order:
3131
- * 1. `body.message` **and** `body.errors` -- when both are present the
3204
+ * 1. `body.externalMessage` -- the user-facing string the proxy intends
3205
+ * consumers to display (e.g. "No route found that satisfies the
3206
+ * requested stop limit"). Preferred when available.
3207
+ * 2. `body.message` **and** `body.errors` -- when both are present the
3132
3208
  * top-level message is combined with the field-level detail so
3133
3209
  * developers see the full picture
3134
3210
  * (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
3135
- * 2. `body.message` alone -- used as-is.
3136
- * 3. `body.errors` alone -- field-level entries joined with "; ".
3137
- * 4. `undefined` -- caller should fall back to the raw HTTP status text.
3211
+ * 3. `body.message` alone -- used as-is.
3212
+ * 4. `body.errors` alone -- field-level entries joined with "; ".
3213
+ * 5. `undefined` -- caller should fall back to the raw HTTP status text.
3138
3214
  *
3139
3215
  * @param body - The parsed response body, may be undefined
3140
3216
  * @returns A detail string, or undefined when no useful info is available
@@ -3144,6 +3220,12 @@ function extractDetailFromBody(body) {
3144
3220
  if (body === undefined) {
3145
3221
  return undefined;
3146
3222
  }
3223
+ const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0
3224
+ ? body.externalMessage
3225
+ : undefined;
3226
+ if (externalMessage !== undefined) {
3227
+ return externalMessage;
3228
+ }
3147
3229
  const topMessage = typeof body.message === 'string' && body.message.length > 0
3148
3230
  ? body.message
3149
3231
  : undefined;
@@ -3341,8 +3423,9 @@ exports.Blockchain = void 0;
3341
3423
  /**
3342
3424
  * Enum representing chains that support same-chain swaps through the Swap Kit.
3343
3425
  *
3344
- * Unlike the full {@link Blockchain} enum, SwapChain includes only mainnet
3345
- * networks where adapter contracts are deployed (CCTPv2 support).
3426
+ * Unlike the full {@link Blockchain} enum, SwapChain includes mainnet
3427
+ * networks and explicitly whitelisted testnets (e.g., {@link Arc_Testnet})
3428
+ * where adapter contracts are deployed (CCTPv2 support).
3346
3429
  *
3347
3430
  * Dynamic validation via {@link isSwapSupportedChain} ensures chains
3348
3431
  * automatically work when adapter contracts and supported tokens are deployed.
@@ -3387,6 +3470,8 @@ exports.SwapChain = void 0;
3387
3470
  SwapChain["XDC"] = "XDC";
3388
3471
  SwapChain["HyperEVM"] = "HyperEVM";
3389
3472
  SwapChain["Monad"] = "Monad";
3473
+ // Testnet chains with swap support
3474
+ SwapChain["Arc_Testnet"] = "Arc_Testnet";
3390
3475
  })(exports.SwapChain || (exports.SwapChain = {}));
3391
3476
  // -----------------------------------------------------------------------------
3392
3477
  // Bridge Chain Enum (CCTPv2 Supported Chains)
@@ -3483,6 +3568,31 @@ exports.BridgeChain = void 0;
3483
3568
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
3484
3569
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
3485
3570
  })(exports.BridgeChain || (exports.BridgeChain = {}));
3571
+ // -----------------------------------------------------------------------------
3572
+ // Earn Chain Enum
3573
+ // -----------------------------------------------------------------------------
3574
+ /**
3575
+ * Enumeration of blockchains that support earn (vault deposit/withdraw)
3576
+ * operations through the Earn Kit.
3577
+ *
3578
+ * Currently only Ethereum mainnet is supported. Additional chains
3579
+ * will be added as vault protocol support expands.
3580
+ *
3581
+ * @example
3582
+ * ```typescript
3583
+ * import { EarnChain } from '@core/chains'
3584
+ *
3585
+ * const result = await earnKit.deposit({
3586
+ * from: { adapter, chain: EarnChain.Ethereum },
3587
+ * vaultAddress: '0x...',
3588
+ * amount: '100',
3589
+ * })
3590
+ * ```
3591
+ */
3592
+ var EarnChain;
3593
+ (function (EarnChain) {
3594
+ EarnChain["Ethereum"] = "Ethereum";
3595
+ })(EarnChain || (EarnChain = {}));
3486
3596
 
3487
3597
  /**
3488
3598
  * Helper function to define a chain with proper TypeScript typing.
@@ -3798,6 +3908,14 @@ const BRIDGE_CONTRACT_EVM_MAINNET = '0xB3FA262d0fB521cc93bE83d87b322b8A23DAf3F0'
3798
3908
  * on EVM-compatible chains. Use this address for mainnet adapter integrations.
3799
3909
  */
3800
3910
  const ADAPTER_CONTRACT_EVM_MAINNET = '0x7FB8c7260b63934d8da38aF902f87ae6e284a845';
3911
+ /**
3912
+ * The adapter contract address for EVM testnet networks.
3913
+ *
3914
+ * This contract serves as an adapter for integrating with various protocols
3915
+ * on EVM-compatible testnet chains. Use this address for testnet adapter
3916
+ * integrations (e.g., Arc Testnet).
3917
+ */
3918
+ const ADAPTER_CONTRACT_EVM_TESTNET = '0xBBD70b01a1CAbc96d5b7b129Ae1AAabdf50dd40b';
3801
3919
 
3802
3920
  /**
3803
3921
  * Arc Testnet chain definition
@@ -3846,6 +3964,7 @@ const ArcTestnet = defineChain({
3846
3964
  },
3847
3965
  kitContracts: {
3848
3966
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
3967
+ adapter: ADAPTER_CONTRACT_EVM_TESTNET,
3849
3968
  },
3850
3969
  });
3851
3970
 
@@ -4536,7 +4655,7 @@ const HyperEVM = defineChain({
4536
4655
  },
4537
4656
  chainId: 999,
4538
4657
  isTestnet: false,
4539
- explorerUrl: 'https://app.hyperliquid.xyz/explorer/tx/{hash}',
4658
+ explorerUrl: 'https://hyperevmscan.io/tx/{hash}',
4540
4659
  rpcEndpoints: ['https://rpc.hyperliquid.xyz/evm'],
4541
4660
  eurcAddress: null,
4542
4661
  usdcAddress: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
@@ -5644,7 +5763,7 @@ const Solana = defineChain({
5644
5763
  },
5645
5764
  forwarderSupported: {
5646
5765
  source: true,
5647
- destination: false,
5766
+ destination: true,
5648
5767
  },
5649
5768
  },
5650
5769
  kitContracts: {
@@ -5691,7 +5810,7 @@ const SolanaDevnet = defineChain({
5691
5810
  },
5692
5811
  forwarderSupported: {
5693
5812
  source: true,
5694
- destination: false,
5813
+ destination: true,
5695
5814
  },
5696
5815
  },
5697
5816
  kitContracts: {
@@ -6501,6 +6620,41 @@ const bridgeChainIdentifierSchema = zod.z.union([
6501
6620
  message: `Chain "${chainDef.name}" (${chainDef.chain}) is not supported for bridging. Only chains in the BridgeChain enum support CCTPv2 bridging.`,
6502
6621
  })),
6503
6622
  ]);
6623
+ /**
6624
+ * Zod schema for validating earn-specific chain identifiers.
6625
+ *
6626
+ * Validate that the provided chain is supported for earn (vault
6627
+ * deposit/withdraw) operations. Currently only Ethereum is
6628
+ * supported.
6629
+ *
6630
+ * Accept an EarnChain enum value, a matching string literal, or
6631
+ * a ChainDefinition for a supported chain.
6632
+ *
6633
+ * @example
6634
+ * ```typescript
6635
+ * import { earnChainIdentifierSchema } from '@core/chains'
6636
+ * import { EarnChain, Ethereum } from '@core/chains'
6637
+ *
6638
+ * // Valid
6639
+ * earnChainIdentifierSchema.parse(EarnChain.Ethereum)
6640
+ * earnChainIdentifierSchema.parse('Ethereum')
6641
+ * earnChainIdentifierSchema.parse(Ethereum)
6642
+ *
6643
+ * // Invalid (throws ZodError)
6644
+ * earnChainIdentifierSchema.parse('Solana')
6645
+ * ```
6646
+ */
6647
+ zod.z.union([
6648
+ zod.z.string().refine((val) => val in EarnChain, (val) => ({
6649
+ message: `"${val}" is not a supported earn chain. ` +
6650
+ `Supported chains: ${Object.values(EarnChain).join(', ')}`,
6651
+ })),
6652
+ zod.z.nativeEnum(EarnChain),
6653
+ chainDefinitionSchema$2.refine((chain) => chain.chain in EarnChain, (chain) => ({
6654
+ message: `"${chain.chain}" is not a supported earn chain. ` +
6655
+ `Supported chains: ${Object.values(EarnChain).join(', ')}`,
6656
+ })),
6657
+ ]);
6504
6658
 
6505
6659
  /**
6506
6660
  * @packageDocumentation
@@ -6868,18 +7022,21 @@ function getSwapOkTokenStatus(tokenIn, tokenOut, chain, tokenRegistry, okSymbols
6868
7022
  * A chain supports swaps if ALL conditions are met:
6869
7023
  * 1. Is a member of the {@link SwapChain} enum
6870
7024
  * 2. Has CCTPv2 support (adapter contract deployed)
6871
- * 3. Is mainnet (not testnet)
7025
+ *
7026
+ * Testnets are allowed only when explicitly listed in the
7027
+ * {@link SwapChain} enum (e.g., Arc_Testnet).
6872
7028
  *
6873
7029
  * @param chain - Chain definition to check
6874
7030
  * @returns true if chain supports swap operations
6875
7031
  *
6876
7032
  * @example
6877
7033
  * ```typescript
6878
- * import { isSwapSupportedChain, Ethereum, Arbitrum, Sui } from '@core/chains'
7034
+ * import { isSwapSupportedChain, Ethereum, Arbitrum, Sui, ArcTestnet } from '@core/chains'
6879
7035
  *
6880
- * isSwapSupportedChain(Ethereum) // true (has CCTPv2, mainnet)
6881
- * isSwapSupportedChain(Arbitrum) // true (has CCTPv2, mainnet)
6882
- * isSwapSupportedChain(Sui) // false (no CCTPv2 yet)
7036
+ * isSwapSupportedChain(Ethereum) // true (has CCTPv2, mainnet)
7037
+ * isSwapSupportedChain(Arbitrum) // true (has CCTPv2, mainnet)
7038
+ * isSwapSupportedChain(ArcTestnet) // true (has CCTPv2, whitelisted testnet)
7039
+ * isSwapSupportedChain(Sui) // false (no CCTPv2 yet)
6883
7040
  * ```
6884
7041
  *
6885
7042
  * @remarks
@@ -6890,18 +7047,12 @@ function getSwapOkTokenStatus(tokenIn, tokenOut, chain, tokenRegistry, okSymbols
6890
7047
  * No code changes needed when new chains are added!
6891
7048
  */
6892
7049
  function isSwapSupportedChain(chain) {
6893
- // Must be in the SwapChain enum
6894
7050
  if (!(chain.chain in exports.SwapChain)) {
6895
7051
  return false;
6896
7052
  }
6897
- // Must have CCTPv2 support (adapter contract)
6898
7053
  if (!isCCTPV2Supported(chain)) {
6899
7054
  return false;
6900
7055
  }
6901
- // Must be mainnet (no testnet swaps)
6902
- if (chain.isTestnet) {
6903
- return false;
6904
- }
6905
7056
  return true;
6906
7057
  }
6907
7058
  /**
@@ -6911,7 +7062,6 @@ function isSwapSupportedChain(chain) {
6911
7062
  * Dynamically filter chain definitions to include only those that:
6912
7063
  * - Are members of the {@link SwapChain} enum
6913
7064
  * - Have CCTPv2 support
6914
- * - Are mainnets
6915
7065
  *
6916
7066
  * This function is pure and accepts chains as parameter to avoid
6917
7067
  * circular dependencies.
@@ -7515,8 +7665,15 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
7515
7665
  }
7516
7666
  }
7517
7667
  }
7518
- // After the loop: we're guaranteed to have a lastError
7519
- throw new Error(`Maximum retry attempts (${String(effectiveConfig.maxRetries)}) exceeded: ${String(lastError?.message)}`);
7668
+ // Preserve responseBody from the last attempt so upstream parsers
7669
+ // (e.g. parseApiError) can inspect the server's structured response.
7670
+ const retryError = new Error(`Maximum retry attempts (${String(effectiveConfig.maxRetries)}) exceeded: ${String(lastError?.message)}`);
7671
+ if (lastError !== undefined &&
7672
+ 'responseBody' in lastError &&
7673
+ lastError.responseBody !== undefined) {
7674
+ retryError.responseBody = lastError.responseBody;
7675
+ }
7676
+ throw retryError;
7520
7677
  };
7521
7678
  /**
7522
7679
  * Convenience function for making GET requests with validation.
@@ -8207,6 +8364,7 @@ const USDC = {
8207
8364
  // =========================================================================
8208
8365
  // Testnets (alphabetically sorted)
8209
8366
  // =========================================================================
8367
+ [exports.Blockchain.Arc_Testnet]: '0x3600000000000000000000000000000000000000',
8210
8368
  [exports.Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
8211
8369
  [exports.Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
8212
8370
  [exports.Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
@@ -8283,6 +8441,8 @@ const EURC = {
8283
8441
  [exports.Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
8284
8442
  [exports.Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
8285
8443
  [exports.Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
8444
+ // Testnets
8445
+ [exports.Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
8286
8446
  },
8287
8447
  };
8288
8448
 
@@ -9206,8 +9366,88 @@ function buildForwardingHookData() {
9206
9366
  return cachedHookDataHex;
9207
9367
  }
9208
9368
 
9369
+ const DEFAULTS = {
9370
+ maxRetries: 3,
9371
+ baseDelayMs: 1000,
9372
+ maxDelayMs: 15_000,
9373
+ deadlineMs: undefined,
9374
+ jitter: true,
9375
+ isRetryable: () => true,
9376
+ };
9377
+ function resolveOptions(options) {
9378
+ if (options === undefined)
9379
+ return DEFAULTS;
9380
+ return {
9381
+ maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
9382
+ baseDelayMs: options.baseDelayMs ?? DEFAULTS.baseDelayMs,
9383
+ maxDelayMs: options.maxDelayMs ?? DEFAULTS.maxDelayMs,
9384
+ deadlineMs: options.deadlineMs ?? DEFAULTS.deadlineMs,
9385
+ jitter: options.jitter ?? DEFAULTS.jitter,
9386
+ isRetryable: options.isRetryable ?? DEFAULTS.isRetryable,
9387
+ };
9388
+ }
9389
+ /**
9390
+ * Calculate exponential backoff delay with optional jitter.
9391
+ *
9392
+ * @param attempt - 1-indexed retry attempt number.
9393
+ * @param config - Resolved retry configuration.
9394
+ * @returns Delay in milliseconds.
9395
+ */
9396
+ function calculateDelay(attempt, config) {
9397
+ let delay = config.baseDelayMs * Math.pow(2, attempt - 1);
9398
+ delay = Math.min(delay, config.maxDelayMs);
9399
+ if (config.jitter) {
9400
+ const jitterFactor = 0.75 + Math.random() * 0.5; // NOSONAR - not security-sensitive
9401
+ delay = Math.round(delay * jitterFactor);
9402
+ }
9403
+ return delay;
9404
+ }
9405
+ /**
9406
+ * Retry an async function with exponential backoff and jitter.
9407
+ *
9408
+ * This is a lightweight standalone utility with no `@core/runtime`
9409
+ * dependency, suitable for use in adapters and providers that do not
9410
+ * participate in the middleware pipeline.
9411
+ *
9412
+ * @typeParam T - The resolved value type.
9413
+ * @param fn - The async function to execute (and potentially retry).
9414
+ * @param options - Retry configuration.
9415
+ * @returns The resolved value of `fn`.
9416
+ * @throws The last error when all retry attempts are exhausted, or
9417
+ * immediately when `isRetryable` returns `false`.
9418
+ *
9419
+ * @example
9420
+ * ```typescript
9421
+ * import { retryAsync } from '@core/utils'
9422
+ *
9423
+ * const receipt = await retryAsync(
9424
+ * () => adapter.waitForTransaction(txHash, { confirmations: 1 }, chain),
9425
+ * { maxRetries: 3, isRetryable: (err) => isTransientRpcError(err) },
9426
+ * )
9427
+ * ```
9428
+ */
9429
+ async function retryAsync(fn, options) {
9430
+ const config = resolveOptions(options);
9431
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
9432
+ try {
9433
+ return await fn();
9434
+ }
9435
+ catch (error) {
9436
+ const pastDeadline = config.deadlineMs !== undefined && Date.now() >= config.deadlineMs;
9437
+ const isLastAttempt = attempt >= config.maxRetries;
9438
+ if (isLastAttempt || pastDeadline || !config.isRetryable(error)) {
9439
+ throw error;
9440
+ }
9441
+ const delayMs = calculateDelay(attempt + 1, config);
9442
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
9443
+ }
9444
+ }
9445
+ /* istanbul ignore next: unreachable safety throw for TypeScript */
9446
+ throw new Error('retryAsync: unreachable');
9447
+ }
9448
+
9209
9449
  var name$1 = "@circle-fin/bridge-kit";
9210
- var version$2 = "1.8.0";
9450
+ var version$2 = "1.8.2";
9211
9451
  var pkg$2 = {
9212
9452
  name: name$1,
9213
9453
  version: version$2};
@@ -12723,10 +12963,13 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
12723
12963
  }
12724
12964
  const txHash = await request.execute();
12725
12965
  step.txHash = txHash;
12726
- const transaction = await adapter.waitForTransaction(txHash, {
12727
- confirmations,
12728
- timeout,
12729
- }, chain);
12966
+ const retryOptions = {
12967
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
12968
+ };
12969
+ if (timeout !== undefined) {
12970
+ retryOptions.deadlineMs = Date.now() + timeout;
12971
+ }
12972
+ const transaction = await retryAsync(async () => adapter.waitForTransaction(txHash, { confirmations, timeout }, chain), retryOptions);
12730
12973
  step.state = transaction.blockNumber ? 'success' : 'error';
12731
12974
  step.data = transaction;
12732
12975
  // Generate explorer URL for the step
@@ -13458,7 +13701,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
13458
13701
  return step;
13459
13702
  }
13460
13703
  try {
13461
- const transaction = await adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain);
13704
+ const transaction = await retryAsync(async () => adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain), {
13705
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, {
13706
+ chain: chain.name,
13707
+ txHash: receipt.txHash,
13708
+ })),
13709
+ });
13462
13710
  step.state = transaction.blockNumber === undefined ? 'error' : 'success';
13463
13711
  step.data = transaction;
13464
13712
  if (transaction.blockNumber === undefined) {
@@ -13474,7 +13722,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
13474
13722
  return step;
13475
13723
  }
13476
13724
 
13477
- var version$1 = "1.6.0";
13725
+ var version$1 = "1.6.2";
13478
13726
  var pkg$1 = {
13479
13727
  version: version$1};
13480
13728
 
@@ -14215,7 +14463,10 @@ async function waitForPendingTransaction(pendingStep, adapter, chain) {
14215
14463
  message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`,
14216
14464
  });
14217
14465
  }
14218
- const txReceipt = await adapter.waitForTransaction(pendingStep.txHash, undefined, chain);
14466
+ const txHash = pendingStep.txHash;
14467
+ const txReceipt = await retryAsync(async () => adapter.waitForTransaction(txHash, undefined, chain), {
14468
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
14469
+ });
14219
14470
  // Check if transaction was confirmed on-chain
14220
14471
  if (!txReceipt.blockNumber) {
14221
14472
  return {
@@ -16284,730 +16535,378 @@ const createBridgeKit = (context) => {
16284
16535
  };
16285
16536
 
16286
16537
  var name = "@circle-fin/swap-kit";
16287
- var version = "1.0.2";
16538
+ var version = "1.1.0";
16288
16539
  var pkg = {
16289
16540
  name: name,
16290
16541
  version: version};
16291
16542
 
16292
16543
  /**
16293
- * Schema for validating AdapterContext.
16294
- * Must always contain both adapter and chain explicitly.
16295
- * Optionally includes address for developer-controlled adapters.
16544
+ * @packageDocumentation
16545
+ * @module StablecoinServiceSwapSchemas
16546
+ *
16547
+ * Zod validation schemas for Stablecoin Service Swap Provider parameters.
16548
+ *
16549
+ * This module defines runtime validation schemas using Zod for type-safe
16550
+ * validation of swap requests and service responses.
16296
16551
  */
16297
- const adapterContextSchema$2 = zod.z.object({
16298
- adapter: adapterSchema$1,
16299
- chain: swapChainIdentifierSchema,
16300
- address: zod.z.string().optional(),
16301
- });
16302
16552
  /**
16303
- * Schema for validating allowance strategy values.
16553
+ * Schema for destination address (to): must be a valid EVM or Solana address.
16554
+ * Catches obviously malformed addresses at parse time; chain-specific validation
16555
+ * is performed in buildServiceParams.
16556
+ */
16557
+ const destinationAddressSchema = zod.z.union([
16558
+ evmAddressSchema,
16559
+ solanaAddressSchema,
16560
+ ]);
16561
+ /**
16562
+ * Zod schema for allowance strategy.
16563
+ *
16564
+ * Validates the allowance strategy for token approvals.
16304
16565
  */
16305
- const allowanceStrategySchema$1 = zod.z.enum(['permit', 'approve']);
16566
+ const allowanceStrategySchema$1 = zod.z.enum(['permit', 'approve'], {
16567
+ invalid_type_error: 'allowanceStrategy must be either "permit" or "approve"',
16568
+ });
16306
16569
  /**
16307
- * Schema for validating SwapKit custom fee configuration.
16570
+ * Schema for validating service swap custom fee configuration.
16308
16571
  *
16309
- * Supports two mutually exclusive approaches:
16572
+ * Supports SwapKit fee approaches:
16310
16573
  * - Percentage-based: percentageBps + recipientAddress
16311
16574
  * - Callback-based: amount + recipientAddress
16312
- *
16313
- * @remarks
16314
- * Mutual exclusivity is validated at runtime in buildServiceParams.
16315
- * Chain-specific address validation is performed in resolveSwapConfig.
16316
16575
  */
16317
- const swapCustomFeeSchema = zod.z
16576
+ const serviceSwapCustomFeeSchema = zod.z
16318
16577
  .object({
16319
- /**
16320
- * Fee percentage in basis points (percentage approach).
16321
- * 100 bps = 1%, must be > 0 and <= 10000.
16322
- */
16323
- percentageBps: zod.z.number().int().positive().max(10000),
16324
- /**
16325
- * Fee recipient address (required).
16326
- * Must be a valid EVM address or Solana address.
16327
- */
16328
- recipientAddress: zod.z
16329
- .string()
16330
- .refine((value) => evmAddressSchema.safeParse(value).success ||
16331
- solanaAddressSchema.safeParse(value).success, {
16332
- message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)',
16333
- }),
16578
+ percentageBps: zod.z.number().int().positive().max(10000).optional(),
16579
+ amount: zod.z.string().optional(),
16580
+ recipientAddress: zod.z.string().min(1).optional(),
16334
16581
  })
16335
16582
  .strict();
16336
16583
  /**
16337
- * Schema for validating swap configuration options.
16584
+ * Zod schema for swap configuration.
16338
16585
  *
16339
- * Validates:
16340
- * - allowanceStrategy: Either 'permit' or 'approve'
16341
- * - slippageBps: Optional positive number for slippage tolerance
16342
- * - stopLimit: Optional decimal string for minimum output
16343
- * - customFee: Optional fee configuration
16344
- * - kitKey: Optional string identifier
16586
+ * Validates the optional configuration object for swap operations.
16587
+ * Uses shared validation utilities from \@core/provider for consistency.
16345
16588
  */
16346
- const swapConfigSchema = zod.z.object({
16589
+ const serviceSwapConfigSchema = zod.z.object({
16347
16590
  allowanceStrategy: allowanceStrategySchema$1.optional(),
16348
- slippageBps: zod.z.number().int().min(0).optional(),
16591
+ slippageBps: zod.z
16592
+ .number({
16593
+ invalid_type_error: 'slippageBps must be a number',
16594
+ })
16595
+ .int('slippageBps must be an integer')
16596
+ .min(0, 'slippageBps must be non-negative')
16597
+ .max(10000, 'slippageBps must be at most 10000 (100%)')
16598
+ .optional(),
16349
16599
  stopLimit: zod.z
16350
- .string()
16351
- .min(1, 'Required')
16352
- .pipe(createDecimalStringValidator({
16353
- allowZero: true,
16354
- regexMessage: 'Stop limit must be a numeric string with dot (.) as decimal separator (e.g., "0.1", ".1", "10.5", "1000.50"), with no thousand separators or comma decimals.',
16355
- attributeName: 'stopLimit',
16356
- })(zod.z.string()))
16600
+ .string({
16601
+ invalid_type_error: 'stopLimit must be a string',
16602
+ })
16603
+ .min(1, 'stopLimit is required when provided')
16604
+ .regex(/^\d+$/, 'stopLimit must be an integer string in base units (e.g., "50000000" for 50 USDC with 6 decimals)')
16605
+ .refine((val) => {
16606
+ try {
16607
+ return BigInt(val) > 0n;
16608
+ }
16609
+ catch {
16610
+ return false;
16611
+ }
16612
+ }, {
16613
+ message: 'stopLimit must be greater than 0',
16614
+ })
16615
+ .optional(),
16616
+ customFee: serviceSwapCustomFeeSchema.optional(),
16617
+ kitKey: zod.z
16618
+ .string({
16619
+ invalid_type_error: 'kitKey must be a string',
16620
+ })
16621
+ .min(1, 'kitKey must be a non-empty string')
16622
+ .optional(),
16623
+ provider: zod.z
16624
+ .string({
16625
+ invalid_type_error: 'provider must be a string',
16626
+ })
16627
+ .min(1, 'provider must be a non-empty string')
16357
16628
  .optional(),
16358
- customFee: swapCustomFeeSchema.optional(),
16359
- kitKey: zod.z.string().optional(),
16360
16629
  });
16361
- const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
16362
- const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
16363
- const BASE58_CHARS_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
16364
16630
  /**
16365
- * Broad heuristic: return true when the input is composed entirely of
16366
- * base58 characters and is at least 32 characters long (the minimum
16367
- * length of a valid Solana address).
16631
+ * Zod schema for adapter context.
16368
16632
  *
16369
- * The caller is expected to have already excluded valid Solana
16370
- * addresses (via {@link SOLANA_ADDRESS_REGEX}) before invoking this
16371
- * helper, so in practice it only matches over-length base58 strings
16372
- * (45+ chars) that are plausibly a malformed Solana address.
16633
+ * Validates the adapter context which contains the adapter and chain.
16634
+ *
16635
+ * @remarks
16636
+ * Optionally includes address for developer-controlled adapters.
16373
16637
  */
16374
- function looksLikeSolanaAddress(value) {
16375
- return BASE58_CHARS_REGEX.test(value) && value.length >= 32;
16376
- }
16377
- const MAX_DISPLAY_LENGTH = 30;
16378
- /** Truncate a value for safe inclusion in error messages. */
16379
- function truncate(value) {
16380
- return value.length > MAX_DISPLAY_LENGTH
16381
- ? `${value.slice(0, MAX_DISPLAY_LENGTH)}...`
16382
- : value;
16383
- }
16638
+ const adapterContextSchema$2 = zod.z.object({
16639
+ adapter: adapterSchema$1,
16640
+ chain: chainIdentifierSchema,
16641
+ address: zod.z.string().optional(),
16642
+ });
16384
16643
  /**
16385
- * Schema for validating swap token input.
16644
+ * Zod schema for ServiceSwapParams.
16386
16645
  *
16387
- * Accepts either:
16388
- * - Token symbols from the supported swap token registry ('USDC', 'WETH', 'NATIVE', etc.)
16389
- * - Token addresses (EVM: 0x..., Solana: base58)
16390
- *
16391
- * This allows swapping both supported tokens (by symbol or address) and
16392
- * arbitrary tokens (by address only), as long as at least one token is
16393
- * an "OK token" for fee collection purposes.
16646
+ * Validates the input parameters for swap operations including
16647
+ * adapter context, tokens, amount, destination, and optional configuration.
16394
16648
  *
16395
- * @remarks
16396
- * Uses input-shape heuristics to produce contextual error messages:
16397
- * - Starts with `0x` validated as EVM address
16398
- * - Recognized symbol — accepted
16399
- * - 32–44 base58 characters — accepted as Solana address
16400
- * - Otherwise — reports the most likely error (unsupported symbol,
16401
- * malformed Solana address, or malformed EVM address)
16649
+ * @example
16650
+ * ```typescript
16651
+ * import { serviceSwapParamsSchema } from '@circle-fin/provider-stablecoin-service-swap'
16402
16652
  *
16403
- * Runtime validation in `isOkToken` determines if the token is supported
16404
- * for fee collection. This schema only validates the format.
16653
+ * const result = serviceSwapParamsSchema.safeParse(params)
16654
+ * if (!result.success) {
16655
+ * console.error('Invalid params:', result.error.issues)
16656
+ * }
16657
+ * ```
16405
16658
  */
16406
- let _symbolResult;
16407
- const swapTokenSchema = zod.z
16408
- .string()
16409
- .superRefine((value, ctx) => {
16410
- if (value.startsWith('0x')) {
16411
- if (!EVM_ADDRESS_REGEX.test(value)) {
16412
- ctx.addIssue({
16413
- code: zod.z.ZodIssueCode.custom,
16414
- message: `Invalid EVM token address format. Expected '0x' followed by 40 hexadecimal characters, but received: '${truncate(value)}'`,
16415
- });
16416
- }
16417
- return;
16418
- }
16419
- _symbolResult = supportedSwapTokenSchema.safeParse(value);
16420
- if (_symbolResult.success) {
16421
- return;
16422
- }
16423
- if (SOLANA_ADDRESS_REGEX.test(value)) {
16424
- return;
16425
- }
16426
- if (looksLikeSolanaAddress(value)) {
16427
- ctx.addIssue({
16428
- code: zod.z.ZodIssueCode.custom,
16429
- message: `Invalid Solana token address format. Expected 32-44 base58 characters, but received: '${truncate(value)}'`,
16430
- });
16431
- return;
16432
- }
16433
- ctx.addIssue({
16434
- code: zod.z.ZodIssueCode.custom,
16435
- message: `Unknown token symbol '${truncate(value)}'. Supported symbols include USDC, USDT, WETH, NATIVE, and others. See SDK documentation for the full list. You can also provide a token contract address (EVM: 0x... or Solana: base58).`,
16436
- });
16437
- })
16438
- .transform((value) => (_symbolResult?.success ? _symbolResult.data : value));
16439
- // Amount-in validation schema - broken out to reduce type complexity
16440
- const amountInSchema = zod.z
16441
- .string()
16442
- .min(1, 'Required')
16443
- .pipe(createDecimalStringValidator({
16444
- allowZero: false,
16445
- regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16446
- attributeName: 'amountIn',
16447
- maxDecimals: 18,
16448
- })(zod.z.string()))
16449
- .describe('The amount of the input token to swap, expressed as a human-readable ' +
16450
- "decimal string in token units (e.g., '0.05' for 0.05 USDC or 0.05 ETH).");
16659
+ const serviceSwapParamsSchema = zod.z.object({
16660
+ from: adapterContextSchema$2,
16661
+ tokenIn: zod.z
16662
+ .string({
16663
+ required_error: 'tokenIn is required',
16664
+ invalid_type_error: 'tokenIn must be a string',
16665
+ })
16666
+ .min(1, 'tokenIn must be a non-empty string'),
16667
+ tokenOut: zod.z
16668
+ .string({
16669
+ required_error: 'tokenOut is required',
16670
+ invalid_type_error: 'tokenOut must be a string',
16671
+ })
16672
+ .min(1, 'tokenOut must be a non-empty string'),
16673
+ amountIn: zod.z
16674
+ .string({
16675
+ required_error: 'amountIn is required',
16676
+ invalid_type_error: 'amountIn must be a string',
16677
+ })
16678
+ .min(1, 'amountIn is required')
16679
+ .regex(/^\d+$/, 'amountIn must be a numeric string in base units (e.g., "1000000")'),
16680
+ to: destinationAddressSchema,
16681
+ config: serviceSwapConfigSchema.optional(),
16682
+ });
16451
16683
  /**
16452
- * Schema for validating swap parameters with chain identifiers.
16453
- *
16454
- * This schema validates the complete swap operation input, ensuring:
16455
- * - Valid adapter context (adapter + chain + optional address)
16456
- * - Valid tokenIn and tokenOut (symbols or addresses)
16457
- * - Valid amountIn as a positive decimal string
16458
- * - Optional valid configuration
16459
- *
16460
- * The schema validates amounts with up to 18 decimal places to support
16461
- * various token standards.
16684
+ * Zod schema for provider-level custom fee configuration.
16462
16685
  *
16463
- * @example
16464
- * ```typescript
16465
- * import { swapParamsSchema } from '@circle-fin/swap-kit'
16686
+ * This schema validates the fee configuration for the provider constructor,
16687
+ * where both amount and recipientAddress are required fields.
16466
16688
  *
16467
- * // Using token symbols (recommended for supported tokens)
16468
- * const paramsWithSymbols = {
16469
- * from: {
16470
- * adapter: sourceAdapter,
16471
- * chain: 'Ethereum'
16472
- * },
16473
- * tokenIn: 'USDC',
16474
- * tokenOut: 'USDT',
16475
- * amountIn: '100.50', // 100.50 USDC
16476
- * config: {
16477
- * slippageBps: 300,
16478
- * allowanceStrategy: 'permit'
16479
- * }
16480
- * }
16689
+ * @remarks
16690
+ * This is different from the swap-level customFee schema imported from \@core/provider,
16691
+ * which has optional fields for per-swap fee overrides.
16692
+ */
16693
+ zod.z.object({
16694
+ amount: zod.z
16695
+ .string({
16696
+ required_error: 'customFee.amount is required',
16697
+ invalid_type_error: 'customFee.amount must be a string',
16698
+ })
16699
+ .min(1, 'customFee.amount must be a non-empty string')
16700
+ .regex(/^\d+$/, 'customFee.amount must be a numeric string (e.g., "1000000")')
16701
+ .refine((val) => {
16702
+ try {
16703
+ return BigInt(val) > 0n;
16704
+ }
16705
+ catch {
16706
+ return false;
16707
+ }
16708
+ }, {
16709
+ message: 'customFee.amount must be greater than 0',
16710
+ }),
16711
+ recipientAddress: zod.z
16712
+ .string({
16713
+ required_error: 'customFee.recipientAddress is required',
16714
+ invalid_type_error: 'customFee.recipientAddress must be a string',
16715
+ })
16716
+ .min(1, 'customFee.recipientAddress must be a non-empty string'),
16717
+ });
16718
+ /**
16719
+ * Fee amount schema for provider output.
16481
16720
  *
16482
- * // Using token addresses (works for any token)
16483
- * const paramsWithAddresses = {
16484
- * from: {
16485
- * adapter: sourceAdapter,
16486
- * chain: 'Base'
16487
- * },
16488
- * tokenIn: '0x4200000000000000000000000000000000000006', // WETH address
16489
- * tokenOut: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // Any token address
16490
- * amountIn: '0.1' // 0.1 WETH
16491
- * }
16721
+ * Accepts both integer strings ("1000000") and decimal strings ("0.002")
16722
+ * because the provider formats raw base-unit amounts into human-readable
16723
+ * decimals via `formatAmount()`.
16724
+ */
16725
+ const formattedFeeAmountSchema = zod.z
16726
+ .string({
16727
+ required_error: 'fee amount is required',
16728
+ invalid_type_error: 'fee amount must be a string',
16729
+ })
16730
+ .min(1, 'fee amount must be a non-empty string')
16731
+ .regex(/^\d+(\.\d+)?$/, 'fee amount must be a non-negative numeric string (e.g., "0.002" or "1000000")');
16732
+ /**
16733
+ * Zod schema for individual fee entry.
16492
16734
  *
16493
- * // Using native token alias
16494
- * const paramsWithNative = {
16495
- * from: {
16496
- * adapter: sourceAdapter,
16497
- * chain: 'Ethereum'
16498
- * },
16499
- * tokenIn: 'NATIVE', // ETH on Ethereum, MATIC on Polygon, etc.
16500
- * tokenOut: 'USDC',
16501
- * amountIn: '1.5' // 1.5 ETH
16502
- * }
16735
+ * Validates ServiceSwapFee structure. Fee amounts can be:
16736
+ * - A valid non-negative numeric string, integer or decimal (including "0")
16737
+ * - null (when fee information is not available)
16503
16738
  *
16504
- * const result = swapParamsSchema.safeParse(paramsWithSymbols)
16505
- * if (result.success) {
16506
- * console.log('Parameters are valid')
16507
- * } else {
16508
- * console.error('Validation failed:', result.error)
16509
- * }
16510
- * ```
16739
+ * @remarks
16740
+ * Zero fee amounts are valid and represent scenarios where no fee is charged.
16511
16741
  */
16512
- const swapParamsSchema = zod.z.object({
16513
- from: adapterContextSchema$2,
16514
- tokenIn: swapTokenSchema,
16515
- tokenOut: swapTokenSchema,
16516
- amountIn: amountInSchema,
16517
- config: swapConfigSchema.optional(),
16742
+ const serviceSwapFeeSchema = zod.z.object({
16743
+ token: zod.z
16744
+ .string({
16745
+ required_error: 'fee token is required',
16746
+ invalid_type_error: 'fee token must be a string',
16747
+ })
16748
+ .min(1, 'fee token must be a non-empty string'),
16749
+ amount: zod.z.union([formattedFeeAmountSchema, zod.z.null()]),
16750
+ type: zod.z.enum(['provider', 'swap', 'gas', 'developer']),
16751
+ recipientAddress: zod.z.string().min(1).optional(),
16518
16752
  });
16519
16753
  /**
16520
- * Schema for validating SwapKit custom fee policy.
16521
- *
16522
- * Validates the shape of CustomFeePolicy, which lets SDK consumers
16523
- * provide custom fee calculation and fee-recipient resolution logic.
16754
+ * Zod schema for validating ServiceSwapResponse data.
16524
16755
  *
16525
- * - computeFee: required function that returns a fee as a string (or Promise<string>).
16526
- * - resolveFeeRecipientAddress: required function that returns a recipient address as a
16527
- * string (or Promise<string>).
16756
+ * This schema validates the estimate response from the Stablecoin Service swap API,
16757
+ * ensuring the estimate output data is properly formatted.
16528
16758
  *
16529
- * This schema only ensures the presence and return types of the functions; it
16530
- * does not validate their argument types.
16759
+ * @remarks
16760
+ * Aligned with CCTP provider pattern - validates only computed response data,
16761
+ * not request parameter echoes.
16531
16762
  *
16532
16763
  * @example
16533
16764
  * ```typescript
16534
- * import { customFeePolicySchema } from '@circle-fin/swap-kit'
16765
+ * import { serviceSwapResponseSchema } from '@circle-fin/provider-stablecoin-service-swap'
16535
16766
  *
16536
- * const config = {
16537
- * computeFee: async () => '0.1',
16538
- * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
16767
+ * const result = serviceSwapResponseSchema.safeParse(responseData)
16768
+ * if (!result.success) {
16769
+ * console.error('Invalid response:', result.error.issues)
16539
16770
  * }
16540
- *
16541
- * const result = customFeePolicySchema.safeParse(config)
16542
- * // result.success === true
16543
16771
  * ```
16544
16772
  */
16545
- const customFeePolicySchema = zod.z
16773
+ zod.z
16546
16774
  .object({
16547
- computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
16548
- resolveFeeRecipientAddress: zod.z
16549
- .function()
16550
- .returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
16551
- })
16552
- .strict();
16553
-
16554
- const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
16555
- /**
16556
- * Assert that the provided value conforms to {@link CustomFeePolicy}.
16557
- *
16558
- * This function validates the custom fee policy configuration using the
16559
- * customFeePolicySchema. It ensures that both required functions
16560
- * (computeFee and resolveFeeRecipientAddress) are present and have
16561
- * the correct return types.
16562
- *
16563
- * Throws a structured error with detailed validation messages if the
16564
- * configuration is malformed. Uses state tracking to avoid duplicate
16565
- * validations.
16566
- *
16567
- * @param config - The custom fee policy to validate
16568
- * @throws \{KitError\} If the policy fails validation
16569
- *
16570
- * @example
16571
- * ```typescript
16572
- * import { assertCustomFeePolicy } from '@circle-fin/swap-kit'
16573
- *
16574
- * const config = {
16575
- * computeFee: () => '0.1',
16576
- * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
16577
- * }
16578
- *
16579
- * try {
16580
- * assertCustomFeePolicy(config)
16581
- * // If no error is thrown, `config` is a valid CustomFeePolicy
16582
- * } catch (error) {
16583
- * console.error('Invalid fee policy:', error.message)
16584
- * }
16585
- * ```
16586
- */
16587
- function assertCustomFeePolicy(config) {
16588
- // Use validateWithStateTracking to avoid duplicate validations
16589
- // This will skip validation if already validated by this function
16590
- // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
16591
- validateWithStateTracking(config, customFeePolicySchema, 'SwapKit custom fee policy', assertCustomFeePolicySymbol);
16592
- }
16775
+ stopLimit: zod.z.object({
16776
+ token: zod.z
16777
+ .string({
16778
+ required_error: 'stopLimit.token is required',
16779
+ invalid_type_error: 'stopLimit.token must be a string',
16780
+ })
16781
+ .min(1, 'stopLimit.token must be a non-empty string'),
16782
+ amount: zod.z
16783
+ .string({
16784
+ required_error: 'stopLimit.amount is required',
16785
+ invalid_type_error: 'stopLimit.amount must be a string',
16786
+ })
16787
+ .min(1, 'stopLimit.amount must be a non-empty string'),
16788
+ }, {
16789
+ required_error: 'stopLimit is required',
16790
+ invalid_type_error: 'stopLimit must be an object',
16791
+ }),
16792
+ estimatedOutput: zod.z.object({
16793
+ token: zod.z
16794
+ .string({
16795
+ required_error: 'estimatedOutput.token is required',
16796
+ invalid_type_error: 'estimatedOutput.token must be a string',
16797
+ })
16798
+ .min(1, 'estimatedOutput.token must be a non-empty string'),
16799
+ amount: zod.z
16800
+ .string({
16801
+ required_error: 'estimatedOutput.amount is required',
16802
+ invalid_type_error: 'estimatedOutput.amount must be a string',
16803
+ })
16804
+ .min(1, 'estimatedOutput.amount must be a non-empty string'),
16805
+ }, {
16806
+ required_error: 'estimatedOutput is required',
16807
+ invalid_type_error: 'estimatedOutput must be an object',
16808
+ }),
16809
+ fees: zod.z.array(serviceSwapFeeSchema).optional(),
16810
+ })
16811
+ .passthrough();
16593
16812
 
16594
16813
  /**
16595
- * Symbol used to track that assertSwapParams has validated an object.
16596
- * @internal
16814
+ * @packageDocumentation
16815
+ * @module StablecoinServiceSwapValidation
16816
+ *
16817
+ * Validation utilities for the Stablecoin Service Swap Provider.
16818
+ *
16819
+ * This module provides runtime validation functions and type guards
16820
+ * to ensure swap parameters and service responses conform to expected formats.
16597
16821
  */
16598
- const ASSERT_SWAP_PARAMS_SYMBOL = Symbol('assertSwapParams');
16599
16822
  /**
16600
- * Assert that the provided value conforms to the SwapParams schema.
16601
- *
16602
- * This function validates swap parameters using the provided Zod schema
16603
- * and tracks validation state to avoid duplicate checks. It performs
16604
- * comprehensive validation including:
16605
- * - Adapter context structure
16606
- * - Token specifications
16607
- * - Amount format and range
16608
- * - Optional configuration values
16823
+ * Validates ServiceSwapParams and throws an error if invalid.
16609
16824
  *
16610
- * Throws a structured error with detailed validation messages if
16611
- * any parameter is invalid.
16825
+ * This function performs strict validation and throws a detailed error
16826
+ * if the parameters do not conform to the expected schema. Use this for
16827
+ * validating input parameters to swap operations.
16612
16828
  *
16613
- * @typeParam T - The expected type after validation
16614
16829
  * @param params - The swap parameters to validate
16615
- * @param schema - The Zod schema to validate against
16616
- * @throws \{KitError\} If the parameters fail validation
16830
+ * @throws \{KitError\} If validation fails, with details about validation errors
16617
16831
  *
16618
16832
  * @example
16619
16833
  * ```typescript
16620
- * import { assertSwapParams, swapParamsSchema } from '@circle-fin/swap-kit'
16621
- *
16622
- * const params = {
16623
- * from: { adapter: sourceAdapter, chain: 'Ethereum' },
16624
- * tokenIn: 'USDC',
16625
- * tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
16626
- * amountIn: '100.50'
16627
- * }
16834
+ * import { validateServiceSwapParams } from '@circle-fin/provider-stablecoin-service-swap'
16628
16835
  *
16629
16836
  * try {
16630
- * assertSwapParams(params, swapParamsSchema)
16631
- * // Parameters are valid, proceed with swap
16837
+ * validateServiceSwapParams(params)
16838
+ * // Proceed with swap
16632
16839
  * } catch (error) {
16633
- * console.error('Invalid parameters:', error.message)
16840
+ * console.error('Invalid swap params:', error.message)
16634
16841
  * }
16635
16842
  * ```
16636
16843
  */
16637
- function assertSwapParams(params, schema) {
16638
- // Use validateWithStateTracking to avoid duplicate validations
16639
- // This will skip validation if already validated by this function
16640
- // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
16641
- validateWithStateTracking(params, schema, 'swap parameters', ASSERT_SWAP_PARAMS_SYMBOL);
16642
- }
16844
+ const validateServiceSwapParams = (params) => {
16845
+ const result = serviceSwapParamsSchema.safeParse(params);
16846
+ if (!result.success) {
16847
+ const errors = result.error.issues.map((issue) => issue.message).join(', ');
16848
+ throw new KitError({
16849
+ ...InputError.VALIDATION_FAILED,
16850
+ recoverability: 'FATAL',
16851
+ message: `Invalid ServiceSwapParams: ${errors}.`,
16852
+ cause: {
16853
+ trace: { params, validationErrors: result.error.issues },
16854
+ },
16855
+ });
16856
+ }
16857
+ };
16643
16858
 
16644
16859
  /**
16645
16860
  * @packageDocumentation
16646
- * @module StablecoinServiceSwapSchemas
16647
- *
16648
- * Zod validation schemas for Stablecoin Service Swap Provider parameters.
16861
+ * @module ServiceClientConstants
16649
16862
  *
16650
- * This module defines runtime validation schemas using Zod for type-safe
16651
- * validation of swap requests and service responses.
16652
- */
16653
- /**
16654
- * Schema for destination address (to): must be a valid EVM or Solana address.
16655
- * Catches obviously malformed addresses at parse time; chain-specific validation
16656
- * is performed in buildServiceParams.
16863
+ * Constants for the Stablecoin Service API.
16657
16864
  */
16658
- const destinationAddressSchema = zod.z.union([
16659
- evmAddressSchema,
16660
- solanaAddressSchema,
16661
- ]);
16662
16865
  /**
16663
- * Zod schema for allowance strategy.
16866
+ * Default configuration values for the quote fetcher.
16664
16867
  *
16665
- * Validates the allowance strategy for token approvals.
16666
- */
16667
- const allowanceStrategySchema = zod.z.enum(['permit', 'approve'], {
16668
- invalid_type_error: 'allowanceStrategy must be either "permit" or "approve"',
16669
- });
16670
- /**
16671
- * Schema for validating service swap custom fee configuration.
16868
+ * @remarks
16869
+ * The timeout is set to 30 seconds to match the load balancer timeout.
16870
+ * This accommodates the LiFi DEX aggregator (5s hard cap), Circle Wallets
16871
+ * signing (~500ms p99), internal overhead (~100ms), and network latency.
16672
16872
  *
16673
- * Supports SwapKit fee approaches:
16674
- * - Percentage-based: percentageBps + recipientAddress
16675
- * - Callback-based: amount + recipientAddress
16873
+ * @internal
16676
16874
  */
16677
- const serviceSwapCustomFeeSchema = zod.z
16678
- .object({
16679
- percentageBps: zod.z.number().int().positive().max(10000).optional(),
16680
- amount: zod.z.string().optional(),
16681
- recipientAddress: zod.z.string().min(1).optional(),
16682
- })
16683
- .strict();
16875
+ const DEFAULT_CONFIG = {
16876
+ timeout: 30_000, // 30 seconds - matches load balancer timeout
16877
+ maxRetries: 3, // 3 retries as per requirements
16878
+ retryDelay: 200, // 200ms between retries
16879
+ headers: {
16880
+ 'Content-Type': 'application/json',
16881
+ },
16882
+ };
16684
16883
  /**
16685
- * Zod schema for swap configuration.
16884
+ * Base URL for the Stablecoin Service API.
16686
16885
  *
16687
- * Validates the optional configuration object for swap operations.
16688
- * Uses shared validation utilities from \@core/provider for consistency.
16886
+ * @internal
16689
16887
  */
16690
- const serviceSwapConfigSchema = zod.z.object({
16691
- allowanceStrategy: allowanceStrategySchema.optional(),
16692
- slippageBps: zod.z
16693
- .number({
16694
- invalid_type_error: 'slippageBps must be a number',
16695
- })
16696
- .int('slippageBps must be an integer')
16697
- .min(0, 'slippageBps must be non-negative')
16698
- .max(10000, 'slippageBps must be at most 10000 (100%)')
16699
- .optional(),
16700
- stopLimit: zod.z
16701
- .string({
16702
- invalid_type_error: 'stopLimit must be a string',
16703
- })
16704
- .min(1, 'stopLimit is required when provided')
16705
- .regex(/^\d+$/, 'stopLimit must be an integer string in base units (e.g., "50000000" for 50 USDC with 6 decimals)')
16706
- .refine((val) => {
16707
- try {
16708
- return BigInt(val) > 0n;
16709
- }
16710
- catch {
16711
- return false;
16712
- }
16713
- }, {
16714
- message: 'stopLimit must be greater than 0',
16715
- })
16716
- .optional(),
16717
- customFee: serviceSwapCustomFeeSchema.optional(),
16718
- kitKey: zod.z
16719
- .string({
16720
- invalid_type_error: 'kitKey must be a string',
16721
- })
16722
- .min(1, 'kitKey must be a non-empty string')
16723
- .optional(),
16724
- provider: zod.z
16725
- .string({
16726
- invalid_type_error: 'provider must be a string',
16727
- })
16728
- .min(1, 'provider must be a non-empty string')
16729
- .optional(),
16730
- });
16888
+ const STABLECOIN_SERVICE_BASE_URL = 'https://api.circle.com';
16889
+
16731
16890
  /**
16732
- * Zod schema for adapter context.
16891
+ * @packageDocumentation
16892
+ * @module ServiceClientSchemas
16733
16893
  *
16734
- * Validates the adapter context which contains the adapter and chain.
16894
+ * Zod validation schemas for Stablecoin Service API parameters.
16735
16895
  *
16736
- * @remarks
16737
- * Optionally includes address for developer-controlled adapters.
16896
+ * This module defines runtime validation schemas using Zod for type-safe
16897
+ * validation of API requests and responses.
16738
16898
  */
16739
- const adapterContextSchema$1 = zod.z.object({
16740
- adapter: adapterSchema$1,
16741
- chain: chainIdentifierSchema,
16742
- address: zod.z.string().optional(),
16743
- });
16744
16899
  /**
16745
- * Zod schema for ServiceSwapParams.
16900
+ * Zod schema for validating stop limits.
16746
16901
  *
16747
- * Validates the input parameters for swap operations including
16748
- * adapter context, tokens, amount, destination, and optional configuration.
16902
+ * The stop limit is the minimum acceptable token output amount expressed in
16903
+ * base units. It must be a positive integer string.
16749
16904
  *
16750
16905
  * @example
16751
16906
  * ```typescript
16752
- * import { serviceSwapParamsSchema } from '@circle-fin/provider-stablecoin-service-swap'
16907
+ * import { stopLimitSchema } from '@core/service-client'
16753
16908
  *
16754
- * const result = serviceSwapParamsSchema.safeParse(params)
16755
- * if (!result.success) {
16756
- * console.error('Invalid params:', result.error.issues)
16757
- * }
16758
- * ```
16759
- */
16760
- const serviceSwapParamsSchema = zod.z.object({
16761
- from: adapterContextSchema$1,
16762
- tokenIn: zod.z
16763
- .string({
16764
- required_error: 'tokenIn is required',
16765
- invalid_type_error: 'tokenIn must be a string',
16766
- })
16767
- .min(1, 'tokenIn must be a non-empty string'),
16768
- tokenOut: zod.z
16769
- .string({
16770
- required_error: 'tokenOut is required',
16771
- invalid_type_error: 'tokenOut must be a string',
16772
- })
16773
- .min(1, 'tokenOut must be a non-empty string'),
16774
- amountIn: zod.z
16775
- .string({
16776
- required_error: 'amountIn is required',
16777
- invalid_type_error: 'amountIn must be a string',
16778
- })
16779
- .min(1, 'amountIn is required')
16780
- .regex(/^\d+$/, 'amountIn must be a numeric string in base units (e.g., "1000000")'),
16781
- to: destinationAddressSchema,
16782
- config: serviceSwapConfigSchema.optional(),
16783
- });
16784
- /**
16785
- * Zod schema for provider-level custom fee configuration.
16786
- *
16787
- * This schema validates the fee configuration for the provider constructor,
16788
- * where both amount and recipientAddress are required fields.
16789
- *
16790
- * @remarks
16791
- * This is different from the swap-level customFee schema imported from \@core/provider,
16792
- * which has optional fields for per-swap fee overrides.
16793
- */
16794
- zod.z.object({
16795
- amount: zod.z
16796
- .string({
16797
- required_error: 'customFee.amount is required',
16798
- invalid_type_error: 'customFee.amount must be a string',
16799
- })
16800
- .min(1, 'customFee.amount must be a non-empty string')
16801
- .regex(/^\d+$/, 'customFee.amount must be a numeric string (e.g., "1000000")')
16802
- .refine((val) => {
16803
- try {
16804
- return BigInt(val) > 0n;
16805
- }
16806
- catch {
16807
- return false;
16808
- }
16809
- }, {
16810
- message: 'customFee.amount must be greater than 0',
16811
- }),
16812
- recipientAddress: zod.z
16813
- .string({
16814
- required_error: 'customFee.recipientAddress is required',
16815
- invalid_type_error: 'customFee.recipientAddress must be a string',
16816
- })
16817
- .min(1, 'customFee.recipientAddress must be a non-empty string'),
16818
- });
16819
- /**
16820
- * Fee amount schema for provider output.
16821
- *
16822
- * Accepts both integer strings ("1000000") and decimal strings ("0.002")
16823
- * because the provider formats raw base-unit amounts into human-readable
16824
- * decimals via `formatAmount()`.
16825
- */
16826
- const formattedFeeAmountSchema = zod.z
16827
- .string({
16828
- required_error: 'fee amount is required',
16829
- invalid_type_error: 'fee amount must be a string',
16830
- })
16831
- .min(1, 'fee amount must be a non-empty string')
16832
- .regex(/^\d+(\.\d+)?$/, 'fee amount must be a non-negative numeric string (e.g., "0.002" or "1000000")');
16833
- /**
16834
- * Zod schema for individual fee entry.
16835
- *
16836
- * Validates ServiceSwapFee structure. Fee amounts can be:
16837
- * - A valid non-negative numeric string, integer or decimal (including "0")
16838
- * - null (when fee information is not available)
16839
- *
16840
- * @remarks
16841
- * Zero fee amounts are valid and represent scenarios where no fee is charged.
16842
- */
16843
- const serviceSwapFeeSchema = zod.z.object({
16844
- token: zod.z
16845
- .string({
16846
- required_error: 'fee token is required',
16847
- invalid_type_error: 'fee token must be a string',
16848
- })
16849
- .min(1, 'fee token must be a non-empty string'),
16850
- amount: zod.z.union([formattedFeeAmountSchema, zod.z.null()]),
16851
- type: zod.z.enum(['provider', 'swap', 'gas', 'developer']),
16852
- recipientAddress: zod.z.string().min(1).optional(),
16853
- });
16854
- /**
16855
- * Zod schema for validating ServiceSwapResponse data.
16856
- *
16857
- * This schema validates the estimate response from the Stablecoin Service swap API,
16858
- * ensuring the estimate output data is properly formatted.
16859
- *
16860
- * @remarks
16861
- * Aligned with CCTP provider pattern - validates only computed response data,
16862
- * not request parameter echoes.
16863
- *
16864
- * @example
16865
- * ```typescript
16866
- * import { serviceSwapResponseSchema } from '@circle-fin/provider-stablecoin-service-swap'
16867
- *
16868
- * const result = serviceSwapResponseSchema.safeParse(responseData)
16869
- * if (!result.success) {
16870
- * console.error('Invalid response:', result.error.issues)
16871
- * }
16872
- * ```
16873
- */
16874
- zod.z
16875
- .object({
16876
- stopLimit: zod.z.object({
16877
- token: zod.z
16878
- .string({
16879
- required_error: 'stopLimit.token is required',
16880
- invalid_type_error: 'stopLimit.token must be a string',
16881
- })
16882
- .min(1, 'stopLimit.token must be a non-empty string'),
16883
- amount: zod.z
16884
- .string({
16885
- required_error: 'stopLimit.amount is required',
16886
- invalid_type_error: 'stopLimit.amount must be a string',
16887
- })
16888
- .min(1, 'stopLimit.amount must be a non-empty string'),
16889
- }, {
16890
- required_error: 'stopLimit is required',
16891
- invalid_type_error: 'stopLimit must be an object',
16892
- }),
16893
- estimatedOutput: zod.z.object({
16894
- token: zod.z
16895
- .string({
16896
- required_error: 'estimatedOutput.token is required',
16897
- invalid_type_error: 'estimatedOutput.token must be a string',
16898
- })
16899
- .min(1, 'estimatedOutput.token must be a non-empty string'),
16900
- amount: zod.z
16901
- .string({
16902
- required_error: 'estimatedOutput.amount is required',
16903
- invalid_type_error: 'estimatedOutput.amount must be a string',
16904
- })
16905
- .min(1, 'estimatedOutput.amount must be a non-empty string'),
16906
- }, {
16907
- required_error: 'estimatedOutput is required',
16908
- invalid_type_error: 'estimatedOutput must be an object',
16909
- }),
16910
- fees: zod.z.array(serviceSwapFeeSchema).optional(),
16911
- })
16912
- .passthrough();
16913
-
16914
- /**
16915
- * @packageDocumentation
16916
- * @module StablecoinServiceSwapValidation
16917
- *
16918
- * Validation utilities for the Stablecoin Service Swap Provider.
16919
- *
16920
- * This module provides runtime validation functions and type guards
16921
- * to ensure swap parameters and service responses conform to expected formats.
16922
- */
16923
- /**
16924
- * Validates ServiceSwapParams and throws an error if invalid.
16925
- *
16926
- * This function performs strict validation and throws a detailed error
16927
- * if the parameters do not conform to the expected schema. Use this for
16928
- * validating input parameters to swap operations.
16929
- *
16930
- * @param params - The swap parameters to validate
16931
- * @throws \{KitError\} If validation fails, with details about validation errors
16932
- *
16933
- * @example
16934
- * ```typescript
16935
- * import { validateServiceSwapParams } from '@circle-fin/provider-stablecoin-service-swap'
16936
- *
16937
- * try {
16938
- * validateServiceSwapParams(params)
16939
- * // Proceed with swap
16940
- * } catch (error) {
16941
- * console.error('Invalid swap params:', error.message)
16942
- * }
16943
- * ```
16944
- */
16945
- const validateServiceSwapParams = (params) => {
16946
- const result = serviceSwapParamsSchema.safeParse(params);
16947
- if (!result.success) {
16948
- const errors = result.error.issues.map((issue) => issue.message).join(', ');
16949
- throw new KitError({
16950
- ...InputError.VALIDATION_FAILED,
16951
- recoverability: 'FATAL',
16952
- message: `Invalid ServiceSwapParams: ${errors}.`,
16953
- cause: {
16954
- trace: { params, validationErrors: result.error.issues },
16955
- },
16956
- });
16957
- }
16958
- };
16959
-
16960
- /**
16961
- * @packageDocumentation
16962
- * @module ServiceClientConstants
16963
- *
16964
- * Constants for the Stablecoin Service API.
16965
- */
16966
- /**
16967
- * Default configuration values for the quote fetcher.
16968
- *
16969
- * @remarks
16970
- * The timeout is set to 30 seconds to match the load balancer timeout.
16971
- * This accommodates the LiFi DEX aggregator (5s hard cap), Circle Wallets
16972
- * signing (~500ms p99), internal overhead (~100ms), and network latency.
16973
- *
16974
- * @internal
16975
- */
16976
- const DEFAULT_CONFIG = {
16977
- timeout: 30_000, // 30 seconds - matches load balancer timeout
16978
- maxRetries: 3, // 3 retries as per requirements
16979
- retryDelay: 200, // 200ms between retries
16980
- headers: {
16981
- 'Content-Type': 'application/json',
16982
- },
16983
- };
16984
- /**
16985
- * Base URL for the Stablecoin Service API.
16986
- *
16987
- * @internal
16988
- */
16989
- const STABLECOIN_SERVICE_BASE_URL = 'https://api.circle.com';
16990
-
16991
- /**
16992
- * @packageDocumentation
16993
- * @module ServiceClientSchemas
16994
- *
16995
- * Zod validation schemas for Stablecoin Service API parameters.
16996
- *
16997
- * This module defines runtime validation schemas using Zod for type-safe
16998
- * validation of API requests and responses.
16999
- */
17000
- /**
17001
- * Zod schema for validating stop limits.
17002
- *
17003
- * The stop limit is the minimum acceptable token output amount expressed in
17004
- * base units. It must be a positive integer string.
17005
- *
17006
- * @example
17007
- * ```typescript
17008
- * import { stopLimitSchema } from '@core/service-client'
17009
- *
17010
- * const result = stopLimitSchema.safeParse('1000000')
16909
+ * const result = stopLimitSchema.safeParse('1000000')
17011
16910
  * if (!result.success) {
17012
16911
  * console.error('Validation failed:', result.error.issues)
17013
16912
  * }
@@ -17418,7 +17317,32 @@ const createSwapParamsSchema = createSwapRequestSchema.extend({
17418
17317
  apiKey: apiKeySchema,
17419
17318
  });
17420
17319
  /**
17421
- * Zod schema for validating CreateSwapResponse payloads.
17320
+ * Zod schema for validating GetSwapStatusResponse data.
17321
+ */
17322
+ const getSwapStatusResponseSchema = zod.z.object({
17323
+ status: zod.z.enum(['DONE', 'PENDING', 'NOT_FOUND', 'FAILED']),
17324
+ amountOut: zod.z.string().optional(),
17325
+ });
17326
+ /**
17327
+ * Zod schema for validating GetSwapStatusParams.
17328
+ */
17329
+ const getSwapStatusParamsSchema = zod.z.object({
17330
+ txHash: zod.z
17331
+ .string({
17332
+ required_error: 'txHash is required',
17333
+ invalid_type_error: 'txHash must be a string',
17334
+ })
17335
+ .min(1, 'txHash is required and must be a non-empty string'),
17336
+ chain: zod.z
17337
+ .string({
17338
+ required_error: 'chain is required',
17339
+ invalid_type_error: 'chain must be a string',
17340
+ })
17341
+ .min(1, 'chain is required and must be a non-empty string'),
17342
+ apiKey: apiKeySchema,
17343
+ });
17344
+ /**
17345
+ * Zod schema for validating CreateSwapResponse payloads.
17422
17346
  */
17423
17347
  const createSwapFeeItemSchema = zod.z.object({
17424
17348
  token: zod.z
@@ -17592,6 +17516,27 @@ const isCreateSwapResponse = (obj) => createSwapResponseSchema.safeParse(obj).su
17592
17516
  * @throws If validation fails.
17593
17517
  */
17594
17518
  const parseCreateSwapResponse = (obj) => createSwapResponseSchema.parse(obj);
17519
+ /**
17520
+ * Type guard to validate GetSwapStatusResponse objects.
17521
+ *
17522
+ * @param obj - The object to validate
17523
+ * @returns True if the object is a valid GetSwapStatusResponse, false otherwise
17524
+ *
17525
+ * @example
17526
+ * ```typescript
17527
+ * import { isGetSwapStatusResponse } from '@core/service-client'
17528
+ *
17529
+ * const response = await fetch('/v1/stablecoinKits/swap/status?...')
17530
+ * const data = await response.json()
17531
+ *
17532
+ * if (isGetSwapStatusResponse(data)) {
17533
+ * console.log('Swap status:', data.status)
17534
+ * } else {
17535
+ * console.error('Invalid response format')
17536
+ * }
17537
+ * ```
17538
+ */
17539
+ const isGetSwapStatusResponse = (obj) => getSwapStatusResponseSchema.safeParse(obj).success;
17595
17540
 
17596
17541
  /**
17597
17542
  * @packageDocumentation
@@ -17826,6 +17771,61 @@ const getQuote = async (params) => {
17826
17771
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
17827
17772
  };
17828
17773
 
17774
+ /**
17775
+ * Build the full URL for the swap-status polling endpoint.
17776
+ *
17777
+ * @param params - Swap status parameters containing the transaction
17778
+ * hash and chain identifier.
17779
+ * @returns Fully-qualified URL string with query parameters set.
17780
+ *
17781
+ * @example
17782
+ * ```typescript
17783
+ * import { buildSwapStatusUrl } from '@core/service-client'
17784
+ *
17785
+ * const url = buildSwapStatusUrl({
17786
+ * txHash: '0xabc123',
17787
+ * chain: 'Ethereum',
17788
+ * apiKey: 'my-api-key',
17789
+ * })
17790
+ * // => 'https://…/v1/stablecoinKits/swap/status?txHash=0xabc123&chain=Ethereum'
17791
+ * ```
17792
+ */
17793
+ const buildSwapStatusUrl = (params) => {
17794
+ const url = new URL('/v1/stablecoinKits/swap/status', STABLECOIN_SERVICE_BASE_URL);
17795
+ url.searchParams.set('txHash', params.txHash);
17796
+ url.searchParams.set('chain', params.chain);
17797
+ return url.toString();
17798
+ };
17799
+ /**
17800
+ * Fetches the swap status from the Stablecoin Service.
17801
+ *
17802
+ * The proxy resolves the final swap status server-side (up to 30s) and
17803
+ * returns a normalized response.
17804
+ *
17805
+ * @param params - txHash, chain, and apiKey
17806
+ * @returns The swap status with optional amountOut when DONE
17807
+ * @throws KitError if parameter validation fails
17808
+ * @throws KitError if the API returns an error response
17809
+ */
17810
+ const getSwapStatus = async (params) => {
17811
+ const result = getSwapStatusParamsSchema.safeParse(params);
17812
+ if (!result.success) {
17813
+ throw convertZodErrorToStructured(result.error, {
17814
+ txHash: params.txHash,
17815
+ chain: params.chain,
17816
+ });
17817
+ }
17818
+ const url = buildSwapStatusUrl(result.data);
17819
+ const effectiveConfig = {
17820
+ ...DEFAULT_CONFIG,
17821
+ headers: {
17822
+ ...DEFAULT_CONFIG.headers,
17823
+ Authorization: `Bearer ${result.data.apiKey}`,
17824
+ },
17825
+ };
17826
+ return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
17827
+ };
17828
+
17829
17829
  /**
17830
17830
  * Core type definitions for EVM-compatible blockchain transaction execution
17831
17831
  * and gas estimation.
@@ -19429,9 +19429,9 @@ const EIP2612_SUPPORTED_TOKENS = new Set(['USDC']);
19429
19429
  * cast call <USDC_ADDRESS> "version()(string)" --rpc-url <RPC_URL>
19430
19430
  * ```
19431
19431
  *
19432
- * Only swap-supported EVM mainnet chains are included. Solana is excluded since
19433
- * it doesn't use EIP-712 signatures. Values were verified on-chain via
19434
- * `cast call <usdc> "name()(string)"`.
19432
+ * All swap-supported EVM chains (mainnet and whitelisted testnets) are included.
19433
+ * Solana is excluded since it doesn't use EIP-712 signatures. Values were
19434
+ * verified on-chain via `cast call <usdc> "name()(string)"`.
19435
19435
  *
19436
19436
  * @internal
19437
19437
  */
@@ -19452,6 +19452,11 @@ const USDC_PERMIT_METADATA_BY_NAME = {
19452
19452
  XDC: { chainId: XDC.chainId, name: 'USDC', version: '2' },
19453
19453
  HyperEVM: { chainId: HyperEVM.chainId, name: 'USDC', version: '2' },
19454
19454
  Monad: { chainId: Monad.chainId, name: 'USDC', version: '2' },
19455
+ Arc_Testnet: {
19456
+ chainId: ArcTestnet.chainId,
19457
+ name: 'USDC',
19458
+ version: '2',
19459
+ },
19455
19460
  };
19456
19461
  /**
19457
19462
  * Flatten {@link USDC_PERMIT_METADATA_BY_NAME} into a chain-ID-keyed map for O(1) lookup.
@@ -19482,7 +19487,7 @@ const USDC_PERMIT_METADATA = Object.values(USDC_PERMIT_METADATA_BY_NAME).reduce(
19482
19487
  * **Unsupported Scenarios**:
19483
19488
  * - Returns `false` for native currency (ETH, MATIC, etc.)
19484
19489
  * - Returns `false` for non-EVM chains (e.g., Solana)
19485
- * - Returns `false` for chains not in USDC_PERMIT_METADATA (testnets, non-swap chains)
19490
+ * - Returns `false` for chains not in USDC_PERMIT_METADATA (e.g. non-whitelisted testnets, non-swap chains)
19486
19491
  * - Returns `false` for chains without USDC deployed (`chain.usdcAddress === null`)
19487
19492
  * - Returns `false` for tokens not in allowlist
19488
19493
  *
@@ -20764,165 +20769,6 @@ async function formatTokenValue(value, token, chain, adapter) {
20764
20769
  }
20765
20770
  }
20766
20771
 
20767
- const LIFI_STATUS_BASE_URL = 'https://li.quest/v1/status';
20768
- const LIFI_SOLANA_CHAIN_ID = 1151111081099710;
20769
- const LIFI_POLLING_CONFIG = {
20770
- maxRetries: 20,
20771
- retryDelay: 3_000,
20772
- };
20773
- const assertNever = (value) => {
20774
- throw new Error(`Unexpected LI.FI status value: ${String(value)}`);
20775
- };
20776
- const createRetryableLifiStatusError = (status, substatus) => {
20777
- return new KitError({
20778
- ...NetworkError.LIFI_STATUS_PENDING,
20779
- recoverability: 'RETRYABLE',
20780
- message: substatus
20781
- ? `LI.FI status is not ready yet: ${status} (${substatus})`
20782
- : `LI.FI status is not ready yet: ${status}`,
20783
- cause: {
20784
- trace: { status, ...(substatus !== undefined && { substatus }) },
20785
- },
20786
- });
20787
- };
20788
- const createFatalLifiStatusError = (message, trace) => {
20789
- return new KitError({
20790
- ...NetworkError.LIFI_STATUS_FAILED,
20791
- recoverability: 'FATAL',
20792
- message,
20793
- cause: { trace },
20794
- });
20795
- };
20796
- const hasValidReceivingStructure = (receiving) => {
20797
- return (receiving === undefined ||
20798
- (typeof receiving === 'object' &&
20799
- receiving !== null &&
20800
- (!('amount' in receiving) ||
20801
- typeof receiving.amount === 'string')));
20802
- };
20803
- const hasValidLifiStatusStructure = (obj) => {
20804
- return (typeof obj === 'object' &&
20805
- obj !== null &&
20806
- 'status' in obj &&
20807
- ['NOT_FOUND', 'INVALID', 'PENDING', 'DONE', 'FAILED'].includes(String(obj.status)) &&
20808
- (!('substatus' in obj) ||
20809
- typeof obj.substatus === 'string') &&
20810
- hasValidReceivingStructure(obj.receiving));
20811
- };
20812
- const getLifiChainId = (chain) => {
20813
- if (chain.type === 'evm') {
20814
- return chain.chainId;
20815
- }
20816
- if (chain.type === 'solana') {
20817
- return LIFI_SOLANA_CHAIN_ID;
20818
- }
20819
- throw new KitError({
20820
- ...InputError.VALIDATION_FAILED,
20821
- recoverability: 'FATAL',
20822
- message: `LI.FI status polling is not supported for chain type "${chain.type}".`,
20823
- cause: {
20824
- trace: { chain: chain.name, chainType: chain.type },
20825
- },
20826
- });
20827
- };
20828
- const buildLifiStatusUrl = (txHash, lifiChainId) => {
20829
- const url = new URL(LIFI_STATUS_BASE_URL);
20830
- url.searchParams.set('txHash', txHash);
20831
- url.searchParams.set('fromChain', lifiChainId.toString());
20832
- url.searchParams.set('toChain', lifiChainId.toString());
20833
- return url.toString();
20834
- };
20835
- const isLifiStatusDone = (obj) => {
20836
- if (!hasValidLifiStatusStructure(obj)) {
20837
- throw new KitError({
20838
- ...InputError.VALIDATION_FAILED,
20839
- recoverability: 'FATAL',
20840
- message: 'Invalid LI.FI status response structure',
20841
- cause: { trace: obj },
20842
- });
20843
- }
20844
- switch (obj.status) {
20845
- case 'PENDING':
20846
- case 'NOT_FOUND':
20847
- throw createRetryableLifiStatusError(obj.status, obj.substatus);
20848
- case 'FAILED':
20849
- case 'INVALID':
20850
- throw createFatalLifiStatusError(`LI.FI status returned a terminal failure state: ${obj.status}`, {
20851
- status: obj.status,
20852
- ...(obj.substatus !== undefined && { substatus: obj.substatus }),
20853
- });
20854
- case 'DONE':
20855
- if (obj.substatus === 'COMPLETED') {
20856
- return true;
20857
- }
20858
- throw createFatalLifiStatusError(`LI.FI status returned a non-completable terminal state: DONE${obj.substatus ? ` (${obj.substatus})` : ''}`, {
20859
- status: obj.status,
20860
- ...(obj.substatus !== undefined && { substatus: obj.substatus }),
20861
- });
20862
- default:
20863
- return assertNever(obj.status);
20864
- }
20865
- };
20866
- const LIFI_TOTAL_TIMEOUT_MS = 90_000;
20867
- const isLifiNotIndexedError = (error) => {
20868
- return error instanceof Error && error.message.includes('HTTP 400');
20869
- };
20870
- /**
20871
- * Poll the LI.FI status API to retrieve the output amount for a
20872
- * completed swap transaction. Return `undefined` on any failure
20873
- * because the swap has already succeeded on-chain — this is
20874
- * best-effort enrichment only.
20875
- *
20876
- * @param txHash - The transaction hash of the swap to look up.
20877
- * @param chain - The chain definition where the swap was executed.
20878
- * @returns The raw token amount string from LI.FI, or `undefined`
20879
- * if the status could not be retrieved.
20880
- * @throws Never throws — all errors are caught and result in an
20881
- * `undefined` return value.
20882
- *
20883
- * @example
20884
- * ```typescript
20885
- * import { fetchLifiAmountOut } from '../utils'
20886
- * import { Ethereum } from '@core/chains'
20887
- *
20888
- * const amountOut = await fetchLifiAmountOut('0xabc...', Ethereum)
20889
- * if (amountOut !== undefined) {
20890
- * console.log('Output amount:', amountOut)
20891
- * }
20892
- * ```
20893
- */
20894
- const fetchLifiAmountOut = async (txHash, chain) => {
20895
- try {
20896
- const lifiChainId = getLifiChainId(chain);
20897
- const url = buildLifiStatusUrl(txHash, lifiChainId);
20898
- // LI.FI returns HTTP 400 before it indexes the transaction.
20899
- // pollApiGet treats 400 as non-retryable, so we wrap it in our
20900
- // own retry loop that catches the "not indexed yet" 400 and
20901
- // waits for LI.FI to become aware of the transaction.
20902
- const { maxRetries, retryDelay } = LIFI_POLLING_CONFIG;
20903
- const deadline = Date.now() + LIFI_TOTAL_TIMEOUT_MS;
20904
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
20905
- if (Date.now() >= deadline)
20906
- return undefined;
20907
- try {
20908
- const response = await pollApiGet(url, isLifiStatusDone, LIFI_POLLING_CONFIG);
20909
- return response.receiving?.amount;
20910
- }
20911
- catch (error) {
20912
- if (!isLifiNotIndexedError(error) || attempt === maxRetries) {
20913
- throw error;
20914
- }
20915
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
20916
- }
20917
- }
20918
- return undefined;
20919
- }
20920
- catch {
20921
- // Best-effort enrichment only; the swap has already succeeded on-chain.
20922
- return undefined;
20923
- }
20924
- };
20925
-
20926
20772
  /**
20927
20773
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
20928
20774
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -21001,7 +20847,7 @@ function handleSwapExecutionError(err, txHash, chain) {
21001
20847
  * Dynamically determined based on:
21002
20848
  * - Membership in the SwapChain enum
21003
20849
  * - CCTPv2 support (adapter contract deployed)
21004
- * - Mainnet only (no testnets)
20850
+ * - Whitelisted chains only (testnets allowed when explicitly in SwapChain enum)
21005
20851
  *
21006
20852
  * This list automatically updates when new chains are added.
21007
20853
  *
@@ -21201,7 +21047,7 @@ class StablecoinServiceSwapProvider {
21201
21047
  *
21202
21048
  * @remarks
21203
21049
  * This method validates that:
21204
- * - The chain is a mainnet CCTPv2-supported chain (not testnet)
21050
+ * - The chain is swap-supported (mainnet or whitelisted testnet with CCTPv2)
21205
21051
  * - Both tokens are supported (USDC, USDT, native currency, or token literals)
21206
21052
  * - The tokens are different (no same-token swaps)
21207
21053
  *
@@ -21256,10 +21102,7 @@ class StablecoinServiceSwapProvider {
21256
21102
  * ```
21257
21103
  */
21258
21104
  supportsRoute(tokenInAddress, tokenOutAddress, chain) {
21259
- if (chain.isTestnet) {
21260
- return false;
21261
- }
21262
- if (!isCCTPV2Supported(chain)) {
21105
+ if (!isSwapSupportedChain(chain)) {
21263
21106
  return false;
21264
21107
  }
21265
21108
  // Normalize for same-token check
@@ -21861,7 +21704,22 @@ class StablecoinServiceSwapProvider {
21861
21704
  const swapResultFees = serviceResponse.fees
21862
21705
  ? await this.buildFormattedFees(serviceResponse.fees, chain, adapter, config?.customFee?.recipientAddress)
21863
21706
  : undefined;
21864
- const amountOut = await fetchLifiAmountOut(txHash, chain);
21707
+ // Best-effort enrichment: fetch amountOut via the proxy service.
21708
+ // If the call fails or times out, amountOut is simply omitted.
21709
+ let amountOut;
21710
+ try {
21711
+ const statusResult = await getSwapStatus({
21712
+ txHash,
21713
+ chain: chain.chain,
21714
+ apiKey: serviceParams.apiKey,
21715
+ });
21716
+ if (statusResult.status === 'DONE' && statusResult.amountOut) {
21717
+ amountOut = statusResult.amountOut;
21718
+ }
21719
+ }
21720
+ catch {
21721
+ // Non-fatal — the swap already succeeded on-chain.
21722
+ }
21865
21723
  // Build and return SwapResult
21866
21724
  return {
21867
21725
  tokenIn,
@@ -21989,85 +21847,355 @@ class StablecoinServiceSwapProvider {
21989
21847
  }
21990
21848
 
21991
21849
  /**
21992
- * The default providers that will be used in addition to the providers provided
21993
- * to the createSwapKitContext factory function.
21850
+ * Schema for validating AdapterContext.
21851
+ * Must always contain both adapter and chain explicitly.
21852
+ * Optionally includes address for developer-controlled adapters.
21853
+ */
21854
+ const adapterContextSchema$1 = zod.z.object({
21855
+ adapter: adapterSchema$1,
21856
+ chain: swapChainIdentifierSchema,
21857
+ address: zod.z.string().optional(),
21858
+ });
21859
+ /**
21860
+ * Schema for validating allowance strategy values.
21861
+ */
21862
+ const allowanceStrategySchema = zod.z.enum(['permit', 'approve']);
21863
+ /**
21864
+ * Schema for validating SwapKit custom fee configuration.
21994
21865
  *
21995
- * @returns An array containing the default StablecoinServiceSwapProvider
21996
- * @internal
21866
+ * Supports two mutually exclusive approaches:
21867
+ * - Percentage-based: percentageBps + recipientAddress
21868
+ * - Callback-based: amount + recipientAddress
21869
+ *
21870
+ * @remarks
21871
+ * Mutual exclusivity is validated at runtime in buildServiceParams.
21872
+ * Chain-specific address validation is performed in resolveSwapConfig.
21997
21873
  */
21998
- const getDefaultProviders = () => [new StablecoinServiceSwapProvider()];
21874
+ const swapCustomFeeSchema = zod.z
21875
+ .object({
21876
+ /**
21877
+ * Fee percentage in basis points (percentage approach).
21878
+ * 100 bps = 1%, must be > 0 and <= 10000.
21879
+ */
21880
+ percentageBps: zod.z.number().int().positive().max(10000),
21881
+ /**
21882
+ * Fee recipient address (required).
21883
+ * Must be a valid EVM address or Solana address.
21884
+ */
21885
+ recipientAddress: zod.z
21886
+ .string()
21887
+ .refine((value) => evmAddressSchema.safeParse(value).success ||
21888
+ solanaAddressSchema.safeParse(value).success, {
21889
+ message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)',
21890
+ }),
21891
+ })
21892
+ .strict();
21999
21893
  /**
22000
- * Create a SwapKit context with validated configuration.
21894
+ * Schema for validating swap configuration options.
22001
21895
  *
22002
- * This factory function initializes a SwapKitContext with default providers
22003
- * and optional custom configuration. It validates any provided custom fee
22004
- * policy and merges default and custom providers, preserving their exact
22005
- * types for type safety.
21896
+ * Validates:
21897
+ * - allowanceStrategy: Either 'permit' or 'approve'
21898
+ * - slippageBps: Optional positive number for slippage tolerance
21899
+ * - stopLimit: Optional decimal string for minimum output
21900
+ * - customFee: Optional fee configuration
21901
+ * - kitKey: Optional string identifier
21902
+ */
21903
+ const swapConfigSchema = zod.z.object({
21904
+ allowanceStrategy: allowanceStrategySchema.optional(),
21905
+ slippageBps: zod.z.number().int().min(0).optional(),
21906
+ stopLimit: zod.z
21907
+ .string()
21908
+ .min(1, 'Required')
21909
+ .pipe(createDecimalStringValidator({
21910
+ allowZero: true,
21911
+ regexMessage: 'Stop limit must be a numeric string with dot (.) as decimal separator (e.g., "0.1", ".1", "10.5", "1000.50"), with no thousand separators or comma decimals.',
21912
+ attributeName: 'stopLimit',
21913
+ })(zod.z.string()))
21914
+ .optional(),
21915
+ customFee: swapCustomFeeSchema.optional(),
21916
+ kitKey: zod.z.string().optional(),
21917
+ });
21918
+ const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
21919
+ const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
21920
+ const BASE58_CHARS_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
21921
+ /**
21922
+ * Broad heuristic: return true when the input is composed entirely of
21923
+ * base58 characters and is at least 32 characters long (the minimum
21924
+ * length of a valid Solana address).
22006
21925
  *
22007
- * The function is pure and side-effect-free, returning a plain context object
22008
- * that can be used with swap operations. Default providers are currently
22009
- * stubbed and will be implemented in future phases.
21926
+ * The caller is expected to have already excluded valid Solana
21927
+ * addresses (via {@link SOLANA_ADDRESS_REGEX}) before invoking this
21928
+ * helper, so in practice it only matches over-length base58 strings
21929
+ * (45+ chars) that are plausibly a malformed Solana address.
21930
+ */
21931
+ function looksLikeSolanaAddress(value) {
21932
+ return BASE58_CHARS_REGEX.test(value) && value.length >= 32;
21933
+ }
21934
+ const MAX_DISPLAY_LENGTH = 30;
21935
+ /** Truncate a value for safe inclusion in error messages. */
21936
+ function truncate(value) {
21937
+ return value.length > MAX_DISPLAY_LENGTH
21938
+ ? `${value.slice(0, MAX_DISPLAY_LENGTH)}...`
21939
+ : value;
21940
+ }
21941
+ /**
21942
+ * Schema for validating swap token input.
22010
21943
  *
22011
- * @typeParam TExtraProviders - Array type of additional swap providers
22012
- * @param config - Optional configuration for the SwapKit context
22013
- * @returns A fully initialized SwapKitContext ready for swap operations
22014
- * @throws \{ValidationError\} If the custom fee policy is invalid
21944
+ * Accepts either:
21945
+ * - Token symbols from the supported swap token registry ('USDC', 'WETH', 'NATIVE', etc.)
21946
+ * - Token addresses (EVM: 0x..., Solana: base58)
21947
+ *
21948
+ * This allows swapping both supported tokens (by symbol or address) and
21949
+ * arbitrary tokens (by address only), as long as at least one token is
21950
+ * an "OK token" for fee collection purposes.
21951
+ *
21952
+ * @remarks
21953
+ * Uses input-shape heuristics to produce contextual error messages:
21954
+ * - Starts with `0x` — validated as EVM address
21955
+ * - Recognized symbol — accepted
21956
+ * - 32–44 base58 characters — accepted as Solana address
21957
+ * - Otherwise — reports the most likely error (unsupported symbol,
21958
+ * malformed Solana address, or malformed EVM address)
21959
+ *
21960
+ * Runtime validation in `isOkToken` determines if the token is supported
21961
+ * for fee collection. This schema only validates the format.
21962
+ */
21963
+ let _symbolResult;
21964
+ const swapTokenSchema = zod.z
21965
+ .string()
21966
+ .superRefine((value, ctx) => {
21967
+ if (value.startsWith('0x')) {
21968
+ if (!EVM_ADDRESS_REGEX.test(value)) {
21969
+ ctx.addIssue({
21970
+ code: zod.z.ZodIssueCode.custom,
21971
+ message: `Invalid EVM token address format. Expected '0x' followed by 40 hexadecimal characters, but received: '${truncate(value)}'`,
21972
+ });
21973
+ }
21974
+ return;
21975
+ }
21976
+ _symbolResult = supportedSwapTokenSchema.safeParse(value);
21977
+ if (_symbolResult.success) {
21978
+ return;
21979
+ }
21980
+ if (SOLANA_ADDRESS_REGEX.test(value)) {
21981
+ return;
21982
+ }
21983
+ if (looksLikeSolanaAddress(value)) {
21984
+ ctx.addIssue({
21985
+ code: zod.z.ZodIssueCode.custom,
21986
+ message: `Invalid Solana token address format. Expected 32-44 base58 characters, but received: '${truncate(value)}'`,
21987
+ });
21988
+ return;
21989
+ }
21990
+ ctx.addIssue({
21991
+ code: zod.z.ZodIssueCode.custom,
21992
+ message: `Unknown token symbol '${truncate(value)}'. Supported symbols include USDC, USDT, WETH, NATIVE, and others. See SDK documentation for the full list. You can also provide a token contract address (EVM: 0x... or Solana: base58).`,
21993
+ });
21994
+ })
21995
+ .transform((value) => (_symbolResult?.success ? _symbolResult.data : value));
21996
+ // Amount-in validation schema - broken out to reduce type complexity
21997
+ const amountInSchema = zod.z
21998
+ .string()
21999
+ .min(1, 'Required')
22000
+ .pipe(createDecimalStringValidator({
22001
+ allowZero: false,
22002
+ regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
22003
+ attributeName: 'amountIn',
22004
+ maxDecimals: 18,
22005
+ })(zod.z.string()))
22006
+ .describe('The amount of the input token to swap, expressed as a human-readable ' +
22007
+ "decimal string in token units (e.g., '0.05' for 0.05 USDC or 0.05 ETH).");
22008
+ /**
22009
+ * Schema for validating swap parameters with chain identifiers.
22010
+ *
22011
+ * This schema validates the complete swap operation input, ensuring:
22012
+ * - Valid adapter context (adapter + chain + optional address)
22013
+ * - Valid tokenIn and tokenOut (symbols or addresses)
22014
+ * - Valid amountIn as a positive decimal string
22015
+ * - Optional valid configuration
22016
+ *
22017
+ * The schema validates amounts with up to 18 decimal places to support
22018
+ * various token standards.
22015
22019
  *
22016
22020
  * @example
22017
22021
  * ```typescript
22018
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22022
+ * import { swapParamsSchema } from '@circle-fin/swap-kit'
22023
+ *
22024
+ * // Using token symbols (recommended for supported tokens)
22025
+ * const paramsWithSymbols = {
22026
+ * from: {
22027
+ * adapter: sourceAdapter,
22028
+ * chain: 'Ethereum'
22029
+ * },
22030
+ * tokenIn: 'USDC',
22031
+ * tokenOut: 'USDT',
22032
+ * amountIn: '100.50', // 100.50 USDC
22033
+ * config: {
22034
+ * slippageBps: 300,
22035
+ * allowanceStrategy: 'permit'
22036
+ * }
22037
+ * }
22038
+ *
22039
+ * // Using token addresses (works for any token)
22040
+ * const paramsWithAddresses = {
22041
+ * from: {
22042
+ * adapter: sourceAdapter,
22043
+ * chain: 'Base'
22044
+ * },
22045
+ * tokenIn: '0x4200000000000000000000000000000000000006', // WETH address
22046
+ * tokenOut: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // Any token address
22047
+ * amountIn: '0.1' // 0.1 WETH
22048
+ * }
22049
+ *
22050
+ * // Using native token alias
22051
+ * const paramsWithNative = {
22052
+ * from: {
22053
+ * adapter: sourceAdapter,
22054
+ * chain: 'Ethereum'
22055
+ * },
22056
+ * tokenIn: 'NATIVE', // ETH on Ethereum, MATIC on Polygon, etc.
22057
+ * tokenOut: 'USDC',
22058
+ * amountIn: '1.5' // 1.5 ETH
22059
+ * }
22060
+ *
22061
+ * const result = swapParamsSchema.safeParse(paramsWithSymbols)
22062
+ * if (result.success) {
22063
+ * console.log('Parameters are valid')
22064
+ * } else {
22065
+ * console.error('Validation failed:', result.error)
22066
+ * }
22067
+ * ```
22068
+ */
22069
+ const swapParamsSchema = zod.z.object({
22070
+ from: adapterContextSchema$1,
22071
+ tokenIn: swapTokenSchema,
22072
+ tokenOut: swapTokenSchema,
22073
+ amountIn: amountInSchema,
22074
+ config: swapConfigSchema.optional(),
22075
+ });
22076
+ /**
22077
+ * Schema for validating SwapKit custom fee policy.
22078
+ *
22079
+ * Validates the shape of CustomFeePolicy, which lets SDK consumers
22080
+ * provide custom fee calculation and fee-recipient resolution logic.
22081
+ *
22082
+ * - computeFee: required function that returns a fee as a string (or Promise<string>).
22083
+ * - resolveFeeRecipientAddress: required function that returns a recipient address as a
22084
+ * string (or Promise<string>).
22085
+ *
22086
+ * This schema only ensures the presence and return types of the functions; it
22087
+ * does not validate their argument types.
22088
+ *
22089
+ * @example
22090
+ * ```typescript
22091
+ * import { customFeePolicySchema } from '@circle-fin/swap-kit'
22092
+ *
22093
+ * const config = {
22094
+ * computeFee: async () => '0.1',
22095
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
22096
+ * }
22097
+ *
22098
+ * const result = customFeePolicySchema.safeParse(config)
22099
+ * // result.success === true
22100
+ * ```
22101
+ */
22102
+ const customFeePolicySchema = zod.z
22103
+ .object({
22104
+ computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
22105
+ resolveFeeRecipientAddress: zod.z
22106
+ .function()
22107
+ .returns(zod.z.string().or(zod.z.promise(zod.z.string()))),
22108
+ })
22109
+ .strict();
22110
+
22111
+ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
22112
+ /**
22113
+ * Assert that the provided value conforms to {@link CustomFeePolicy}.
22114
+ *
22115
+ * This function validates the custom fee policy configuration using the
22116
+ * customFeePolicySchema. It ensures that both required functions
22117
+ * (computeFee and resolveFeeRecipientAddress) are present and have
22118
+ * the correct return types.
22119
+ *
22120
+ * Throws a structured error with detailed validation messages if the
22121
+ * configuration is malformed. Uses state tracking to avoid duplicate
22122
+ * validations.
22123
+ *
22124
+ * @param config - The custom fee policy to validate
22125
+ * @throws \{KitError\} If the policy fails validation
22126
+ *
22127
+ * @example
22128
+ * ```typescript
22129
+ * import { assertCustomFeePolicy } from '@circle-fin/swap-kit'
22130
+ *
22131
+ * const config = {
22132
+ * computeFee: () => '0.1',
22133
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
22134
+ * }
22135
+ *
22136
+ * try {
22137
+ * assertCustomFeePolicy(config)
22138
+ * // If no error is thrown, `config` is a valid CustomFeePolicy
22139
+ * } catch (error) {
22140
+ * console.error('Invalid fee policy:', error.message)
22141
+ * }
22142
+ * ```
22143
+ */
22144
+ function assertCustomFeePolicy(config) {
22145
+ // Use validateWithStateTracking to avoid duplicate validations
22146
+ // This will skip validation if already validated by this function
22147
+ // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
22148
+ validateWithStateTracking(config, customFeePolicySchema, 'SwapKit custom fee policy', assertCustomFeePolicySymbol);
22149
+ }
22150
+
22151
+ /**
22152
+ * Symbol used to track that assertSwapParams has validated an object.
22153
+ * @internal
22154
+ */
22155
+ const ASSERT_SWAP_PARAMS_SYMBOL = Symbol('assertSwapParams');
22156
+ /**
22157
+ * Assert that the provided value conforms to the SwapParams schema.
22019
22158
  *
22020
- * // Create context with defaults
22021
- * const context = createSwapKitContext()
22022
- * ```
22159
+ * This function validates swap parameters using the provided Zod schema
22160
+ * and tracks validation state to avoid duplicate checks. It performs
22161
+ * comprehensive validation including:
22162
+ * - Adapter context structure
22163
+ * - Token specifications
22164
+ * - Amount format and range
22165
+ * - Optional configuration values
22023
22166
  *
22024
- * @example
22025
- * ```typescript
22026
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22167
+ * Throws a structured error with detailed validation messages if
22168
+ * any parameter is invalid.
22027
22169
  *
22028
- * // Create context with custom fee policy
22029
- * const context = createSwapKitContext({
22030
- * customFeePolicy: {
22031
- * computeFee: async (params) => {
22032
- * // Calculate based on swap amount
22033
- * return '0.1' // 0.1 USDC
22034
- * },
22035
- * resolveFeeRecipientAddress: async (chain, params) => {
22036
- * // Return recipient based on chain
22037
- * if (chain.type === 'solana') {
22038
- * return 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
22039
- * }
22040
- * return '0x1234567890123456789012345678901234567890'
22041
- * }
22042
- * }
22043
- * })
22044
- * ```
22170
+ * @typeParam T - The expected type after validation
22171
+ * @param params - The swap parameters to validate
22172
+ * @param schema - The Zod schema to validate against
22173
+ * @throws \{KitError\} If the parameters fail validation
22045
22174
  *
22046
22175
  * @example
22047
22176
  * ```typescript
22048
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22177
+ * import { assertSwapParams, swapParamsSchema } from '@circle-fin/swap-kit'
22049
22178
  *
22050
- * // Create context with custom providers (future use)
22051
- * const context = createSwapKitContext({
22052
- * providers: [] // Custom swap providers will be supported in future phases
22053
- * })
22179
+ * const params = {
22180
+ * from: { adapter: sourceAdapter, chain: 'Ethereum' },
22181
+ * tokenIn: 'USDC',
22182
+ * tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
22183
+ * amountIn: '100.50'
22184
+ * }
22185
+ *
22186
+ * try {
22187
+ * assertSwapParams(params, swapParamsSchema)
22188
+ * // Parameters are valid, proceed with swap
22189
+ * } catch (error) {
22190
+ * console.error('Invalid parameters:', error.message)
22191
+ * }
22054
22192
  * ```
22055
22193
  */
22056
- function createSwapKitContext(config = {}) {
22057
- // Validate custom fee policy if provided
22058
- if (config.customFeePolicy !== undefined) {
22059
- assertCustomFeePolicy(config.customFeePolicy);
22060
- }
22061
- // Initialize default providers
22062
- const defaultProviders = getDefaultProviders();
22063
- // Merge default and custom providers
22064
- const providers = [...defaultProviders, ...(config.providers ?? [])];
22065
- // Return the initialized context
22066
- return {
22067
- providers,
22068
- customFeePolicy: config.customFeePolicy ?? undefined,
22069
- tokens: createTokenRegistry(),
22070
- };
22194
+ function assertSwapParams(params, schema) {
22195
+ // Use validateWithStateTracking to avoid duplicate validations
22196
+ // This will skip validation if already validated by this function
22197
+ // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
22198
+ validateWithStateTracking(params, schema, 'swap parameters', ASSERT_SWAP_PARAMS_SYMBOL);
22071
22199
  }
22072
22200
 
22073
22201
  /**
@@ -23391,6 +23519,60 @@ class Amount {
23391
23519
  }
23392
23520
  }
23393
23521
 
23522
+ /**
23523
+ * Return the stablecoin symbol that should replace NATIVE for chains whose
23524
+ * native gas token is itself a supported stablecoin.
23525
+ *
23526
+ * @param chain - The chain definition to inspect.
23527
+ * @returns `'USDC'` when the chain's native currency symbol is USDC and a
23528
+ * USDC contract address exists, `null` otherwise.
23529
+ *
23530
+ * @example
23531
+ * ```typescript
23532
+ * import { ArcTestnet, Ethereum } from '@core/chains'
23533
+ *
23534
+ * getNativeStablecoinSymbol(ArcTestnet) // 'USDC'
23535
+ * getNativeStablecoinSymbol(Ethereum) // null
23536
+ * ```
23537
+ */
23538
+ function getNativeStablecoinSymbol(chain) {
23539
+ if (chain.nativeCurrency.symbol.toUpperCase() === 'USDC' &&
23540
+ chain.usdcAddress) {
23541
+ return 'USDC';
23542
+ }
23543
+ return null;
23544
+ }
23545
+ /**
23546
+ * Canonicalize the NATIVE alias to the matching stablecoin symbol on chains
23547
+ * whose native gas token is itself a supported stablecoin.
23548
+ *
23549
+ * The comparison is case-insensitive for the NATIVE token identifier.
23550
+ * Non-NATIVE inputs and native placeholder addresses (e.g. `0xEeee...`)
23551
+ * pass through unchanged.
23552
+ *
23553
+ * @param token - The token symbol or alias to canonicalize.
23554
+ * @param chain - The chain definition used for context-specific resolution.
23555
+ * @returns The canonicalized token symbol. Returns `'USDC'` when `token` is
23556
+ * `'NATIVE'` on a native-stablecoin chain; otherwise returns `token`
23557
+ * unchanged.
23558
+ *
23559
+ * @example
23560
+ * ```typescript
23561
+ * import { ArcTestnet, Ethereum } from '@core/chains'
23562
+ *
23563
+ * canonicalizeNativeStablecoinAlias('NATIVE', ArcTestnet) // 'USDC'
23564
+ * canonicalizeNativeStablecoinAlias('native', ArcTestnet) // 'USDC'
23565
+ * canonicalizeNativeStablecoinAlias('NATIVE', Ethereum) // 'NATIVE'
23566
+ * canonicalizeNativeStablecoinAlias('USDC', ArcTestnet) // 'USDC'
23567
+ * ```
23568
+ */
23569
+ function canonicalizeNativeStablecoinAlias(token, chain) {
23570
+ if (token.toUpperCase() !== NATIVE_TOKEN) {
23571
+ return token;
23572
+ }
23573
+ return getNativeStablecoinSymbol(chain) ?? token;
23574
+ }
23575
+
23394
23576
  /**
23395
23577
  * Assert the resolved chain definition is supported by swap operations.
23396
23578
  *
@@ -23414,6 +23596,74 @@ function assertIsSwapSupportedChainDefinition(chain) {
23414
23596
  throw createValidationFailedError$1('chain', chain.chain, `Unsupported swap chain. Supported chains: ${supported}`);
23415
23597
  }
23416
23598
  }
23599
+ /**
23600
+ * Resolve the on-chain address of the stablecoin that matches the
23601
+ * native currency symbol, or `null` if there is no match.
23602
+ */
23603
+ function getNativeStablecoinAddress(chain) {
23604
+ if (getNativeStablecoinSymbol(chain) === 'USDC') {
23605
+ return chain.usdcAddress;
23606
+ }
23607
+ return null;
23608
+ }
23609
+ /**
23610
+ * Check whether a token input (symbol or address) refers to the
23611
+ * native currency's underlying stablecoin on this chain.
23612
+ */
23613
+ function isNativeEquivalent(token, nativeSymbol, nativeStablecoinAddress) {
23614
+ const upper = token.toUpperCase();
23615
+ if (upper === nativeSymbol)
23616
+ return true;
23617
+ if (upper === nativeStablecoinAddress.toUpperCase())
23618
+ return true;
23619
+ return false;
23620
+ }
23621
+ /**
23622
+ * Check whether a token string represents the native gas token,
23623
+ * either as the `"NATIVE"` alias or a well-known placeholder address.
23624
+ */
23625
+ function isNativeTokenInput(token) {
23626
+ const lower = token.toLowerCase();
23627
+ if (lower === NATIVE_TOKEN.toLowerCase())
23628
+ return true;
23629
+ return OK_NATIVE_TOKEN_ADDRESSES_EVM.some((addr) => addr.toLowerCase() === lower);
23630
+ }
23631
+ /**
23632
+ * Reject swaps between NATIVE and a token that IS the native currency.
23633
+ *
23634
+ * On chains like Arc where the native gas token is USDC, swapping
23635
+ * USDC ↔ NATIVE is a no-op because they represent the same asset.
23636
+ * This guard handles symbol inputs (`'USDC'`, `'NATIVE'`), raw
23637
+ * contract addresses (`'0x3600...'`), and native placeholder
23638
+ * addresses (`'0xEeee...'`, `'0x0000...'`).
23639
+ *
23640
+ * @param tokenIn - The input token alias or address
23641
+ * @param tokenOut - The output token alias or address
23642
+ * @param chain - The resolved chain definition
23643
+ * @returns Nothing.
23644
+ * @throws \{KitError\} When one side is NATIVE (alias or placeholder)
23645
+ * and the other matches the chain's native currency symbol or
23646
+ * contract address
23647
+ */
23648
+ function assertNativeNotEquivalent(tokenIn, tokenOut, chain) {
23649
+ const nativeStablecoinAddress = getNativeStablecoinAddress(chain);
23650
+ if (nativeStablecoinAddress === null)
23651
+ return;
23652
+ const nativeSymbol = chain.nativeCurrency.symbol.toUpperCase();
23653
+ const inIsNative = isNativeTokenInput(tokenIn);
23654
+ const outIsNative = isNativeTokenInput(tokenOut);
23655
+ const shouldReject = (inIsNative &&
23656
+ isNativeEquivalent(tokenOut, nativeSymbol, nativeStablecoinAddress)) ||
23657
+ (outIsNative &&
23658
+ isNativeEquivalent(tokenIn, nativeSymbol, nativeStablecoinAddress)) ||
23659
+ (inIsNative && outIsNative);
23660
+ if (shouldReject) {
23661
+ throw createValidationFailedError$1(inIsNative ? 'tokenIn' : 'tokenOut', inIsNative ? tokenIn : tokenOut, `On ${chain.name}, the native gas token is ${nativeSymbol}. ` +
23662
+ `Swapping between NATIVE and ${nativeSymbol} is not supported ` +
23663
+ `because they represent the same asset. ` +
23664
+ `Use ${nativeSymbol} directly instead of NATIVE.`);
23665
+ }
23666
+ }
23417
23667
  /**
23418
23668
  * Resolves the amount of a swap by formatting it according to the token's decimal places.
23419
23669
  *
@@ -23524,12 +23774,14 @@ async function resolveSwapConfig(params, chain, tokens, adapter) {
23524
23774
  * Resolves swap parameters from user input to provider-consumable format.
23525
23775
  *
23526
23776
  * Transforms SwapParams with chain identifiers and token aliases into
23527
- * ResolvedSwapParams with full chain definitions, preserved token aliases,
23777
+ * ResolvedSwapParams with full chain definitions, canonicalized token aliases,
23528
23778
  * and validated wallet addresses. Uses the TokenRegistry from context for
23529
23779
  * decimal resolution.
23530
23780
  *
23531
- * Note: Token aliases ('USDC', 'USDT', 'NATIVE') are preserved and passed
23532
- * through unchanged. Address resolution is handled by the provider layer.
23781
+ * Note: Raw user input is validated first, then `NATIVE` may be canonicalized
23782
+ * to a stablecoin symbol on chains whose native gas token is itself a
23783
+ * supported stablecoin (for example, Arc Testnet `NATIVE` → `USDC`).
23784
+ * Address resolution is handled by the provider layer.
23533
23785
  *
23534
23786
  * @typeParam TFromAdapterCapabilities - The adapter capabilities type for the source adapter
23535
23787
  * @param params - The input swap parameters to resolve
@@ -23557,8 +23809,8 @@ async function resolveSwapConfig(params, chain, tokens, adapter) {
23557
23809
  * amountIn: '100.5'
23558
23810
  * }, tokens)
23559
23811
  *
23560
- * // resolved.tokenIn === 'USDC' // Alias preserved
23561
- * // resolved.tokenOut === 'USDT' // Alias preserved
23812
+ * // resolved.tokenIn === 'USDC' // Canonicalized for provider routing
23813
+ * // resolved.tokenOut === 'USDT' // Canonicalized for provider routing
23562
23814
  * // resolved.to === wallet address from adapter
23563
23815
  * ```
23564
23816
  */
@@ -23569,16 +23821,20 @@ async function resolveSwapParams(params, tokens) {
23569
23821
  const resolvedChain = resolveChainIdentifier(params.from.chain);
23570
23822
  assertIsSwapSupportedChainDefinition(resolvedChain);
23571
23823
  const fromChain = resolvedChain;
23824
+ assertNativeNotEquivalent(params.tokenIn, params.tokenOut, fromChain);
23572
23825
  params.from.adapter.validateChainSupport(fromChain);
23573
23826
  // Cast required: SwapAdapterContext and AdapterContext differ in their
23574
23827
  // optional address field handling under exactOptionalPropertyTypes.
23575
23828
  const walletAddress = await resolveAddress(params.from);
23576
- // Pass token aliases directly to the resolved params
23577
- // Resolution to addresses will be handled by the provider
23578
- const tokenIn = params.tokenIn;
23579
- const tokenOut = params.tokenOut;
23580
- const resolvedAmount = await resolveAmount(params.amountIn, params.tokenIn, fromChain, tokens, params.from.adapter);
23581
- const resolvedConfig = await resolveSwapConfig(params, fromChain, tokens, params.from.adapter);
23829
+ // Canonicalize NATIVE after raw-input validation so native-stablecoin
23830
+ // chains use stablecoin decimals and downstream provider routing.
23831
+ const tokenIn = canonicalizeNativeStablecoinAlias(params.tokenIn, fromChain);
23832
+ const tokenOut = canonicalizeNativeStablecoinAlias(params.tokenOut, fromChain);
23833
+ const resolvedAmount = await resolveAmount(params.amountIn, tokenIn, fromChain, tokens, params.from.adapter);
23834
+ const resolvedConfig = await resolveSwapConfig({
23835
+ ...params,
23836
+ tokenOut,
23837
+ }, fromChain, tokens, params.from.adapter);
23582
23838
  return {
23583
23839
  from: {
23584
23840
  ...params.from,
@@ -23843,6 +24099,10 @@ const formatConfig = (config, outputTokenTransform) => {
23843
24099
  if (Object.keys(cloneConfig).length === 0) {
23844
24100
  return undefined;
23845
24101
  }
24102
+ // Transform stopLimit if present (minimum output amount, uses output token decimals)
24103
+ if (cloneConfig.stopLimit !== undefined) {
24104
+ cloneConfig.stopLimit = outputTokenTransform(cloneConfig.stopLimit);
24105
+ }
23846
24106
  // Handle customFee transformation
23847
24107
  if (config.customFee !== undefined) {
23848
24108
  const { amount, percentageBps, recipientAddress } = config.customFee;
@@ -23957,7 +24217,9 @@ function getNativeTokenAddress(chain) {
23957
24217
  * The quote endpoint requires resolved addresses, not aliases. This function
23958
24218
  * mirrors the provider's `resolveTokenAlias` logic using the context's
23959
24219
  * TokenRegistry:
23960
- * - "NATIVE" alias → chain-specific native token placeholder address
24220
+ * - "NATIVE" alias → native token placeholder, unless the chain's native gas
24221
+ * token is a supported stablecoin, in which case it canonicalizes to the
24222
+ * stablecoin symbol and resolves through the registry
23961
24223
  * - Native currency symbol (ETH, SOL, MON, etc.) → native token placeholder
23962
24224
  * - Known symbols (USDC, USDT, etc.) → resolved chain-specific address
23963
24225
  * - Already-an-address strings → passed through unchanged
@@ -23970,9 +24232,13 @@ function getNativeTokenAddress(chain) {
23970
24232
  */
23971
24233
  function resolveTokenForQuote(token, chain, tokens) {
23972
24234
  const upperToken = token.toUpperCase();
23973
- // Handle NATIVE alias and native currency symbols (ETH, SOL, MON, etc.)
24235
+ const nativeStablecoinSymbol = getNativeStablecoinSymbol(chain);
24236
+ // Handle native aliases and native currency symbols for standard native-token
24237
+ // chains. On native-stablecoin chains, canonicalized symbols should resolve
24238
+ // via the token registry instead of the native placeholder.
23974
24239
  if (upperToken === 'NATIVE' ||
23975
- upperToken === chain.nativeCurrency.symbol.toUpperCase()) {
24240
+ (nativeStablecoinSymbol === null &&
24241
+ upperToken === chain.nativeCurrency.symbol.toUpperCase())) {
23976
24242
  return getNativeTokenAddress(chain);
23977
24243
  }
23978
24244
  // If the token is a known symbol in the registry, resolve to address
@@ -24034,8 +24300,15 @@ async function handleOutputFeeCallback(context, params) {
24034
24300
  // Resolve token aliases to addresses for the quote API
24035
24301
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
24036
24302
  const chain = params.from.chain;
24037
- const resolvedTokenIn = resolveTokenForQuote(params.tokenIn, chain, context.tokens);
24038
- const resolvedTokenOut = resolveTokenForQuote(params.tokenOut, chain, context.tokens);
24303
+ const tokenIn = canonicalizeNativeStablecoinAlias(params.tokenIn, chain);
24304
+ const tokenOut = canonicalizeNativeStablecoinAlias(params.tokenOut, chain);
24305
+ const canonicalizedParams = {
24306
+ ...params,
24307
+ tokenIn,
24308
+ tokenOut,
24309
+ };
24310
+ const resolvedTokenIn = resolveTokenForQuote(canonicalizedParams.tokenIn, chain, context.tokens);
24311
+ const resolvedTokenOut = resolveTokenForQuote(canonicalizedParams.tokenOut, chain, context.tokens);
24039
24312
  const quoteParams = {
24040
24313
  tokenInAddress: resolvedTokenIn,
24041
24314
  tokenInChain: chain.chain, // Use enum value (e.g., "World_Chain")
@@ -24053,7 +24326,7 @@ async function handleOutputFeeCallback(context, params) {
24053
24326
  const quoteResponse = await getQuote(quoteParams);
24054
24327
  // Step 3: Build fee context from quote
24055
24328
  const feeContext = {
24056
- ...params,
24329
+ ...canonicalizedParams,
24057
24330
  type: 'output',
24058
24331
  minAmount: quoteResponse.quote.minAmount,
24059
24332
  estimatedAmount: quoteResponse.quote.estimatedAmount,
@@ -24161,8 +24434,8 @@ async function estimate(context, params) {
24161
24434
  // Return the estimate with input context fields populated
24162
24435
  return {
24163
24436
  // Input context fields
24164
- tokenIn: params.tokenIn,
24165
- tokenOut: params.tokenOut,
24437
+ tokenIn: resolvedParams.tokenIn,
24438
+ tokenOut: resolvedParams.tokenOut,
24166
24439
  amountIn: params.amountIn,
24167
24440
  chain: chainName,
24168
24441
  fromAddress: resolvedParams.from.address,
@@ -24286,11 +24559,11 @@ async function swap$1(context, params) {
24286
24559
  };
24287
24560
  const providerResult = await provider.swap(swapParams);
24288
24561
  const [tokenInDecimals, tokenOutDecimals] = await Promise.all([
24289
- getTokenDecimals(params.tokenIn, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24290
- getTokenDecimals(params.tokenOut, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24562
+ getTokenDecimals(resolvedParams.tokenIn, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24563
+ getTokenDecimals(resolvedParams.tokenOut, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24291
24564
  ]);
24292
24565
  // Step 6: Format provider result to human-readable values
24293
- return formatSwapResult(providerResult, 'to-human-readable', params.tokenIn, params.tokenOut, tokenInDecimals, tokenOutDecimals);
24566
+ return formatSwapResult(providerResult, 'to-human-readable', resolvedParams.tokenIn, resolvedParams.tokenOut, tokenInDecimals, tokenOutDecimals);
24294
24567
  }
24295
24568
 
24296
24569
  /**
@@ -24356,9 +24629,10 @@ function getSupportedChains$1(context) {
24356
24629
  * This function follows an immutable pattern, returning a new context with the
24357
24630
  * wrapped policy attached. The original context remains unchanged.
24358
24631
  *
24632
+ * @typeParam TProviders - The tuple of swap providers in the context, preserved through the returned context
24359
24633
  * @param context - The SwapKitContext to configure with the custom fee policy
24360
24634
  * @param customFeePolicy - The custom fee policy containing fee calculation and recipient resolution logic
24361
- * @returns A new SwapKitContext with the custom fee policy configured
24635
+ * @returns A new SwapKitContext\<TProviders\> with the custom fee policy configured, preserving the original provider types
24362
24636
  * @throws \{ValidationError\} If the custom fee policy is invalid or missing required functions
24363
24637
  * @throws \{KitError\} If the token is not supported (not USDC, USDT, or NATIVE)
24364
24638
  *
@@ -24504,6 +24778,90 @@ function removeCustomFeePolicy(context) {
24504
24778
  };
24505
24779
  }
24506
24780
 
24781
+ /**
24782
+ * The default providers that will be used in addition to the providers provided
24783
+ * to the createSwapKitContext factory function.
24784
+ *
24785
+ * @returns An array containing the default StablecoinServiceSwapProvider
24786
+ * @internal
24787
+ */
24788
+ const getDefaultProviders = () => [new StablecoinServiceSwapProvider()];
24789
+ /**
24790
+ * Create a SwapKit context with validated configuration.
24791
+ *
24792
+ * This factory function initializes a SwapKitContext with default providers
24793
+ * and optional custom configuration. It validates any provided custom fee
24794
+ * policy and merges default and custom providers, preserving their exact
24795
+ * types for type safety.
24796
+ *
24797
+ * The function is pure and side-effect-free, returning a plain context object
24798
+ * that can be used with swap operations. Default providers are currently
24799
+ * stubbed and will be implemented in future phases.
24800
+ *
24801
+ * @typeParam TExtraProviders - Array type of additional swap providers
24802
+ * @param config - Optional configuration for the SwapKit context
24803
+ * @returns A fully initialized SwapKitContext ready for swap operations
24804
+ * @throws \{ValidationError\} If the custom fee policy is invalid
24805
+ *
24806
+ * @example
24807
+ * ```typescript
24808
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24809
+ *
24810
+ * // Create context with defaults
24811
+ * const context = createSwapKitContext()
24812
+ * ```
24813
+ *
24814
+ * @example
24815
+ * ```typescript
24816
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24817
+ *
24818
+ * // Create context with custom fee policy
24819
+ * const context = createSwapKitContext({
24820
+ * customFeePolicy: {
24821
+ * computeFee: async (params) => {
24822
+ * // Calculate based on swap amount
24823
+ * return '0.1' // 0.1 USDC
24824
+ * },
24825
+ * resolveFeeRecipientAddress: async (chain, params) => {
24826
+ * // Return recipient based on chain
24827
+ * if (chain.type === 'solana') {
24828
+ * return 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
24829
+ * }
24830
+ * return '0x1234567890123456789012345678901234567890'
24831
+ * }
24832
+ * }
24833
+ * })
24834
+ * ```
24835
+ *
24836
+ * @example
24837
+ * ```typescript
24838
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24839
+ *
24840
+ * // Create context with custom providers (future use)
24841
+ * const context = createSwapKitContext({
24842
+ * providers: [] // Custom swap providers will be supported in future phases
24843
+ * })
24844
+ * ```
24845
+ */
24846
+ function createSwapKitContext(config = {}) {
24847
+ // Initialize default providers
24848
+ const defaultProviders = getDefaultProviders();
24849
+ // Merge default and custom providers
24850
+ const providers = [...defaultProviders, ...(config.providers ?? [])];
24851
+ // Build base context without fee policy
24852
+ const context = {
24853
+ providers,
24854
+ customFeePolicy: undefined,
24855
+ tokens: createTokenRegistry(),
24856
+ };
24857
+ // If fee policy provided, apply wrapping via setCustomFeePolicy
24858
+ // This ensures computeFee converts between human-readable and base units
24859
+ if (config.customFeePolicy !== undefined) {
24860
+ return setCustomFeePolicy(context, config.customFeePolicy);
24861
+ }
24862
+ return context;
24863
+ }
24864
+
24507
24865
  /**
24508
24866
  * A high-level class-based interface for single-chain token swap operations.
24509
24867
  *
@@ -25642,6 +26000,56 @@ const bridge = async (context, params) => {
25642
26000
  return kit.bridge(params);
25643
26001
  };
25644
26002
 
26003
+ /**
26004
+ * Retry a failed cross-chain bridge operation using the AppKit context.
26005
+ *
26006
+ * This function provides a standardized interface for retrying bridge
26007
+ * operations within the AppKit ecosystem. It delegates to the underlying
26008
+ * BridgeKit retry infrastructure while maintaining consistency with the
26009
+ * AppKit patterns and context-based architecture.
26010
+ *
26011
+ * Use this after detecting a retryable error on a failed step via
26012
+ * {@link isRetryableError} to resume a bridge that failed due to a
26013
+ * transient issue.
26014
+ *
26015
+ * @param context - The AppKit context containing action handlers
26016
+ * @param result - The bridge result from the failed operation
26017
+ * @param retryContext - The retry context with source and optional
26018
+ * destination adapters
26019
+ * @returns Promise resolving to the bridge result with transaction
26020
+ * details
26021
+ * @throws If the provider from the original result is not found
26022
+ * @throws If the retry operation fails
26023
+ *
26024
+ * @example
26025
+ * ```typescript
26026
+ * import { AppKit, isRetryableError } from '@circle-fin/app-kit'
26027
+ *
26028
+ * const kit = new AppKit()
26029
+ *
26030
+ * const result = await kit.bridge({
26031
+ * from: sourceAdapter,
26032
+ * to: { adapter: destAdapter, chain: 'Polygon' },
26033
+ * amount: '100.50',
26034
+ * })
26035
+ *
26036
+ * const failedStep = result.steps.find(s => s.error)
26037
+ * if (result.state === 'error' && failedStep?.error && isRetryableError(failedStep.error)) {
26038
+ * const retried = await kit.retryBridge(result, {
26039
+ * from: sourceAdapter,
26040
+ * to: destAdapter,
26041
+ * })
26042
+ * console.log('Retry result:', retried.state)
26043
+ * }
26044
+ * ```
26045
+ */
26046
+ const retryBridge = async (context, result, retryContext) => {
26047
+ const kit = createBridgeKit(context);
26048
+ registerActionHandlers(kit, context.actions.bridge, 'bridge');
26049
+ // Delegate to the BridgeKit for actual retry execution
26050
+ return kit.retry(result, retryContext);
26051
+ };
26052
+
25645
26053
  /**
25646
26054
  * Execute a same-chain token swap operation using the AppKit context.
25647
26055
  *
@@ -25913,6 +26321,46 @@ class AppKit {
25913
26321
  async bridge(params) {
25914
26322
  return bridge(this.context, params);
25915
26323
  }
26324
+ /**
26325
+ * Retry a failed cross-chain USDC bridge transfer.
26326
+ *
26327
+ * Resume a bridge operation that failed due to a transient error.
26328
+ * Use {@link isRetryableError} to check whether a failed step's error
26329
+ * is eligible for retry before calling this method.
26330
+ *
26331
+ * @param result - The bridge result from the failed operation
26332
+ * @param retryContext - The retry context with source and optional
26333
+ * destination adapters
26334
+ * @returns Promise resolving to the bridge result with transaction
26335
+ * details
26336
+ * @throws If the provider from the original result is not found
26337
+ * @throws If the retry operation fails
26338
+ *
26339
+ * @example
26340
+ * ```typescript
26341
+ * import { AppKit, isRetryableError } from '@circle-fin/app-kit'
26342
+ *
26343
+ * const kit = new AppKit()
26344
+ *
26345
+ * const result = await kit.bridge({
26346
+ * from: sourceAdapter,
26347
+ * to: { adapter: destAdapter, chain: 'Polygon' },
26348
+ * amount: '100.50',
26349
+ * })
26350
+ *
26351
+ * const failedStep = result.steps.find(s => s.error)
26352
+ * if (result.state === 'error' && failedStep?.error && isRetryableError(failedStep.error)) {
26353
+ * const retried = await kit.retryBridge(result, {
26354
+ * from: sourceAdapter,
26355
+ * to: destAdapter,
26356
+ * })
26357
+ * console.log('Retry result:', retried.state)
26358
+ * }
26359
+ * ```
26360
+ */
26361
+ async retryBridge(result, retryContext) {
26362
+ return retryBridge(this.context, result, retryContext);
26363
+ }
25916
26364
  /**
25917
26365
  * Estimate the bridge operation.
25918
26366
  *