@circle-fin/app-kit 1.2.1 → 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.mjs CHANGED
@@ -683,6 +683,12 @@ const InputError = {
683
683
  name: 'INPUT_UNSUPPORTED_ACTION',
684
684
  type: 'INPUT',
685
685
  },
686
+ /** No route satisfies the slippage or minimum-output constraint */
687
+ SLIPPAGE_CONSTRAINT_NOT_MET: {
688
+ code: 1009,
689
+ name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
690
+ type: 'INPUT',
691
+ },
686
692
  /** General validation failure for complex validation rules */
687
693
  VALIDATION_FAILED: {
688
694
  code: 1098,
@@ -1713,6 +1719,40 @@ function normalizeTransactionDetails(txHash, explorerUrl) {
1713
1719
  }
1714
1720
  return normalized;
1715
1721
  }
1722
+ /**
1723
+ * Pattern that matches on-chain revert reasons caused by slippage or
1724
+ * price constraints. When a revert matches, the error is surfaced as
1725
+ * {@link InputError.SLIPPAGE_CONSTRAINT_NOT_MET} (RETRYABLE) instead of
1726
+ * a generic simulation-failed / transaction-reverted error.
1727
+ *
1728
+ * @internal
1729
+ */
1730
+ const SLIPPAGE_REVERT_PATTERN = /slippage|lower than the minimum required|less than the initial balance|minimum.?output|price.?impact|stop.?limit|InsufficientOutput|InsufficientFinalBalance/i;
1731
+ /**
1732
+ * Handle simulation / execution revert errors, distinguishing slippage
1733
+ * constraint failures from generic reverts and simulation failures.
1734
+ *
1735
+ * @internal
1736
+ */
1737
+ function handleRevertError(msg, error, context) {
1738
+ const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
1739
+ if (SLIPPAGE_REVERT_PATTERN.test(reason)) {
1740
+ return new KitError({
1741
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
1742
+ recoverability: 'RETRYABLE',
1743
+ message: `Transaction on ${context.chain} reverted: "${reason}". ` +
1744
+ 'Try increasing slippageBps or adjusting stopLimit.',
1745
+ cause: { trace: { rawError: error, chain: context.chain, reason } },
1746
+ });
1747
+ }
1748
+ if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
1749
+ return createSimulationFailedError(context.chain, reason, {
1750
+ rawError: error,
1751
+ });
1752
+ }
1753
+ const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
1754
+ return createTransactionRevertedError(context.chain, reason, { rawError: error }, txHash, explorerUrl);
1755
+ }
1716
1756
  /**
1717
1757
  * Parses raw blockchain errors into structured KitError instances.
1718
1758
  *
@@ -1793,21 +1833,7 @@ function parseBlockchainError(error, context) {
1793
1833
  // Pattern 2: Simulation and execution reverts
1794
1834
  // Matches contract revert errors and simulation failures
1795
1835
  if (/execution reverted|simulation failed|transaction reverted|transaction failed/i.test(msg)) {
1796
- const reason = extractRevertReason(msg, error) ?? 'Transaction reverted';
1797
- // Distinguish between simulation failures and transaction reverts
1798
- // "simulation failed" or "eth_call" indicates pre-flight simulation
1799
- // "transaction failed" or context.operation === 'transaction' indicates post-execution
1800
- if (/simulation failed/i.test(msg) || context.operation === 'simulation') {
1801
- return createSimulationFailedError(context.chain, reason, {
1802
- rawError: error,
1803
- });
1804
- }
1805
- // Transaction execution failures or reverts
1806
- // Include txHash and explorerUrl if available (transaction was submitted)
1807
- const { txHash, explorerUrl } = normalizeTransactionDetails(context.txHash, context.explorerUrl);
1808
- return createTransactionRevertedError(context.chain, reason, {
1809
- rawError: error,
1810
- }, txHash, explorerUrl);
1836
+ return handleRevertError(msg, error, context);
1811
1837
  }
1812
1838
  // Pattern 3: Gas-related errors
1813
1839
  // Matches gas estimation failures and gas exhaustion
@@ -1832,8 +1858,12 @@ function parseBlockchainError(error, context) {
1832
1858
  return createNetworkConnectionError(context.chain, { rawError: error });
1833
1859
  }
1834
1860
  // Pattern 5: RPC provider errors
1835
- // Matches RPC endpoint errors, invalid responses, and rate limits
1836
- if (/rpc|invalid response|rate limit|too many requests/i.test(msg)) {
1861
+ // Matches RPC endpoint errors, invalid responses, rate limits, and
1862
+ // transient JSON-RPC internal errors (e.g. ethers.js "could not coalesce error").
1863
+ // Note: "internal error" alone is too broad — contracts like USDT emit
1864
+ // "An internal error was received" for on-chain assertion failures.
1865
+ // We require JSON-RPC context (codes -32603/-32000) instead.
1866
+ 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)) {
1837
1867
  return createRpcEndpointError(context.chain, { rawError: error });
1838
1868
  }
1839
1869
  // Pattern 6: Transaction size limit errors
@@ -2930,8 +2960,19 @@ function handleClientError(statusCode, serviceName, operation, error, msg, respo
2930
2960
  trace: error,
2931
2961
  },
2932
2962
  });
2933
- // 404 - Not found - unsupported route
2963
+ // 404 - Not found - unsupported route OR stop-limit / slippage constraint not met
2934
2964
  case 404:
2965
+ if (isSlippageConstraintFailure(responseBody)) {
2966
+ return new KitError({
2967
+ ...InputError.SLIPPAGE_CONSTRAINT_NOT_MET,
2968
+ recoverability: 'RETRYABLE',
2969
+ message: `${serviceName} ${operation} failed: ${detail}. ` +
2970
+ 'Try increasing slippageBps or adjusting stopLimit.',
2971
+ cause: {
2972
+ trace: error,
2973
+ },
2974
+ });
2975
+ }
2935
2976
  return new KitError({
2936
2977
  ...InputError.UNSUPPORTED_ROUTE,
2937
2978
  recoverability: 'FATAL',
@@ -2966,6 +3007,38 @@ function handleClientError(statusCode, serviceName, operation, error, msg, respo
2966
3007
  });
2967
3008
  }
2968
3009
  }
3010
+ /**
3011
+ * Pattern that matches proxy response body text indicating the 404 was
3012
+ * caused by a slippage / price constraint rather than a truly unsupported
3013
+ * route. Kept case-insensitive so future proxy wording changes are tolerated.
3014
+ *
3015
+ * @internal
3016
+ */
3017
+ const SLIPPAGE_BODY_PATTERN = /slippage|stop.?limit|price.?impact|minimum.?output|SLIPPAGE_CONSTRAINT_NOT_MET/i;
3018
+ /**
3019
+ * Determine whether a 404 was caused by an unmet slippage or price
3020
+ * constraint rather than a genuinely unsupported route.
3021
+ *
3022
+ * Detection relies on the proxy response body containing slippage-related
3023
+ * language or a structured reason code. This avoids false positives that
3024
+ * would occur if we guessed based on request parameters alone (a user
3025
+ * can set `slippageBps` and still hit a truly unsupported route).
3026
+ *
3027
+ * @param responseBody - The parsed JSON body returned by the proxy
3028
+ * @returns `true` when the 404 should be treated as a slippage constraint failure
3029
+ * @internal
3030
+ */
3031
+ function isSlippageConstraintFailure(responseBody) {
3032
+ if (responseBody === undefined) {
3033
+ return false;
3034
+ }
3035
+ const textsToCheck = [
3036
+ responseBody.externalMessage,
3037
+ responseBody.message,
3038
+ extractDetailFromBody(responseBody),
3039
+ ];
3040
+ return textsToCheck.some((t) => typeof t === 'string' && SLIPPAGE_BODY_PATTERN.test(t));
3041
+ }
2969
3042
  /**
2970
3043
  * Handles HTTP 5xx server errors and maps them to appropriate KitError instances.
2971
3044
  *
@@ -3121,13 +3194,16 @@ function joinFieldErrors(errors) {
3121
3194
  * Derive a human-readable detail string from an {@link ApiErrorResponseBody}.
3122
3195
  *
3123
3196
  * Resolution order:
3124
- * 1. `body.message` **and** `body.errors` -- when both are present the
3197
+ * 1. `body.externalMessage` -- the user-facing string the proxy intends
3198
+ * consumers to display (e.g. "No route found that satisfies the
3199
+ * requested stop limit"). Preferred when available.
3200
+ * 2. `body.message` **and** `body.errors` -- when both are present the
3125
3201
  * top-level message is combined with the field-level detail so
3126
3202
  * developers see the full picture
3127
3203
  * (e.g. `"Validation error: tokenInChain: Invalid input; amount: …"`).
3128
- * 2. `body.message` alone -- used as-is.
3129
- * 3. `body.errors` alone -- field-level entries joined with "; ".
3130
- * 4. `undefined` -- caller should fall back to the raw HTTP status text.
3204
+ * 3. `body.message` alone -- used as-is.
3205
+ * 4. `body.errors` alone -- field-level entries joined with "; ".
3206
+ * 5. `undefined` -- caller should fall back to the raw HTTP status text.
3131
3207
  *
3132
3208
  * @param body - The parsed response body, may be undefined
3133
3209
  * @returns A detail string, or undefined when no useful info is available
@@ -3137,6 +3213,12 @@ function extractDetailFromBody(body) {
3137
3213
  if (body === undefined) {
3138
3214
  return undefined;
3139
3215
  }
3216
+ const externalMessage = typeof body.externalMessage === 'string' && body.externalMessage.length > 0
3217
+ ? body.externalMessage
3218
+ : undefined;
3219
+ if (externalMessage !== undefined) {
3220
+ return externalMessage;
3221
+ }
3140
3222
  const topMessage = typeof body.message === 'string' && body.message.length > 0
3141
3223
  ? body.message
3142
3224
  : undefined;
@@ -3334,8 +3416,9 @@ var Blockchain;
3334
3416
  /**
3335
3417
  * Enum representing chains that support same-chain swaps through the Swap Kit.
3336
3418
  *
3337
- * Unlike the full {@link Blockchain} enum, SwapChain includes only mainnet
3338
- * networks where adapter contracts are deployed (CCTPv2 support).
3419
+ * Unlike the full {@link Blockchain} enum, SwapChain includes mainnet
3420
+ * networks and explicitly whitelisted testnets (e.g., {@link Arc_Testnet})
3421
+ * where adapter contracts are deployed (CCTPv2 support).
3339
3422
  *
3340
3423
  * Dynamic validation via {@link isSwapSupportedChain} ensures chains
3341
3424
  * automatically work when adapter contracts and supported tokens are deployed.
@@ -3380,6 +3463,8 @@ var SwapChain;
3380
3463
  SwapChain["XDC"] = "XDC";
3381
3464
  SwapChain["HyperEVM"] = "HyperEVM";
3382
3465
  SwapChain["Monad"] = "Monad";
3466
+ // Testnet chains with swap support
3467
+ SwapChain["Arc_Testnet"] = "Arc_Testnet";
3383
3468
  })(SwapChain || (SwapChain = {}));
3384
3469
  // -----------------------------------------------------------------------------
3385
3470
  // Bridge Chain Enum (CCTPv2 Supported Chains)
@@ -3476,6 +3561,31 @@ var BridgeChain;
3476
3561
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
3477
3562
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
3478
3563
  })(BridgeChain || (BridgeChain = {}));
3564
+ // -----------------------------------------------------------------------------
3565
+ // Earn Chain Enum
3566
+ // -----------------------------------------------------------------------------
3567
+ /**
3568
+ * Enumeration of blockchains that support earn (vault deposit/withdraw)
3569
+ * operations through the Earn Kit.
3570
+ *
3571
+ * Currently only Ethereum mainnet is supported. Additional chains
3572
+ * will be added as vault protocol support expands.
3573
+ *
3574
+ * @example
3575
+ * ```typescript
3576
+ * import { EarnChain } from '@core/chains'
3577
+ *
3578
+ * const result = await earnKit.deposit({
3579
+ * from: { adapter, chain: EarnChain.Ethereum },
3580
+ * vaultAddress: '0x...',
3581
+ * amount: '100',
3582
+ * })
3583
+ * ```
3584
+ */
3585
+ var EarnChain;
3586
+ (function (EarnChain) {
3587
+ EarnChain["Ethereum"] = "Ethereum";
3588
+ })(EarnChain || (EarnChain = {}));
3479
3589
 
3480
3590
  /**
3481
3591
  * Helper function to define a chain with proper TypeScript typing.
@@ -3791,6 +3901,14 @@ const BRIDGE_CONTRACT_EVM_MAINNET = '0xB3FA262d0fB521cc93bE83d87b322b8A23DAf3F0'
3791
3901
  * on EVM-compatible chains. Use this address for mainnet adapter integrations.
3792
3902
  */
3793
3903
  const ADAPTER_CONTRACT_EVM_MAINNET = '0x7FB8c7260b63934d8da38aF902f87ae6e284a845';
3904
+ /**
3905
+ * The adapter contract address for EVM testnet networks.
3906
+ *
3907
+ * This contract serves as an adapter for integrating with various protocols
3908
+ * on EVM-compatible testnet chains. Use this address for testnet adapter
3909
+ * integrations (e.g., Arc Testnet).
3910
+ */
3911
+ const ADAPTER_CONTRACT_EVM_TESTNET = '0xBBD70b01a1CAbc96d5b7b129Ae1AAabdf50dd40b';
3794
3912
 
3795
3913
  /**
3796
3914
  * Arc Testnet chain definition
@@ -3839,6 +3957,7 @@ const ArcTestnet = defineChain({
3839
3957
  },
3840
3958
  kitContracts: {
3841
3959
  bridge: BRIDGE_CONTRACT_EVM_TESTNET,
3960
+ adapter: ADAPTER_CONTRACT_EVM_TESTNET,
3842
3961
  },
3843
3962
  });
3844
3963
 
@@ -4529,7 +4648,7 @@ const HyperEVM = defineChain({
4529
4648
  },
4530
4649
  chainId: 999,
4531
4650
  isTestnet: false,
4532
- explorerUrl: 'https://app.hyperliquid.xyz/explorer/tx/{hash}',
4651
+ explorerUrl: 'https://hyperevmscan.io/tx/{hash}',
4533
4652
  rpcEndpoints: ['https://rpc.hyperliquid.xyz/evm'],
4534
4653
  eurcAddress: null,
4535
4654
  usdcAddress: '0xb88339CB7199b77E23DB6E890353E22632Ba630f',
@@ -6494,6 +6613,41 @@ const bridgeChainIdentifierSchema = z.union([
6494
6613
  message: `Chain "${chainDef.name}" (${chainDef.chain}) is not supported for bridging. Only chains in the BridgeChain enum support CCTPv2 bridging.`,
6495
6614
  })),
6496
6615
  ]);
6616
+ /**
6617
+ * Zod schema for validating earn-specific chain identifiers.
6618
+ *
6619
+ * Validate that the provided chain is supported for earn (vault
6620
+ * deposit/withdraw) operations. Currently only Ethereum is
6621
+ * supported.
6622
+ *
6623
+ * Accept an EarnChain enum value, a matching string literal, or
6624
+ * a ChainDefinition for a supported chain.
6625
+ *
6626
+ * @example
6627
+ * ```typescript
6628
+ * import { earnChainIdentifierSchema } from '@core/chains'
6629
+ * import { EarnChain, Ethereum } from '@core/chains'
6630
+ *
6631
+ * // Valid
6632
+ * earnChainIdentifierSchema.parse(EarnChain.Ethereum)
6633
+ * earnChainIdentifierSchema.parse('Ethereum')
6634
+ * earnChainIdentifierSchema.parse(Ethereum)
6635
+ *
6636
+ * // Invalid (throws ZodError)
6637
+ * earnChainIdentifierSchema.parse('Solana')
6638
+ * ```
6639
+ */
6640
+ z.union([
6641
+ z.string().refine((val) => val in EarnChain, (val) => ({
6642
+ message: `"${val}" is not a supported earn chain. ` +
6643
+ `Supported chains: ${Object.values(EarnChain).join(', ')}`,
6644
+ })),
6645
+ z.nativeEnum(EarnChain),
6646
+ chainDefinitionSchema$2.refine((chain) => chain.chain in EarnChain, (chain) => ({
6647
+ message: `"${chain.chain}" is not a supported earn chain. ` +
6648
+ `Supported chains: ${Object.values(EarnChain).join(', ')}`,
6649
+ })),
6650
+ ]);
6497
6651
 
6498
6652
  /**
6499
6653
  * @packageDocumentation
@@ -6861,18 +7015,21 @@ function getSwapOkTokenStatus(tokenIn, tokenOut, chain, tokenRegistry, okSymbols
6861
7015
  * A chain supports swaps if ALL conditions are met:
6862
7016
  * 1. Is a member of the {@link SwapChain} enum
6863
7017
  * 2. Has CCTPv2 support (adapter contract deployed)
6864
- * 3. Is mainnet (not testnet)
7018
+ *
7019
+ * Testnets are allowed only when explicitly listed in the
7020
+ * {@link SwapChain} enum (e.g., Arc_Testnet).
6865
7021
  *
6866
7022
  * @param chain - Chain definition to check
6867
7023
  * @returns true if chain supports swap operations
6868
7024
  *
6869
7025
  * @example
6870
7026
  * ```typescript
6871
- * import { isSwapSupportedChain, Ethereum, Arbitrum, Sui } from '@core/chains'
7027
+ * import { isSwapSupportedChain, Ethereum, Arbitrum, Sui, ArcTestnet } from '@core/chains'
6872
7028
  *
6873
- * isSwapSupportedChain(Ethereum) // true (has CCTPv2, mainnet)
6874
- * isSwapSupportedChain(Arbitrum) // true (has CCTPv2, mainnet)
6875
- * isSwapSupportedChain(Sui) // false (no CCTPv2 yet)
7029
+ * isSwapSupportedChain(Ethereum) // true (has CCTPv2, mainnet)
7030
+ * isSwapSupportedChain(Arbitrum) // true (has CCTPv2, mainnet)
7031
+ * isSwapSupportedChain(ArcTestnet) // true (has CCTPv2, whitelisted testnet)
7032
+ * isSwapSupportedChain(Sui) // false (no CCTPv2 yet)
6876
7033
  * ```
6877
7034
  *
6878
7035
  * @remarks
@@ -6883,18 +7040,12 @@ function getSwapOkTokenStatus(tokenIn, tokenOut, chain, tokenRegistry, okSymbols
6883
7040
  * No code changes needed when new chains are added!
6884
7041
  */
6885
7042
  function isSwapSupportedChain(chain) {
6886
- // Must be in the SwapChain enum
6887
7043
  if (!(chain.chain in SwapChain)) {
6888
7044
  return false;
6889
7045
  }
6890
- // Must have CCTPv2 support (adapter contract)
6891
7046
  if (!isCCTPV2Supported(chain)) {
6892
7047
  return false;
6893
7048
  }
6894
- // Must be mainnet (no testnet swaps)
6895
- if (chain.isTestnet) {
6896
- return false;
6897
- }
6898
7049
  return true;
6899
7050
  }
6900
7051
  /**
@@ -6904,7 +7055,6 @@ function isSwapSupportedChain(chain) {
6904
7055
  * Dynamically filter chain definitions to include only those that:
6905
7056
  * - Are members of the {@link SwapChain} enum
6906
7057
  * - Have CCTPv2 support
6907
- * - Are mainnets
6908
7058
  *
6909
7059
  * This function is pure and accepts chains as parameter to avoid
6910
7060
  * circular dependencies.
@@ -7508,8 +7658,15 @@ const pollApiWithValidation = async (url, method, isValidType, config = {}, body
7508
7658
  }
7509
7659
  }
7510
7660
  }
7511
- // After the loop: we're guaranteed to have a lastError
7512
- throw new Error(`Maximum retry attempts (${String(effectiveConfig.maxRetries)}) exceeded: ${String(lastError?.message)}`);
7661
+ // Preserve responseBody from the last attempt so upstream parsers
7662
+ // (e.g. parseApiError) can inspect the server's structured response.
7663
+ const retryError = new Error(`Maximum retry attempts (${String(effectiveConfig.maxRetries)}) exceeded: ${String(lastError?.message)}`);
7664
+ if (lastError !== undefined &&
7665
+ 'responseBody' in lastError &&
7666
+ lastError.responseBody !== undefined) {
7667
+ retryError.responseBody = lastError.responseBody;
7668
+ }
7669
+ throw retryError;
7513
7670
  };
7514
7671
  /**
7515
7672
  * Convenience function for making GET requests with validation.
@@ -8200,6 +8357,7 @@ const USDC = {
8200
8357
  // =========================================================================
8201
8358
  // Testnets (alphabetically sorted)
8202
8359
  // =========================================================================
8360
+ [Blockchain.Arc_Testnet]: '0x3600000000000000000000000000000000000000',
8203
8361
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
8204
8362
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
8205
8363
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
@@ -8276,6 +8434,8 @@ const EURC = {
8276
8434
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
8277
8435
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
8278
8436
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
8437
+ // Testnets
8438
+ [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
8279
8439
  },
8280
8440
  };
8281
8441
 
@@ -9199,8 +9359,88 @@ function buildForwardingHookData() {
9199
9359
  return cachedHookDataHex;
9200
9360
  }
9201
9361
 
9362
+ const DEFAULTS = {
9363
+ maxRetries: 3,
9364
+ baseDelayMs: 1000,
9365
+ maxDelayMs: 15_000,
9366
+ deadlineMs: undefined,
9367
+ jitter: true,
9368
+ isRetryable: () => true,
9369
+ };
9370
+ function resolveOptions(options) {
9371
+ if (options === undefined)
9372
+ return DEFAULTS;
9373
+ return {
9374
+ maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
9375
+ baseDelayMs: options.baseDelayMs ?? DEFAULTS.baseDelayMs,
9376
+ maxDelayMs: options.maxDelayMs ?? DEFAULTS.maxDelayMs,
9377
+ deadlineMs: options.deadlineMs ?? DEFAULTS.deadlineMs,
9378
+ jitter: options.jitter ?? DEFAULTS.jitter,
9379
+ isRetryable: options.isRetryable ?? DEFAULTS.isRetryable,
9380
+ };
9381
+ }
9382
+ /**
9383
+ * Calculate exponential backoff delay with optional jitter.
9384
+ *
9385
+ * @param attempt - 1-indexed retry attempt number.
9386
+ * @param config - Resolved retry configuration.
9387
+ * @returns Delay in milliseconds.
9388
+ */
9389
+ function calculateDelay(attempt, config) {
9390
+ let delay = config.baseDelayMs * Math.pow(2, attempt - 1);
9391
+ delay = Math.min(delay, config.maxDelayMs);
9392
+ if (config.jitter) {
9393
+ const jitterFactor = 0.75 + Math.random() * 0.5; // NOSONAR - not security-sensitive
9394
+ delay = Math.round(delay * jitterFactor);
9395
+ }
9396
+ return delay;
9397
+ }
9398
+ /**
9399
+ * Retry an async function with exponential backoff and jitter.
9400
+ *
9401
+ * This is a lightweight standalone utility with no `@core/runtime`
9402
+ * dependency, suitable for use in adapters and providers that do not
9403
+ * participate in the middleware pipeline.
9404
+ *
9405
+ * @typeParam T - The resolved value type.
9406
+ * @param fn - The async function to execute (and potentially retry).
9407
+ * @param options - Retry configuration.
9408
+ * @returns The resolved value of `fn`.
9409
+ * @throws The last error when all retry attempts are exhausted, or
9410
+ * immediately when `isRetryable` returns `false`.
9411
+ *
9412
+ * @example
9413
+ * ```typescript
9414
+ * import { retryAsync } from '@core/utils'
9415
+ *
9416
+ * const receipt = await retryAsync(
9417
+ * () => adapter.waitForTransaction(txHash, { confirmations: 1 }, chain),
9418
+ * { maxRetries: 3, isRetryable: (err) => isTransientRpcError(err) },
9419
+ * )
9420
+ * ```
9421
+ */
9422
+ async function retryAsync(fn, options) {
9423
+ const config = resolveOptions(options);
9424
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
9425
+ try {
9426
+ return await fn();
9427
+ }
9428
+ catch (error) {
9429
+ const pastDeadline = config.deadlineMs !== undefined && Date.now() >= config.deadlineMs;
9430
+ const isLastAttempt = attempt >= config.maxRetries;
9431
+ if (isLastAttempt || pastDeadline || !config.isRetryable(error)) {
9432
+ throw error;
9433
+ }
9434
+ const delayMs = calculateDelay(attempt + 1, config);
9435
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
9436
+ }
9437
+ }
9438
+ /* istanbul ignore next: unreachable safety throw for TypeScript */
9439
+ throw new Error('retryAsync: unreachable');
9440
+ }
9441
+
9202
9442
  var name$1 = "@circle-fin/bridge-kit";
9203
- var version$2 = "1.8.1";
9443
+ var version$2 = "1.8.2";
9204
9444
  var pkg$2 = {
9205
9445
  name: name$1,
9206
9446
  version: version$2};
@@ -12716,10 +12956,13 @@ async function executePreparedChainRequest({ name, request, adapter, chain, conf
12716
12956
  }
12717
12957
  const txHash = await request.execute();
12718
12958
  step.txHash = txHash;
12719
- const transaction = await adapter.waitForTransaction(txHash, {
12720
- confirmations,
12721
- timeout,
12722
- }, chain);
12959
+ const retryOptions = {
12960
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
12961
+ };
12962
+ if (timeout !== undefined) {
12963
+ retryOptions.deadlineMs = Date.now() + timeout;
12964
+ }
12965
+ const transaction = await retryAsync(async () => adapter.waitForTransaction(txHash, { confirmations, timeout }, chain), retryOptions);
12723
12966
  step.state = transaction.blockNumber ? 'success' : 'error';
12724
12967
  step.data = transaction;
12725
12968
  // Generate explorer URL for the step
@@ -13451,7 +13694,12 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
13451
13694
  return step;
13452
13695
  }
13453
13696
  try {
13454
- const transaction = await adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain);
13697
+ const transaction = await retryAsync(async () => adapter.waitForTransaction(receipt.txHash, { confirmations: 1 }, chain), {
13698
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, {
13699
+ chain: chain.name,
13700
+ txHash: receipt.txHash,
13701
+ })),
13702
+ });
13455
13703
  step.state = transaction.blockNumber === undefined ? 'error' : 'success';
13456
13704
  step.data = transaction;
13457
13705
  if (transaction.blockNumber === undefined) {
@@ -13467,7 +13715,7 @@ async function buildBatchedStep(name, receipt, batchId, adapter, chain) {
13467
13715
  return step;
13468
13716
  }
13469
13717
 
13470
- var version$1 = "1.6.1";
13718
+ var version$1 = "1.6.2";
13471
13719
  var pkg$1 = {
13472
13720
  version: version$1};
13473
13721
 
@@ -14208,7 +14456,10 @@ async function waitForPendingTransaction(pendingStep, adapter, chain) {
14208
14456
  message: `Cannot wait for pending ${pendingStep.name}: no transaction hash available`,
14209
14457
  });
14210
14458
  }
14211
- const txReceipt = await adapter.waitForTransaction(pendingStep.txHash, undefined, chain);
14459
+ const txHash = pendingStep.txHash;
14460
+ const txReceipt = await retryAsync(async () => adapter.waitForTransaction(txHash, undefined, chain), {
14461
+ isRetryable: (err) => isRetryableError$1(parseBlockchainError(err, { chain: chain.name, txHash })),
14462
+ });
14212
14463
  // Check if transaction was confirmed on-chain
14213
14464
  if (!txReceipt.blockNumber) {
14214
14465
  return {
@@ -16277,732 +16528,380 @@ const createBridgeKit = (context) => {
16277
16528
  };
16278
16529
 
16279
16530
  var name = "@circle-fin/swap-kit";
16280
- var version = "1.0.3";
16531
+ var version = "1.1.0";
16281
16532
  var pkg = {
16282
16533
  name: name,
16283
16534
  version: version};
16284
16535
 
16285
16536
  /**
16286
- * Schema for validating AdapterContext.
16287
- * Must always contain both adapter and chain explicitly.
16288
- * Optionally includes address for developer-controlled adapters.
16537
+ * @packageDocumentation
16538
+ * @module StablecoinServiceSwapSchemas
16539
+ *
16540
+ * Zod validation schemas for Stablecoin Service Swap Provider parameters.
16541
+ *
16542
+ * This module defines runtime validation schemas using Zod for type-safe
16543
+ * validation of swap requests and service responses.
16289
16544
  */
16290
- const adapterContextSchema$2 = z.object({
16291
- adapter: adapterSchema$1,
16292
- chain: swapChainIdentifierSchema,
16293
- address: z.string().optional(),
16294
- });
16295
16545
  /**
16296
- * Schema for validating allowance strategy values.
16546
+ * Schema for destination address (to): must be a valid EVM or Solana address.
16547
+ * Catches obviously malformed addresses at parse time; chain-specific validation
16548
+ * is performed in buildServiceParams.
16297
16549
  */
16298
- const allowanceStrategySchema$1 = z.enum(['permit', 'approve']);
16550
+ const destinationAddressSchema = z.union([
16551
+ evmAddressSchema,
16552
+ solanaAddressSchema,
16553
+ ]);
16299
16554
  /**
16300
- * Schema for validating SwapKit custom fee configuration.
16555
+ * Zod schema for allowance strategy.
16301
16556
  *
16302
- * Supports two mutually exclusive approaches:
16557
+ * Validates the allowance strategy for token approvals.
16558
+ */
16559
+ const allowanceStrategySchema$1 = z.enum(['permit', 'approve'], {
16560
+ invalid_type_error: 'allowanceStrategy must be either "permit" or "approve"',
16561
+ });
16562
+ /**
16563
+ * Schema for validating service swap custom fee configuration.
16564
+ *
16565
+ * Supports SwapKit fee approaches:
16303
16566
  * - Percentage-based: percentageBps + recipientAddress
16304
16567
  * - Callback-based: amount + recipientAddress
16305
- *
16306
- * @remarks
16307
- * Mutual exclusivity is validated at runtime in buildServiceParams.
16308
- * Chain-specific address validation is performed in resolveSwapConfig.
16309
16568
  */
16310
- const swapCustomFeeSchema = z
16569
+ const serviceSwapCustomFeeSchema = z
16311
16570
  .object({
16312
- /**
16313
- * Fee percentage in basis points (percentage approach).
16314
- * 100 bps = 1%, must be > 0 and <= 10000.
16315
- */
16316
- percentageBps: z.number().int().positive().max(10000),
16317
- /**
16318
- * Fee recipient address (required).
16319
- * Must be a valid EVM address or Solana address.
16320
- */
16321
- recipientAddress: z
16322
- .string()
16323
- .refine((value) => evmAddressSchema.safeParse(value).success ||
16324
- solanaAddressSchema.safeParse(value).success, {
16325
- message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)',
16326
- }),
16571
+ percentageBps: z.number().int().positive().max(10000).optional(),
16572
+ amount: z.string().optional(),
16573
+ recipientAddress: z.string().min(1).optional(),
16327
16574
  })
16328
16575
  .strict();
16329
16576
  /**
16330
- * Schema for validating swap configuration options.
16577
+ * Zod schema for swap configuration.
16331
16578
  *
16332
- * Validates:
16333
- * - allowanceStrategy: Either 'permit' or 'approve'
16334
- * - slippageBps: Optional positive number for slippage tolerance
16335
- * - stopLimit: Optional decimal string for minimum output
16336
- * - customFee: Optional fee configuration
16337
- * - kitKey: Optional string identifier
16579
+ * Validates the optional configuration object for swap operations.
16580
+ * Uses shared validation utilities from \@core/provider for consistency.
16338
16581
  */
16339
- const swapConfigSchema = z.object({
16582
+ const serviceSwapConfigSchema = z.object({
16340
16583
  allowanceStrategy: allowanceStrategySchema$1.optional(),
16341
- slippageBps: z.number().int().min(0).optional(),
16584
+ slippageBps: z
16585
+ .number({
16586
+ invalid_type_error: 'slippageBps must be a number',
16587
+ })
16588
+ .int('slippageBps must be an integer')
16589
+ .min(0, 'slippageBps must be non-negative')
16590
+ .max(10000, 'slippageBps must be at most 10000 (100%)')
16591
+ .optional(),
16342
16592
  stopLimit: z
16343
- .string()
16344
- .min(1, 'Required')
16345
- .pipe(createDecimalStringValidator({
16346
- allowZero: true,
16347
- 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.',
16348
- attributeName: 'stopLimit',
16349
- })(z.string()))
16593
+ .string({
16594
+ invalid_type_error: 'stopLimit must be a string',
16595
+ })
16596
+ .min(1, 'stopLimit is required when provided')
16597
+ .regex(/^\d+$/, 'stopLimit must be an integer string in base units (e.g., "50000000" for 50 USDC with 6 decimals)')
16598
+ .refine((val) => {
16599
+ try {
16600
+ return BigInt(val) > 0n;
16601
+ }
16602
+ catch {
16603
+ return false;
16604
+ }
16605
+ }, {
16606
+ message: 'stopLimit must be greater than 0',
16607
+ })
16608
+ .optional(),
16609
+ customFee: serviceSwapCustomFeeSchema.optional(),
16610
+ kitKey: z
16611
+ .string({
16612
+ invalid_type_error: 'kitKey must be a string',
16613
+ })
16614
+ .min(1, 'kitKey must be a non-empty string')
16615
+ .optional(),
16616
+ provider: z
16617
+ .string({
16618
+ invalid_type_error: 'provider must be a string',
16619
+ })
16620
+ .min(1, 'provider must be a non-empty string')
16350
16621
  .optional(),
16351
- customFee: swapCustomFeeSchema.optional(),
16352
- kitKey: z.string().optional(),
16353
16622
  });
16354
- const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
16355
- const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
16356
- const BASE58_CHARS_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
16357
16623
  /**
16358
- * Broad heuristic: return true when the input is composed entirely of
16359
- * base58 characters and is at least 32 characters long (the minimum
16360
- * length of a valid Solana address).
16624
+ * Zod schema for adapter context.
16361
16625
  *
16362
- * The caller is expected to have already excluded valid Solana
16363
- * addresses (via {@link SOLANA_ADDRESS_REGEX}) before invoking this
16364
- * helper, so in practice it only matches over-length base58 strings
16365
- * (45+ chars) that are plausibly a malformed Solana address.
16626
+ * Validates the adapter context which contains the adapter and chain.
16627
+ *
16628
+ * @remarks
16629
+ * Optionally includes address for developer-controlled adapters.
16366
16630
  */
16367
- function looksLikeSolanaAddress(value) {
16368
- return BASE58_CHARS_REGEX.test(value) && value.length >= 32;
16369
- }
16370
- const MAX_DISPLAY_LENGTH = 30;
16371
- /** Truncate a value for safe inclusion in error messages. */
16372
- function truncate(value) {
16373
- return value.length > MAX_DISPLAY_LENGTH
16374
- ? `${value.slice(0, MAX_DISPLAY_LENGTH)}...`
16375
- : value;
16376
- }
16631
+ const adapterContextSchema$2 = z.object({
16632
+ adapter: adapterSchema$1,
16633
+ chain: chainIdentifierSchema,
16634
+ address: z.string().optional(),
16635
+ });
16377
16636
  /**
16378
- * Schema for validating swap token input.
16379
- *
16380
- * Accepts either:
16381
- * - Token symbols from the supported swap token registry ('USDC', 'WETH', 'NATIVE', etc.)
16382
- * - Token addresses (EVM: 0x..., Solana: base58)
16637
+ * Zod schema for ServiceSwapParams.
16383
16638
  *
16384
- * This allows swapping both supported tokens (by symbol or address) and
16385
- * arbitrary tokens (by address only), as long as at least one token is
16386
- * an "OK token" for fee collection purposes.
16387
- *
16388
- * @remarks
16389
- * Uses input-shape heuristics to produce contextual error messages:
16390
- * - Starts with `0x` — validated as EVM address
16391
- * - Recognized symbol — accepted
16392
- * - 32–44 base58 characters — accepted as Solana address
16393
- * - Otherwise — reports the most likely error (unsupported symbol,
16394
- * malformed Solana address, or malformed EVM address)
16395
- *
16396
- * Runtime validation in `isOkToken` determines if the token is supported
16397
- * for fee collection. This schema only validates the format.
16398
- */
16399
- let _symbolResult;
16400
- const swapTokenSchema = z
16401
- .string()
16402
- .superRefine((value, ctx) => {
16403
- if (value.startsWith('0x')) {
16404
- if (!EVM_ADDRESS_REGEX.test(value)) {
16405
- ctx.addIssue({
16406
- code: z.ZodIssueCode.custom,
16407
- message: `Invalid EVM token address format. Expected '0x' followed by 40 hexadecimal characters, but received: '${truncate(value)}'`,
16408
- });
16409
- }
16410
- return;
16411
- }
16412
- _symbolResult = supportedSwapTokenSchema.safeParse(value);
16413
- if (_symbolResult.success) {
16414
- return;
16415
- }
16416
- if (SOLANA_ADDRESS_REGEX.test(value)) {
16417
- return;
16418
- }
16419
- if (looksLikeSolanaAddress(value)) {
16420
- ctx.addIssue({
16421
- code: z.ZodIssueCode.custom,
16422
- message: `Invalid Solana token address format. Expected 32-44 base58 characters, but received: '${truncate(value)}'`,
16423
- });
16424
- return;
16425
- }
16426
- ctx.addIssue({
16427
- code: z.ZodIssueCode.custom,
16428
- 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).`,
16429
- });
16430
- })
16431
- .transform((value) => (_symbolResult?.success ? _symbolResult.data : value));
16432
- // Amount-in validation schema - broken out to reduce type complexity
16433
- const amountInSchema = z
16434
- .string()
16435
- .min(1, 'Required')
16436
- .pipe(createDecimalStringValidator({
16437
- allowZero: false,
16438
- regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16439
- attributeName: 'amountIn',
16440
- maxDecimals: 18,
16441
- })(z.string()))
16442
- .describe('The amount of the input token to swap, expressed as a human-readable ' +
16443
- "decimal string in token units (e.g., '0.05' for 0.05 USDC or 0.05 ETH).");
16444
- /**
16445
- * Schema for validating swap parameters with chain identifiers.
16446
- *
16447
- * This schema validates the complete swap operation input, ensuring:
16448
- * - Valid adapter context (adapter + chain + optional address)
16449
- * - Valid tokenIn and tokenOut (symbols or addresses)
16450
- * - Valid amountIn as a positive decimal string
16451
- * - Optional valid configuration
16452
- *
16453
- * The schema validates amounts with up to 18 decimal places to support
16454
- * various token standards.
16639
+ * Validates the input parameters for swap operations including
16640
+ * adapter context, tokens, amount, destination, and optional configuration.
16455
16641
  *
16456
16642
  * @example
16457
16643
  * ```typescript
16458
- * import { swapParamsSchema } from '@circle-fin/swap-kit'
16459
- *
16460
- * // Using token symbols (recommended for supported tokens)
16461
- * const paramsWithSymbols = {
16462
- * from: {
16463
- * adapter: sourceAdapter,
16464
- * chain: 'Ethereum'
16465
- * },
16466
- * tokenIn: 'USDC',
16467
- * tokenOut: 'USDT',
16468
- * amountIn: '100.50', // 100.50 USDC
16469
- * config: {
16470
- * slippageBps: 300,
16471
- * allowanceStrategy: 'permit'
16472
- * }
16473
- * }
16474
- *
16475
- * // Using token addresses (works for any token)
16476
- * const paramsWithAddresses = {
16477
- * from: {
16478
- * adapter: sourceAdapter,
16479
- * chain: 'Base'
16480
- * },
16481
- * tokenIn: '0x4200000000000000000000000000000000000006', // WETH address
16482
- * tokenOut: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // Any token address
16483
- * amountIn: '0.1' // 0.1 WETH
16484
- * }
16485
- *
16486
- * // Using native token alias
16487
- * const paramsWithNative = {
16488
- * from: {
16489
- * adapter: sourceAdapter,
16490
- * chain: 'Ethereum'
16491
- * },
16492
- * tokenIn: 'NATIVE', // ETH on Ethereum, MATIC on Polygon, etc.
16493
- * tokenOut: 'USDC',
16494
- * amountIn: '1.5' // 1.5 ETH
16495
- * }
16644
+ * import { serviceSwapParamsSchema } from '@circle-fin/provider-stablecoin-service-swap'
16496
16645
  *
16497
- * const result = swapParamsSchema.safeParse(paramsWithSymbols)
16498
- * if (result.success) {
16499
- * console.log('Parameters are valid')
16500
- * } else {
16501
- * console.error('Validation failed:', result.error)
16646
+ * const result = serviceSwapParamsSchema.safeParse(params)
16647
+ * if (!result.success) {
16648
+ * console.error('Invalid params:', result.error.issues)
16502
16649
  * }
16503
16650
  * ```
16504
16651
  */
16505
- const swapParamsSchema = z.object({
16652
+ const serviceSwapParamsSchema = z.object({
16506
16653
  from: adapterContextSchema$2,
16507
- tokenIn: swapTokenSchema,
16508
- tokenOut: swapTokenSchema,
16509
- amountIn: amountInSchema,
16510
- config: swapConfigSchema.optional(),
16654
+ tokenIn: z
16655
+ .string({
16656
+ required_error: 'tokenIn is required',
16657
+ invalid_type_error: 'tokenIn must be a string',
16658
+ })
16659
+ .min(1, 'tokenIn must be a non-empty string'),
16660
+ tokenOut: z
16661
+ .string({
16662
+ required_error: 'tokenOut is required',
16663
+ invalid_type_error: 'tokenOut must be a string',
16664
+ })
16665
+ .min(1, 'tokenOut must be a non-empty string'),
16666
+ amountIn: z
16667
+ .string({
16668
+ required_error: 'amountIn is required',
16669
+ invalid_type_error: 'amountIn must be a string',
16670
+ })
16671
+ .min(1, 'amountIn is required')
16672
+ .regex(/^\d+$/, 'amountIn must be a numeric string in base units (e.g., "1000000")'),
16673
+ to: destinationAddressSchema,
16674
+ config: serviceSwapConfigSchema.optional(),
16511
16675
  });
16512
16676
  /**
16513
- * Schema for validating SwapKit custom fee policy.
16514
- *
16515
- * Validates the shape of CustomFeePolicy, which lets SDK consumers
16516
- * provide custom fee calculation and fee-recipient resolution logic.
16517
- *
16518
- * - computeFee: required function that returns a fee as a string (or Promise<string>).
16519
- * - resolveFeeRecipientAddress: required function that returns a recipient address as a
16520
- * string (or Promise<string>).
16521
- *
16522
- * This schema only ensures the presence and return types of the functions; it
16523
- * does not validate their argument types.
16677
+ * Zod schema for provider-level custom fee configuration.
16524
16678
  *
16525
- * @example
16526
- * ```typescript
16527
- * import { customFeePolicySchema } from '@circle-fin/swap-kit'
16679
+ * This schema validates the fee configuration for the provider constructor,
16680
+ * where both amount and recipientAddress are required fields.
16528
16681
  *
16529
- * const config = {
16530
- * computeFee: async () => '0.1',
16531
- * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
16532
- * }
16682
+ * @remarks
16683
+ * This is different from the swap-level customFee schema imported from \@core/provider,
16684
+ * which has optional fields for per-swap fee overrides.
16685
+ */
16686
+ z.object({
16687
+ amount: z
16688
+ .string({
16689
+ required_error: 'customFee.amount is required',
16690
+ invalid_type_error: 'customFee.amount must be a string',
16691
+ })
16692
+ .min(1, 'customFee.amount must be a non-empty string')
16693
+ .regex(/^\d+$/, 'customFee.amount must be a numeric string (e.g., "1000000")')
16694
+ .refine((val) => {
16695
+ try {
16696
+ return BigInt(val) > 0n;
16697
+ }
16698
+ catch {
16699
+ return false;
16700
+ }
16701
+ }, {
16702
+ message: 'customFee.amount must be greater than 0',
16703
+ }),
16704
+ recipientAddress: z
16705
+ .string({
16706
+ required_error: 'customFee.recipientAddress is required',
16707
+ invalid_type_error: 'customFee.recipientAddress must be a string',
16708
+ })
16709
+ .min(1, 'customFee.recipientAddress must be a non-empty string'),
16710
+ });
16711
+ /**
16712
+ * Fee amount schema for provider output.
16533
16713
  *
16534
- * const result = customFeePolicySchema.safeParse(config)
16535
- * // result.success === true
16536
- * ```
16714
+ * Accepts both integer strings ("1000000") and decimal strings ("0.002")
16715
+ * because the provider formats raw base-unit amounts into human-readable
16716
+ * decimals via `formatAmount()`.
16537
16717
  */
16538
- const customFeePolicySchema = z
16539
- .object({
16540
- computeFee: z.function().returns(z.string().or(z.promise(z.string()))),
16541
- resolveFeeRecipientAddress: z
16542
- .function()
16543
- .returns(z.string().or(z.promise(z.string()))),
16718
+ const formattedFeeAmountSchema = z
16719
+ .string({
16720
+ required_error: 'fee amount is required',
16721
+ invalid_type_error: 'fee amount must be a string',
16544
16722
  })
16545
- .strict();
16546
-
16547
- const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
16723
+ .min(1, 'fee amount must be a non-empty string')
16724
+ .regex(/^\d+(\.\d+)?$/, 'fee amount must be a non-negative numeric string (e.g., "0.002" or "1000000")');
16548
16725
  /**
16549
- * Assert that the provided value conforms to {@link CustomFeePolicy}.
16726
+ * Zod schema for individual fee entry.
16550
16727
  *
16551
- * This function validates the custom fee policy configuration using the
16552
- * customFeePolicySchema. It ensures that both required functions
16553
- * (computeFee and resolveFeeRecipientAddress) are present and have
16554
- * the correct return types.
16728
+ * Validates ServiceSwapFee structure. Fee amounts can be:
16729
+ * - A valid non-negative numeric string, integer or decimal (including "0")
16730
+ * - null (when fee information is not available)
16555
16731
  *
16556
- * Throws a structured error with detailed validation messages if the
16557
- * configuration is malformed. Uses state tracking to avoid duplicate
16558
- * validations.
16732
+ * @remarks
16733
+ * Zero fee amounts are valid and represent scenarios where no fee is charged.
16734
+ */
16735
+ const serviceSwapFeeSchema = z.object({
16736
+ token: z
16737
+ .string({
16738
+ required_error: 'fee token is required',
16739
+ invalid_type_error: 'fee token must be a string',
16740
+ })
16741
+ .min(1, 'fee token must be a non-empty string'),
16742
+ amount: z.union([formattedFeeAmountSchema, z.null()]),
16743
+ type: z.enum(['provider', 'swap', 'gas', 'developer']),
16744
+ recipientAddress: z.string().min(1).optional(),
16745
+ });
16746
+ /**
16747
+ * Zod schema for validating ServiceSwapResponse data.
16559
16748
  *
16560
- * @param config - The custom fee policy to validate
16561
- * @throws \{KitError\} If the policy fails validation
16749
+ * This schema validates the estimate response from the Stablecoin Service swap API,
16750
+ * ensuring the estimate output data is properly formatted.
16751
+ *
16752
+ * @remarks
16753
+ * Aligned with CCTP provider pattern - validates only computed response data,
16754
+ * not request parameter echoes.
16562
16755
  *
16563
16756
  * @example
16564
16757
  * ```typescript
16565
- * import { assertCustomFeePolicy } from '@circle-fin/swap-kit'
16566
- *
16567
- * const config = {
16568
- * computeFee: () => '0.1',
16569
- * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
16570
- * }
16758
+ * import { serviceSwapResponseSchema } from '@circle-fin/provider-stablecoin-service-swap'
16571
16759
  *
16572
- * try {
16573
- * assertCustomFeePolicy(config)
16574
- * // If no error is thrown, `config` is a valid CustomFeePolicy
16575
- * } catch (error) {
16576
- * console.error('Invalid fee policy:', error.message)
16760
+ * const result = serviceSwapResponseSchema.safeParse(responseData)
16761
+ * if (!result.success) {
16762
+ * console.error('Invalid response:', result.error.issues)
16577
16763
  * }
16578
16764
  * ```
16579
16765
  */
16580
- function assertCustomFeePolicy(config) {
16581
- // Use validateWithStateTracking to avoid duplicate validations
16582
- // This will skip validation if already validated by this function
16583
- // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
16584
- validateWithStateTracking(config, customFeePolicySchema, 'SwapKit custom fee policy', assertCustomFeePolicySymbol);
16585
- }
16766
+ z
16767
+ .object({
16768
+ stopLimit: z.object({
16769
+ token: z
16770
+ .string({
16771
+ required_error: 'stopLimit.token is required',
16772
+ invalid_type_error: 'stopLimit.token must be a string',
16773
+ })
16774
+ .min(1, 'stopLimit.token must be a non-empty string'),
16775
+ amount: z
16776
+ .string({
16777
+ required_error: 'stopLimit.amount is required',
16778
+ invalid_type_error: 'stopLimit.amount must be a string',
16779
+ })
16780
+ .min(1, 'stopLimit.amount must be a non-empty string'),
16781
+ }, {
16782
+ required_error: 'stopLimit is required',
16783
+ invalid_type_error: 'stopLimit must be an object',
16784
+ }),
16785
+ estimatedOutput: z.object({
16786
+ token: z
16787
+ .string({
16788
+ required_error: 'estimatedOutput.token is required',
16789
+ invalid_type_error: 'estimatedOutput.token must be a string',
16790
+ })
16791
+ .min(1, 'estimatedOutput.token must be a non-empty string'),
16792
+ amount: z
16793
+ .string({
16794
+ required_error: 'estimatedOutput.amount is required',
16795
+ invalid_type_error: 'estimatedOutput.amount must be a string',
16796
+ })
16797
+ .min(1, 'estimatedOutput.amount must be a non-empty string'),
16798
+ }, {
16799
+ required_error: 'estimatedOutput is required',
16800
+ invalid_type_error: 'estimatedOutput must be an object',
16801
+ }),
16802
+ fees: z.array(serviceSwapFeeSchema).optional(),
16803
+ })
16804
+ .passthrough();
16586
16805
 
16587
16806
  /**
16588
- * Symbol used to track that assertSwapParams has validated an object.
16589
- * @internal
16807
+ * @packageDocumentation
16808
+ * @module StablecoinServiceSwapValidation
16809
+ *
16810
+ * Validation utilities for the Stablecoin Service Swap Provider.
16811
+ *
16812
+ * This module provides runtime validation functions and type guards
16813
+ * to ensure swap parameters and service responses conform to expected formats.
16590
16814
  */
16591
- const ASSERT_SWAP_PARAMS_SYMBOL = Symbol('assertSwapParams');
16592
16815
  /**
16593
- * Assert that the provided value conforms to the SwapParams schema.
16594
- *
16595
- * This function validates swap parameters using the provided Zod schema
16596
- * and tracks validation state to avoid duplicate checks. It performs
16597
- * comprehensive validation including:
16598
- * - Adapter context structure
16599
- * - Token specifications
16600
- * - Amount format and range
16601
- * - Optional configuration values
16816
+ * Validates ServiceSwapParams and throws an error if invalid.
16602
16817
  *
16603
- * Throws a structured error with detailed validation messages if
16604
- * any parameter is invalid.
16818
+ * This function performs strict validation and throws a detailed error
16819
+ * if the parameters do not conform to the expected schema. Use this for
16820
+ * validating input parameters to swap operations.
16605
16821
  *
16606
- * @typeParam T - The expected type after validation
16607
16822
  * @param params - The swap parameters to validate
16608
- * @param schema - The Zod schema to validate against
16609
- * @throws \{KitError\} If the parameters fail validation
16823
+ * @throws \{KitError\} If validation fails, with details about validation errors
16610
16824
  *
16611
16825
  * @example
16612
16826
  * ```typescript
16613
- * import { assertSwapParams, swapParamsSchema } from '@circle-fin/swap-kit'
16614
- *
16615
- * const params = {
16616
- * from: { adapter: sourceAdapter, chain: 'Ethereum' },
16617
- * tokenIn: 'USDC',
16618
- * tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
16619
- * amountIn: '100.50'
16620
- * }
16827
+ * import { validateServiceSwapParams } from '@circle-fin/provider-stablecoin-service-swap'
16621
16828
  *
16622
16829
  * try {
16623
- * assertSwapParams(params, swapParamsSchema)
16624
- * // Parameters are valid, proceed with swap
16830
+ * validateServiceSwapParams(params)
16831
+ * // Proceed with swap
16625
16832
  * } catch (error) {
16626
- * console.error('Invalid parameters:', error.message)
16833
+ * console.error('Invalid swap params:', error.message)
16627
16834
  * }
16628
16835
  * ```
16629
16836
  */
16630
- function assertSwapParams(params, schema) {
16631
- // Use validateWithStateTracking to avoid duplicate validations
16632
- // This will skip validation if already validated by this function
16633
- // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
16634
- validateWithStateTracking(params, schema, 'swap parameters', ASSERT_SWAP_PARAMS_SYMBOL);
16635
- }
16837
+ const validateServiceSwapParams = (params) => {
16838
+ const result = serviceSwapParamsSchema.safeParse(params);
16839
+ if (!result.success) {
16840
+ const errors = result.error.issues.map((issue) => issue.message).join(', ');
16841
+ throw new KitError({
16842
+ ...InputError.VALIDATION_FAILED,
16843
+ recoverability: 'FATAL',
16844
+ message: `Invalid ServiceSwapParams: ${errors}.`,
16845
+ cause: {
16846
+ trace: { params, validationErrors: result.error.issues },
16847
+ },
16848
+ });
16849
+ }
16850
+ };
16636
16851
 
16637
16852
  /**
16638
16853
  * @packageDocumentation
16639
- * @module StablecoinServiceSwapSchemas
16640
- *
16641
- * Zod validation schemas for Stablecoin Service Swap Provider parameters.
16854
+ * @module ServiceClientConstants
16642
16855
  *
16643
- * This module defines runtime validation schemas using Zod for type-safe
16644
- * validation of swap requests and service responses.
16645
- */
16646
- /**
16647
- * Schema for destination address (to): must be a valid EVM or Solana address.
16648
- * Catches obviously malformed addresses at parse time; chain-specific validation
16649
- * is performed in buildServiceParams.
16856
+ * Constants for the Stablecoin Service API.
16650
16857
  */
16651
- const destinationAddressSchema = z.union([
16652
- evmAddressSchema,
16653
- solanaAddressSchema,
16654
- ]);
16655
16858
  /**
16656
- * Zod schema for allowance strategy.
16859
+ * Default configuration values for the quote fetcher.
16657
16860
  *
16658
- * Validates the allowance strategy for token approvals.
16659
- */
16660
- const allowanceStrategySchema = z.enum(['permit', 'approve'], {
16661
- invalid_type_error: 'allowanceStrategy must be either "permit" or "approve"',
16662
- });
16663
- /**
16664
- * Schema for validating service swap custom fee configuration.
16861
+ * @remarks
16862
+ * The timeout is set to 30 seconds to match the load balancer timeout.
16863
+ * This accommodates the LiFi DEX aggregator (5s hard cap), Circle Wallets
16864
+ * signing (~500ms p99), internal overhead (~100ms), and network latency.
16665
16865
  *
16666
- * Supports SwapKit fee approaches:
16667
- * - Percentage-based: percentageBps + recipientAddress
16668
- * - Callback-based: amount + recipientAddress
16866
+ * @internal
16669
16867
  */
16670
- const serviceSwapCustomFeeSchema = z
16671
- .object({
16672
- percentageBps: z.number().int().positive().max(10000).optional(),
16673
- amount: z.string().optional(),
16674
- recipientAddress: z.string().min(1).optional(),
16675
- })
16676
- .strict();
16868
+ const DEFAULT_CONFIG = {
16869
+ timeout: 30_000, // 30 seconds - matches load balancer timeout
16870
+ maxRetries: 3, // 3 retries as per requirements
16871
+ retryDelay: 200, // 200ms between retries
16872
+ headers: {
16873
+ 'Content-Type': 'application/json',
16874
+ },
16875
+ };
16677
16876
  /**
16678
- * Zod schema for swap configuration.
16877
+ * Base URL for the Stablecoin Service API.
16679
16878
  *
16680
- * Validates the optional configuration object for swap operations.
16681
- * Uses shared validation utilities from \@core/provider for consistency.
16879
+ * @internal
16682
16880
  */
16683
- const serviceSwapConfigSchema = z.object({
16684
- allowanceStrategy: allowanceStrategySchema.optional(),
16685
- slippageBps: z
16686
- .number({
16687
- invalid_type_error: 'slippageBps must be a number',
16688
- })
16689
- .int('slippageBps must be an integer')
16690
- .min(0, 'slippageBps must be non-negative')
16691
- .max(10000, 'slippageBps must be at most 10000 (100%)')
16692
- .optional(),
16693
- stopLimit: z
16694
- .string({
16695
- invalid_type_error: 'stopLimit must be a string',
16696
- })
16697
- .min(1, 'stopLimit is required when provided')
16698
- .regex(/^\d+$/, 'stopLimit must be an integer string in base units (e.g., "50000000" for 50 USDC with 6 decimals)')
16699
- .refine((val) => {
16700
- try {
16701
- return BigInt(val) > 0n;
16702
- }
16703
- catch {
16704
- return false;
16705
- }
16706
- }, {
16707
- message: 'stopLimit must be greater than 0',
16708
- })
16709
- .optional(),
16710
- customFee: serviceSwapCustomFeeSchema.optional(),
16711
- kitKey: z
16712
- .string({
16713
- invalid_type_error: 'kitKey must be a string',
16714
- })
16715
- .min(1, 'kitKey must be a non-empty string')
16716
- .optional(),
16717
- provider: z
16718
- .string({
16719
- invalid_type_error: 'provider must be a string',
16720
- })
16721
- .min(1, 'provider must be a non-empty string')
16722
- .optional(),
16723
- });
16881
+ const STABLECOIN_SERVICE_BASE_URL = 'https://api.circle.com';
16882
+
16724
16883
  /**
16725
- * Zod schema for adapter context.
16884
+ * @packageDocumentation
16885
+ * @module ServiceClientSchemas
16726
16886
  *
16727
- * Validates the adapter context which contains the adapter and chain.
16887
+ * Zod validation schemas for Stablecoin Service API parameters.
16728
16888
  *
16729
- * @remarks
16730
- * Optionally includes address for developer-controlled adapters.
16889
+ * This module defines runtime validation schemas using Zod for type-safe
16890
+ * validation of API requests and responses.
16731
16891
  */
16732
- const adapterContextSchema$1 = z.object({
16733
- adapter: adapterSchema$1,
16734
- chain: chainIdentifierSchema,
16735
- address: z.string().optional(),
16736
- });
16737
16892
  /**
16738
- * Zod schema for ServiceSwapParams.
16893
+ * Zod schema for validating stop limits.
16739
16894
  *
16740
- * Validates the input parameters for swap operations including
16741
- * adapter context, tokens, amount, destination, and optional configuration.
16895
+ * The stop limit is the minimum acceptable token output amount expressed in
16896
+ * base units. It must be a positive integer string.
16742
16897
  *
16743
16898
  * @example
16744
16899
  * ```typescript
16745
- * import { serviceSwapParamsSchema } from '@circle-fin/provider-stablecoin-service-swap'
16900
+ * import { stopLimitSchema } from '@core/service-client'
16746
16901
  *
16747
- * const result = serviceSwapParamsSchema.safeParse(params)
16902
+ * const result = stopLimitSchema.safeParse('1000000')
16748
16903
  * if (!result.success) {
16749
- * console.error('Invalid params:', result.error.issues)
16750
- * }
16751
- * ```
16752
- */
16753
- const serviceSwapParamsSchema = z.object({
16754
- from: adapterContextSchema$1,
16755
- tokenIn: z
16756
- .string({
16757
- required_error: 'tokenIn is required',
16758
- invalid_type_error: 'tokenIn must be a string',
16759
- })
16760
- .min(1, 'tokenIn must be a non-empty string'),
16761
- tokenOut: z
16762
- .string({
16763
- required_error: 'tokenOut is required',
16764
- invalid_type_error: 'tokenOut must be a string',
16765
- })
16766
- .min(1, 'tokenOut must be a non-empty string'),
16767
- amountIn: z
16768
- .string({
16769
- required_error: 'amountIn is required',
16770
- invalid_type_error: 'amountIn must be a string',
16771
- })
16772
- .min(1, 'amountIn is required')
16773
- .regex(/^\d+$/, 'amountIn must be a numeric string in base units (e.g., "1000000")'),
16774
- to: destinationAddressSchema,
16775
- config: serviceSwapConfigSchema.optional(),
16776
- });
16777
- /**
16778
- * Zod schema for provider-level custom fee configuration.
16779
- *
16780
- * This schema validates the fee configuration for the provider constructor,
16781
- * where both amount and recipientAddress are required fields.
16782
- *
16783
- * @remarks
16784
- * This is different from the swap-level customFee schema imported from \@core/provider,
16785
- * which has optional fields for per-swap fee overrides.
16786
- */
16787
- z.object({
16788
- amount: z
16789
- .string({
16790
- required_error: 'customFee.amount is required',
16791
- invalid_type_error: 'customFee.amount must be a string',
16792
- })
16793
- .min(1, 'customFee.amount must be a non-empty string')
16794
- .regex(/^\d+$/, 'customFee.amount must be a numeric string (e.g., "1000000")')
16795
- .refine((val) => {
16796
- try {
16797
- return BigInt(val) > 0n;
16798
- }
16799
- catch {
16800
- return false;
16801
- }
16802
- }, {
16803
- message: 'customFee.amount must be greater than 0',
16804
- }),
16805
- recipientAddress: z
16806
- .string({
16807
- required_error: 'customFee.recipientAddress is required',
16808
- invalid_type_error: 'customFee.recipientAddress must be a string',
16809
- })
16810
- .min(1, 'customFee.recipientAddress must be a non-empty string'),
16811
- });
16812
- /**
16813
- * Fee amount schema for provider output.
16814
- *
16815
- * Accepts both integer strings ("1000000") and decimal strings ("0.002")
16816
- * because the provider formats raw base-unit amounts into human-readable
16817
- * decimals via `formatAmount()`.
16818
- */
16819
- const formattedFeeAmountSchema = z
16820
- .string({
16821
- required_error: 'fee amount is required',
16822
- invalid_type_error: 'fee amount must be a string',
16823
- })
16824
- .min(1, 'fee amount must be a non-empty string')
16825
- .regex(/^\d+(\.\d+)?$/, 'fee amount must be a non-negative numeric string (e.g., "0.002" or "1000000")');
16826
- /**
16827
- * Zod schema for individual fee entry.
16828
- *
16829
- * Validates ServiceSwapFee structure. Fee amounts can be:
16830
- * - A valid non-negative numeric string, integer or decimal (including "0")
16831
- * - null (when fee information is not available)
16832
- *
16833
- * @remarks
16834
- * Zero fee amounts are valid and represent scenarios where no fee is charged.
16835
- */
16836
- const serviceSwapFeeSchema = z.object({
16837
- token: z
16838
- .string({
16839
- required_error: 'fee token is required',
16840
- invalid_type_error: 'fee token must be a string',
16841
- })
16842
- .min(1, 'fee token must be a non-empty string'),
16843
- amount: z.union([formattedFeeAmountSchema, z.null()]),
16844
- type: z.enum(['provider', 'swap', 'gas', 'developer']),
16845
- recipientAddress: z.string().min(1).optional(),
16846
- });
16847
- /**
16848
- * Zod schema for validating ServiceSwapResponse data.
16849
- *
16850
- * This schema validates the estimate response from the Stablecoin Service swap API,
16851
- * ensuring the estimate output data is properly formatted.
16852
- *
16853
- * @remarks
16854
- * Aligned with CCTP provider pattern - validates only computed response data,
16855
- * not request parameter echoes.
16856
- *
16857
- * @example
16858
- * ```typescript
16859
- * import { serviceSwapResponseSchema } from '@circle-fin/provider-stablecoin-service-swap'
16860
- *
16861
- * const result = serviceSwapResponseSchema.safeParse(responseData)
16862
- * if (!result.success) {
16863
- * console.error('Invalid response:', result.error.issues)
16864
- * }
16865
- * ```
16866
- */
16867
- z
16868
- .object({
16869
- stopLimit: z.object({
16870
- token: z
16871
- .string({
16872
- required_error: 'stopLimit.token is required',
16873
- invalid_type_error: 'stopLimit.token must be a string',
16874
- })
16875
- .min(1, 'stopLimit.token must be a non-empty string'),
16876
- amount: z
16877
- .string({
16878
- required_error: 'stopLimit.amount is required',
16879
- invalid_type_error: 'stopLimit.amount must be a string',
16880
- })
16881
- .min(1, 'stopLimit.amount must be a non-empty string'),
16882
- }, {
16883
- required_error: 'stopLimit is required',
16884
- invalid_type_error: 'stopLimit must be an object',
16885
- }),
16886
- estimatedOutput: z.object({
16887
- token: z
16888
- .string({
16889
- required_error: 'estimatedOutput.token is required',
16890
- invalid_type_error: 'estimatedOutput.token must be a string',
16891
- })
16892
- .min(1, 'estimatedOutput.token must be a non-empty string'),
16893
- amount: z
16894
- .string({
16895
- required_error: 'estimatedOutput.amount is required',
16896
- invalid_type_error: 'estimatedOutput.amount must be a string',
16897
- })
16898
- .min(1, 'estimatedOutput.amount must be a non-empty string'),
16899
- }, {
16900
- required_error: 'estimatedOutput is required',
16901
- invalid_type_error: 'estimatedOutput must be an object',
16902
- }),
16903
- fees: z.array(serviceSwapFeeSchema).optional(),
16904
- })
16905
- .passthrough();
16906
-
16907
- /**
16908
- * @packageDocumentation
16909
- * @module StablecoinServiceSwapValidation
16910
- *
16911
- * Validation utilities for the Stablecoin Service Swap Provider.
16912
- *
16913
- * This module provides runtime validation functions and type guards
16914
- * to ensure swap parameters and service responses conform to expected formats.
16915
- */
16916
- /**
16917
- * Validates ServiceSwapParams and throws an error if invalid.
16918
- *
16919
- * This function performs strict validation and throws a detailed error
16920
- * if the parameters do not conform to the expected schema. Use this for
16921
- * validating input parameters to swap operations.
16922
- *
16923
- * @param params - The swap parameters to validate
16924
- * @throws \{KitError\} If validation fails, with details about validation errors
16925
- *
16926
- * @example
16927
- * ```typescript
16928
- * import { validateServiceSwapParams } from '@circle-fin/provider-stablecoin-service-swap'
16929
- *
16930
- * try {
16931
- * validateServiceSwapParams(params)
16932
- * // Proceed with swap
16933
- * } catch (error) {
16934
- * console.error('Invalid swap params:', error.message)
16935
- * }
16936
- * ```
16937
- */
16938
- const validateServiceSwapParams = (params) => {
16939
- const result = serviceSwapParamsSchema.safeParse(params);
16940
- if (!result.success) {
16941
- const errors = result.error.issues.map((issue) => issue.message).join(', ');
16942
- throw new KitError({
16943
- ...InputError.VALIDATION_FAILED,
16944
- recoverability: 'FATAL',
16945
- message: `Invalid ServiceSwapParams: ${errors}.`,
16946
- cause: {
16947
- trace: { params, validationErrors: result.error.issues },
16948
- },
16949
- });
16950
- }
16951
- };
16952
-
16953
- /**
16954
- * @packageDocumentation
16955
- * @module ServiceClientConstants
16956
- *
16957
- * Constants for the Stablecoin Service API.
16958
- */
16959
- /**
16960
- * Default configuration values for the quote fetcher.
16961
- *
16962
- * @remarks
16963
- * The timeout is set to 30 seconds to match the load balancer timeout.
16964
- * This accommodates the LiFi DEX aggregator (5s hard cap), Circle Wallets
16965
- * signing (~500ms p99), internal overhead (~100ms), and network latency.
16966
- *
16967
- * @internal
16968
- */
16969
- const DEFAULT_CONFIG = {
16970
- timeout: 30_000, // 30 seconds - matches load balancer timeout
16971
- maxRetries: 3, // 3 retries as per requirements
16972
- retryDelay: 200, // 200ms between retries
16973
- headers: {
16974
- 'Content-Type': 'application/json',
16975
- },
16976
- };
16977
- /**
16978
- * Base URL for the Stablecoin Service API.
16979
- *
16980
- * @internal
16981
- */
16982
- const STABLECOIN_SERVICE_BASE_URL = 'https://api.circle.com';
16983
-
16984
- /**
16985
- * @packageDocumentation
16986
- * @module ServiceClientSchemas
16987
- *
16988
- * Zod validation schemas for Stablecoin Service API parameters.
16989
- *
16990
- * This module defines runtime validation schemas using Zod for type-safe
16991
- * validation of API requests and responses.
16992
- */
16993
- /**
16994
- * Zod schema for validating stop limits.
16995
- *
16996
- * The stop limit is the minimum acceptable token output amount expressed in
16997
- * base units. It must be a positive integer string.
16998
- *
16999
- * @example
17000
- * ```typescript
17001
- * import { stopLimitSchema } from '@core/service-client'
17002
- *
17003
- * const result = stopLimitSchema.safeParse('1000000')
17004
- * if (!result.success) {
17005
- * console.error('Validation failed:', result.error.issues)
16904
+ * console.error('Validation failed:', result.error.issues)
17006
16905
  * }
17007
16906
  * ```
17008
16907
  */
@@ -17410,6 +17309,31 @@ const createSwapParamsSchema = createSwapRequestSchema.extend({
17410
17309
  */
17411
17310
  apiKey: apiKeySchema,
17412
17311
  });
17312
+ /**
17313
+ * Zod schema for validating GetSwapStatusResponse data.
17314
+ */
17315
+ const getSwapStatusResponseSchema = z.object({
17316
+ status: z.enum(['DONE', 'PENDING', 'NOT_FOUND', 'FAILED']),
17317
+ amountOut: z.string().optional(),
17318
+ });
17319
+ /**
17320
+ * Zod schema for validating GetSwapStatusParams.
17321
+ */
17322
+ const getSwapStatusParamsSchema = z.object({
17323
+ txHash: z
17324
+ .string({
17325
+ required_error: 'txHash is required',
17326
+ invalid_type_error: 'txHash must be a string',
17327
+ })
17328
+ .min(1, 'txHash is required and must be a non-empty string'),
17329
+ chain: z
17330
+ .string({
17331
+ required_error: 'chain is required',
17332
+ invalid_type_error: 'chain must be a string',
17333
+ })
17334
+ .min(1, 'chain is required and must be a non-empty string'),
17335
+ apiKey: apiKeySchema,
17336
+ });
17413
17337
  /**
17414
17338
  * Zod schema for validating CreateSwapResponse payloads.
17415
17339
  */
@@ -17585,6 +17509,27 @@ const isCreateSwapResponse = (obj) => createSwapResponseSchema.safeParse(obj).su
17585
17509
  * @throws If validation fails.
17586
17510
  */
17587
17511
  const parseCreateSwapResponse = (obj) => createSwapResponseSchema.parse(obj);
17512
+ /**
17513
+ * Type guard to validate GetSwapStatusResponse objects.
17514
+ *
17515
+ * @param obj - The object to validate
17516
+ * @returns True if the object is a valid GetSwapStatusResponse, false otherwise
17517
+ *
17518
+ * @example
17519
+ * ```typescript
17520
+ * import { isGetSwapStatusResponse } from '@core/service-client'
17521
+ *
17522
+ * const response = await fetch('/v1/stablecoinKits/swap/status?...')
17523
+ * const data = await response.json()
17524
+ *
17525
+ * if (isGetSwapStatusResponse(data)) {
17526
+ * console.log('Swap status:', data.status)
17527
+ * } else {
17528
+ * console.error('Invalid response format')
17529
+ * }
17530
+ * ```
17531
+ */
17532
+ const isGetSwapStatusResponse = (obj) => getSwapStatusResponseSchema.safeParse(obj).success;
17588
17533
 
17589
17534
  /**
17590
17535
  * @packageDocumentation
@@ -17819,6 +17764,61 @@ const getQuote = async (params) => {
17819
17764
  return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
17820
17765
  };
17821
17766
 
17767
+ /**
17768
+ * Build the full URL for the swap-status polling endpoint.
17769
+ *
17770
+ * @param params - Swap status parameters containing the transaction
17771
+ * hash and chain identifier.
17772
+ * @returns Fully-qualified URL string with query parameters set.
17773
+ *
17774
+ * @example
17775
+ * ```typescript
17776
+ * import { buildSwapStatusUrl } from '@core/service-client'
17777
+ *
17778
+ * const url = buildSwapStatusUrl({
17779
+ * txHash: '0xabc123',
17780
+ * chain: 'Ethereum',
17781
+ * apiKey: 'my-api-key',
17782
+ * })
17783
+ * // => 'https://…/v1/stablecoinKits/swap/status?txHash=0xabc123&chain=Ethereum'
17784
+ * ```
17785
+ */
17786
+ const buildSwapStatusUrl = (params) => {
17787
+ const url = new URL('/v1/stablecoinKits/swap/status', STABLECOIN_SERVICE_BASE_URL);
17788
+ url.searchParams.set('txHash', params.txHash);
17789
+ url.searchParams.set('chain', params.chain);
17790
+ return url.toString();
17791
+ };
17792
+ /**
17793
+ * Fetches the swap status from the Stablecoin Service.
17794
+ *
17795
+ * The proxy resolves the final swap status server-side (up to 30s) and
17796
+ * returns a normalized response.
17797
+ *
17798
+ * @param params - txHash, chain, and apiKey
17799
+ * @returns The swap status with optional amountOut when DONE
17800
+ * @throws KitError if parameter validation fails
17801
+ * @throws KitError if the API returns an error response
17802
+ */
17803
+ const getSwapStatus = async (params) => {
17804
+ const result = getSwapStatusParamsSchema.safeParse(params);
17805
+ if (!result.success) {
17806
+ throw convertZodErrorToStructured(result.error, {
17807
+ txHash: params.txHash,
17808
+ chain: params.chain,
17809
+ });
17810
+ }
17811
+ const url = buildSwapStatusUrl(result.data);
17812
+ const effectiveConfig = {
17813
+ ...DEFAULT_CONFIG,
17814
+ headers: {
17815
+ ...DEFAULT_CONFIG.headers,
17816
+ Authorization: `Bearer ${result.data.apiKey}`,
17817
+ },
17818
+ };
17819
+ return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
17820
+ };
17821
+
17822
17822
  /**
17823
17823
  * Core type definitions for EVM-compatible blockchain transaction execution
17824
17824
  * and gas estimation.
@@ -19422,9 +19422,9 @@ const EIP2612_SUPPORTED_TOKENS = new Set(['USDC']);
19422
19422
  * cast call <USDC_ADDRESS> "version()(string)" --rpc-url <RPC_URL>
19423
19423
  * ```
19424
19424
  *
19425
- * Only swap-supported EVM mainnet chains are included. Solana is excluded since
19426
- * it doesn't use EIP-712 signatures. Values were verified on-chain via
19427
- * `cast call <usdc> "name()(string)"`.
19425
+ * All swap-supported EVM chains (mainnet and whitelisted testnets) are included.
19426
+ * Solana is excluded since it doesn't use EIP-712 signatures. Values were
19427
+ * verified on-chain via `cast call <usdc> "name()(string)"`.
19428
19428
  *
19429
19429
  * @internal
19430
19430
  */
@@ -19445,6 +19445,11 @@ const USDC_PERMIT_METADATA_BY_NAME = {
19445
19445
  XDC: { chainId: XDC.chainId, name: 'USDC', version: '2' },
19446
19446
  HyperEVM: { chainId: HyperEVM.chainId, name: 'USDC', version: '2' },
19447
19447
  Monad: { chainId: Monad.chainId, name: 'USDC', version: '2' },
19448
+ Arc_Testnet: {
19449
+ chainId: ArcTestnet.chainId,
19450
+ name: 'USDC',
19451
+ version: '2',
19452
+ },
19448
19453
  };
19449
19454
  /**
19450
19455
  * Flatten {@link USDC_PERMIT_METADATA_BY_NAME} into a chain-ID-keyed map for O(1) lookup.
@@ -19475,7 +19480,7 @@ const USDC_PERMIT_METADATA = Object.values(USDC_PERMIT_METADATA_BY_NAME).reduce(
19475
19480
  * **Unsupported Scenarios**:
19476
19481
  * - Returns `false` for native currency (ETH, MATIC, etc.)
19477
19482
  * - Returns `false` for non-EVM chains (e.g., Solana)
19478
- * - Returns `false` for chains not in USDC_PERMIT_METADATA (testnets, non-swap chains)
19483
+ * - Returns `false` for chains not in USDC_PERMIT_METADATA (e.g. non-whitelisted testnets, non-swap chains)
19479
19484
  * - Returns `false` for chains without USDC deployed (`chain.usdcAddress === null`)
19480
19485
  * - Returns `false` for tokens not in allowlist
19481
19486
  *
@@ -20757,165 +20762,6 @@ async function formatTokenValue(value, token, chain, adapter) {
20757
20762
  }
20758
20763
  }
20759
20764
 
20760
- const LIFI_STATUS_BASE_URL = 'https://li.quest/v1/status';
20761
- const LIFI_SOLANA_CHAIN_ID = 1151111081099710;
20762
- const LIFI_POLLING_CONFIG = {
20763
- maxRetries: 20,
20764
- retryDelay: 3_000,
20765
- };
20766
- const assertNever = (value) => {
20767
- throw new Error(`Unexpected LI.FI status value: ${String(value)}`);
20768
- };
20769
- const createRetryableLifiStatusError = (status, substatus) => {
20770
- return new KitError({
20771
- ...NetworkError.LIFI_STATUS_PENDING,
20772
- recoverability: 'RETRYABLE',
20773
- message: substatus
20774
- ? `LI.FI status is not ready yet: ${status} (${substatus})`
20775
- : `LI.FI status is not ready yet: ${status}`,
20776
- cause: {
20777
- trace: { status, ...(substatus !== undefined && { substatus }) },
20778
- },
20779
- });
20780
- };
20781
- const createFatalLifiStatusError = (message, trace) => {
20782
- return new KitError({
20783
- ...NetworkError.LIFI_STATUS_FAILED,
20784
- recoverability: 'FATAL',
20785
- message,
20786
- cause: { trace },
20787
- });
20788
- };
20789
- const hasValidReceivingStructure = (receiving) => {
20790
- return (receiving === undefined ||
20791
- (typeof receiving === 'object' &&
20792
- receiving !== null &&
20793
- (!('amount' in receiving) ||
20794
- typeof receiving.amount === 'string')));
20795
- };
20796
- const hasValidLifiStatusStructure = (obj) => {
20797
- return (typeof obj === 'object' &&
20798
- obj !== null &&
20799
- 'status' in obj &&
20800
- ['NOT_FOUND', 'INVALID', 'PENDING', 'DONE', 'FAILED'].includes(String(obj.status)) &&
20801
- (!('substatus' in obj) ||
20802
- typeof obj.substatus === 'string') &&
20803
- hasValidReceivingStructure(obj.receiving));
20804
- };
20805
- const getLifiChainId = (chain) => {
20806
- if (chain.type === 'evm') {
20807
- return chain.chainId;
20808
- }
20809
- if (chain.type === 'solana') {
20810
- return LIFI_SOLANA_CHAIN_ID;
20811
- }
20812
- throw new KitError({
20813
- ...InputError.VALIDATION_FAILED,
20814
- recoverability: 'FATAL',
20815
- message: `LI.FI status polling is not supported for chain type "${chain.type}".`,
20816
- cause: {
20817
- trace: { chain: chain.name, chainType: chain.type },
20818
- },
20819
- });
20820
- };
20821
- const buildLifiStatusUrl = (txHash, lifiChainId) => {
20822
- const url = new URL(LIFI_STATUS_BASE_URL);
20823
- url.searchParams.set('txHash', txHash);
20824
- url.searchParams.set('fromChain', lifiChainId.toString());
20825
- url.searchParams.set('toChain', lifiChainId.toString());
20826
- return url.toString();
20827
- };
20828
- const isLifiStatusDone = (obj) => {
20829
- if (!hasValidLifiStatusStructure(obj)) {
20830
- throw new KitError({
20831
- ...InputError.VALIDATION_FAILED,
20832
- recoverability: 'FATAL',
20833
- message: 'Invalid LI.FI status response structure',
20834
- cause: { trace: obj },
20835
- });
20836
- }
20837
- switch (obj.status) {
20838
- case 'PENDING':
20839
- case 'NOT_FOUND':
20840
- throw createRetryableLifiStatusError(obj.status, obj.substatus);
20841
- case 'FAILED':
20842
- case 'INVALID':
20843
- throw createFatalLifiStatusError(`LI.FI status returned a terminal failure state: ${obj.status}`, {
20844
- status: obj.status,
20845
- ...(obj.substatus !== undefined && { substatus: obj.substatus }),
20846
- });
20847
- case 'DONE':
20848
- if (obj.substatus === 'COMPLETED') {
20849
- return true;
20850
- }
20851
- throw createFatalLifiStatusError(`LI.FI status returned a non-completable terminal state: DONE${obj.substatus ? ` (${obj.substatus})` : ''}`, {
20852
- status: obj.status,
20853
- ...(obj.substatus !== undefined && { substatus: obj.substatus }),
20854
- });
20855
- default:
20856
- return assertNever(obj.status);
20857
- }
20858
- };
20859
- const LIFI_TOTAL_TIMEOUT_MS = 90_000;
20860
- const isLifiNotIndexedError = (error) => {
20861
- return error instanceof Error && error.message.includes('HTTP 400');
20862
- };
20863
- /**
20864
- * Poll the LI.FI status API to retrieve the output amount for a
20865
- * completed swap transaction. Return `undefined` on any failure
20866
- * because the swap has already succeeded on-chain — this is
20867
- * best-effort enrichment only.
20868
- *
20869
- * @param txHash - The transaction hash of the swap to look up.
20870
- * @param chain - The chain definition where the swap was executed.
20871
- * @returns The raw token amount string from LI.FI, or `undefined`
20872
- * if the status could not be retrieved.
20873
- * @throws Never throws — all errors are caught and result in an
20874
- * `undefined` return value.
20875
- *
20876
- * @example
20877
- * ```typescript
20878
- * import { fetchLifiAmountOut } from '../utils'
20879
- * import { Ethereum } from '@core/chains'
20880
- *
20881
- * const amountOut = await fetchLifiAmountOut('0xabc...', Ethereum)
20882
- * if (amountOut !== undefined) {
20883
- * console.log('Output amount:', amountOut)
20884
- * }
20885
- * ```
20886
- */
20887
- const fetchLifiAmountOut = async (txHash, chain) => {
20888
- try {
20889
- const lifiChainId = getLifiChainId(chain);
20890
- const url = buildLifiStatusUrl(txHash, lifiChainId);
20891
- // LI.FI returns HTTP 400 before it indexes the transaction.
20892
- // pollApiGet treats 400 as non-retryable, so we wrap it in our
20893
- // own retry loop that catches the "not indexed yet" 400 and
20894
- // waits for LI.FI to become aware of the transaction.
20895
- const { maxRetries, retryDelay } = LIFI_POLLING_CONFIG;
20896
- const deadline = Date.now() + LIFI_TOTAL_TIMEOUT_MS;
20897
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
20898
- if (Date.now() >= deadline)
20899
- return undefined;
20900
- try {
20901
- const response = await pollApiGet(url, isLifiStatusDone, LIFI_POLLING_CONFIG);
20902
- return response.receiving?.amount;
20903
- }
20904
- catch (error) {
20905
- if (!isLifiNotIndexedError(error) || attempt === maxRetries) {
20906
- throw error;
20907
- }
20908
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
20909
- }
20910
- }
20911
- return undefined;
20912
- }
20913
- catch {
20914
- // Best-effort enrichment only; the swap has already succeeded on-chain.
20915
- return undefined;
20916
- }
20917
- };
20918
-
20919
20765
  /**
20920
20766
  * Safety multiplier applied to locally estimated gas for EVM swap execution.
20921
20767
  * Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
@@ -20994,7 +20840,7 @@ function handleSwapExecutionError(err, txHash, chain) {
20994
20840
  * Dynamically determined based on:
20995
20841
  * - Membership in the SwapChain enum
20996
20842
  * - CCTPv2 support (adapter contract deployed)
20997
- * - Mainnet only (no testnets)
20843
+ * - Whitelisted chains only (testnets allowed when explicitly in SwapChain enum)
20998
20844
  *
20999
20845
  * This list automatically updates when new chains are added.
21000
20846
  *
@@ -21194,7 +21040,7 @@ class StablecoinServiceSwapProvider {
21194
21040
  *
21195
21041
  * @remarks
21196
21042
  * This method validates that:
21197
- * - The chain is a mainnet CCTPv2-supported chain (not testnet)
21043
+ * - The chain is swap-supported (mainnet or whitelisted testnet with CCTPv2)
21198
21044
  * - Both tokens are supported (USDC, USDT, native currency, or token literals)
21199
21045
  * - The tokens are different (no same-token swaps)
21200
21046
  *
@@ -21249,10 +21095,7 @@ class StablecoinServiceSwapProvider {
21249
21095
  * ```
21250
21096
  */
21251
21097
  supportsRoute(tokenInAddress, tokenOutAddress, chain) {
21252
- if (chain.isTestnet) {
21253
- return false;
21254
- }
21255
- if (!isCCTPV2Supported(chain)) {
21098
+ if (!isSwapSupportedChain(chain)) {
21256
21099
  return false;
21257
21100
  }
21258
21101
  // Normalize for same-token check
@@ -21854,7 +21697,22 @@ class StablecoinServiceSwapProvider {
21854
21697
  const swapResultFees = serviceResponse.fees
21855
21698
  ? await this.buildFormattedFees(serviceResponse.fees, chain, adapter, config?.customFee?.recipientAddress)
21856
21699
  : undefined;
21857
- const amountOut = await fetchLifiAmountOut(txHash, chain);
21700
+ // Best-effort enrichment: fetch amountOut via the proxy service.
21701
+ // If the call fails or times out, amountOut is simply omitted.
21702
+ let amountOut;
21703
+ try {
21704
+ const statusResult = await getSwapStatus({
21705
+ txHash,
21706
+ chain: chain.chain,
21707
+ apiKey: serviceParams.apiKey,
21708
+ });
21709
+ if (statusResult.status === 'DONE' && statusResult.amountOut) {
21710
+ amountOut = statusResult.amountOut;
21711
+ }
21712
+ }
21713
+ catch {
21714
+ // Non-fatal — the swap already succeeded on-chain.
21715
+ }
21858
21716
  // Build and return SwapResult
21859
21717
  return {
21860
21718
  tokenIn,
@@ -21982,85 +21840,355 @@ class StablecoinServiceSwapProvider {
21982
21840
  }
21983
21841
 
21984
21842
  /**
21985
- * The default providers that will be used in addition to the providers provided
21986
- * to the createSwapKitContext factory function.
21843
+ * Schema for validating AdapterContext.
21844
+ * Must always contain both adapter and chain explicitly.
21845
+ * Optionally includes address for developer-controlled adapters.
21846
+ */
21847
+ const adapterContextSchema$1 = z.object({
21848
+ adapter: adapterSchema$1,
21849
+ chain: swapChainIdentifierSchema,
21850
+ address: z.string().optional(),
21851
+ });
21852
+ /**
21853
+ * Schema for validating allowance strategy values.
21854
+ */
21855
+ const allowanceStrategySchema = z.enum(['permit', 'approve']);
21856
+ /**
21857
+ * Schema for validating SwapKit custom fee configuration.
21987
21858
  *
21988
- * @returns An array containing the default StablecoinServiceSwapProvider
21989
- * @internal
21859
+ * Supports two mutually exclusive approaches:
21860
+ * - Percentage-based: percentageBps + recipientAddress
21861
+ * - Callback-based: amount + recipientAddress
21862
+ *
21863
+ * @remarks
21864
+ * Mutual exclusivity is validated at runtime in buildServiceParams.
21865
+ * Chain-specific address validation is performed in resolveSwapConfig.
21990
21866
  */
21991
- const getDefaultProviders = () => [new StablecoinServiceSwapProvider()];
21867
+ const swapCustomFeeSchema = z
21868
+ .object({
21869
+ /**
21870
+ * Fee percentage in basis points (percentage approach).
21871
+ * 100 bps = 1%, must be > 0 and <= 10000.
21872
+ */
21873
+ percentageBps: z.number().int().positive().max(10000),
21874
+ /**
21875
+ * Fee recipient address (required).
21876
+ * Must be a valid EVM address or Solana address.
21877
+ */
21878
+ recipientAddress: z
21879
+ .string()
21880
+ .refine((value) => evmAddressSchema.safeParse(value).success ||
21881
+ solanaAddressSchema.safeParse(value).success, {
21882
+ message: 'recipientAddress must be a valid blockchain address: EVM (0x + 40 hex chars) or Solana (base58, 32-44 chars)',
21883
+ }),
21884
+ })
21885
+ .strict();
21992
21886
  /**
21993
- * Create a SwapKit context with validated configuration.
21887
+ * Schema for validating swap configuration options.
21994
21888
  *
21995
- * This factory function initializes a SwapKitContext with default providers
21996
- * and optional custom configuration. It validates any provided custom fee
21997
- * policy and merges default and custom providers, preserving their exact
21998
- * types for type safety.
21889
+ * Validates:
21890
+ * - allowanceStrategy: Either 'permit' or 'approve'
21891
+ * - slippageBps: Optional positive number for slippage tolerance
21892
+ * - stopLimit: Optional decimal string for minimum output
21893
+ * - customFee: Optional fee configuration
21894
+ * - kitKey: Optional string identifier
21895
+ */
21896
+ const swapConfigSchema = z.object({
21897
+ allowanceStrategy: allowanceStrategySchema.optional(),
21898
+ slippageBps: z.number().int().min(0).optional(),
21899
+ stopLimit: z
21900
+ .string()
21901
+ .min(1, 'Required')
21902
+ .pipe(createDecimalStringValidator({
21903
+ allowZero: true,
21904
+ 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.',
21905
+ attributeName: 'stopLimit',
21906
+ })(z.string()))
21907
+ .optional(),
21908
+ customFee: swapCustomFeeSchema.optional(),
21909
+ kitKey: z.string().optional(),
21910
+ });
21911
+ const EVM_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
21912
+ const SOLANA_ADDRESS_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
21913
+ const BASE58_CHARS_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
21914
+ /**
21915
+ * Broad heuristic: return true when the input is composed entirely of
21916
+ * base58 characters and is at least 32 characters long (the minimum
21917
+ * length of a valid Solana address).
21999
21918
  *
22000
- * The function is pure and side-effect-free, returning a plain context object
22001
- * that can be used with swap operations. Default providers are currently
22002
- * stubbed and will be implemented in future phases.
21919
+ * The caller is expected to have already excluded valid Solana
21920
+ * addresses (via {@link SOLANA_ADDRESS_REGEX}) before invoking this
21921
+ * helper, so in practice it only matches over-length base58 strings
21922
+ * (45+ chars) that are plausibly a malformed Solana address.
21923
+ */
21924
+ function looksLikeSolanaAddress(value) {
21925
+ return BASE58_CHARS_REGEX.test(value) && value.length >= 32;
21926
+ }
21927
+ const MAX_DISPLAY_LENGTH = 30;
21928
+ /** Truncate a value for safe inclusion in error messages. */
21929
+ function truncate(value) {
21930
+ return value.length > MAX_DISPLAY_LENGTH
21931
+ ? `${value.slice(0, MAX_DISPLAY_LENGTH)}...`
21932
+ : value;
21933
+ }
21934
+ /**
21935
+ * Schema for validating swap token input.
22003
21936
  *
22004
- * @typeParam TExtraProviders - Array type of additional swap providers
22005
- * @param config - Optional configuration for the SwapKit context
22006
- * @returns A fully initialized SwapKitContext ready for swap operations
22007
- * @throws \{ValidationError\} If the custom fee policy is invalid
21937
+ * Accepts either:
21938
+ * - Token symbols from the supported swap token registry ('USDC', 'WETH', 'NATIVE', etc.)
21939
+ * - Token addresses (EVM: 0x..., Solana: base58)
21940
+ *
21941
+ * This allows swapping both supported tokens (by symbol or address) and
21942
+ * arbitrary tokens (by address only), as long as at least one token is
21943
+ * an "OK token" for fee collection purposes.
21944
+ *
21945
+ * @remarks
21946
+ * Uses input-shape heuristics to produce contextual error messages:
21947
+ * - Starts with `0x` — validated as EVM address
21948
+ * - Recognized symbol — accepted
21949
+ * - 32–44 base58 characters — accepted as Solana address
21950
+ * - Otherwise — reports the most likely error (unsupported symbol,
21951
+ * malformed Solana address, or malformed EVM address)
21952
+ *
21953
+ * Runtime validation in `isOkToken` determines if the token is supported
21954
+ * for fee collection. This schema only validates the format.
21955
+ */
21956
+ let _symbolResult;
21957
+ const swapTokenSchema = z
21958
+ .string()
21959
+ .superRefine((value, ctx) => {
21960
+ if (value.startsWith('0x')) {
21961
+ if (!EVM_ADDRESS_REGEX.test(value)) {
21962
+ ctx.addIssue({
21963
+ code: z.ZodIssueCode.custom,
21964
+ message: `Invalid EVM token address format. Expected '0x' followed by 40 hexadecimal characters, but received: '${truncate(value)}'`,
21965
+ });
21966
+ }
21967
+ return;
21968
+ }
21969
+ _symbolResult = supportedSwapTokenSchema.safeParse(value);
21970
+ if (_symbolResult.success) {
21971
+ return;
21972
+ }
21973
+ if (SOLANA_ADDRESS_REGEX.test(value)) {
21974
+ return;
21975
+ }
21976
+ if (looksLikeSolanaAddress(value)) {
21977
+ ctx.addIssue({
21978
+ code: z.ZodIssueCode.custom,
21979
+ message: `Invalid Solana token address format. Expected 32-44 base58 characters, but received: '${truncate(value)}'`,
21980
+ });
21981
+ return;
21982
+ }
21983
+ ctx.addIssue({
21984
+ code: z.ZodIssueCode.custom,
21985
+ 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).`,
21986
+ });
21987
+ })
21988
+ .transform((value) => (_symbolResult?.success ? _symbolResult.data : value));
21989
+ // Amount-in validation schema - broken out to reduce type complexity
21990
+ const amountInSchema = z
21991
+ .string()
21992
+ .min(1, 'Required')
21993
+ .pipe(createDecimalStringValidator({
21994
+ allowZero: false,
21995
+ regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
21996
+ attributeName: 'amountIn',
21997
+ maxDecimals: 18,
21998
+ })(z.string()))
21999
+ .describe('The amount of the input token to swap, expressed as a human-readable ' +
22000
+ "decimal string in token units (e.g., '0.05' for 0.05 USDC or 0.05 ETH).");
22001
+ /**
22002
+ * Schema for validating swap parameters with chain identifiers.
22003
+ *
22004
+ * This schema validates the complete swap operation input, ensuring:
22005
+ * - Valid adapter context (adapter + chain + optional address)
22006
+ * - Valid tokenIn and tokenOut (symbols or addresses)
22007
+ * - Valid amountIn as a positive decimal string
22008
+ * - Optional valid configuration
22009
+ *
22010
+ * The schema validates amounts with up to 18 decimal places to support
22011
+ * various token standards.
22008
22012
  *
22009
22013
  * @example
22010
22014
  * ```typescript
22011
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22015
+ * import { swapParamsSchema } from '@circle-fin/swap-kit'
22016
+ *
22017
+ * // Using token symbols (recommended for supported tokens)
22018
+ * const paramsWithSymbols = {
22019
+ * from: {
22020
+ * adapter: sourceAdapter,
22021
+ * chain: 'Ethereum'
22022
+ * },
22023
+ * tokenIn: 'USDC',
22024
+ * tokenOut: 'USDT',
22025
+ * amountIn: '100.50', // 100.50 USDC
22026
+ * config: {
22027
+ * slippageBps: 300,
22028
+ * allowanceStrategy: 'permit'
22029
+ * }
22030
+ * }
22031
+ *
22032
+ * // Using token addresses (works for any token)
22033
+ * const paramsWithAddresses = {
22034
+ * from: {
22035
+ * adapter: sourceAdapter,
22036
+ * chain: 'Base'
22037
+ * },
22038
+ * tokenIn: '0x4200000000000000000000000000000000000006', // WETH address
22039
+ * tokenOut: '0x532f27101965dd16442E59d40670FaF5eBB142E4', // Any token address
22040
+ * amountIn: '0.1' // 0.1 WETH
22041
+ * }
22042
+ *
22043
+ * // Using native token alias
22044
+ * const paramsWithNative = {
22045
+ * from: {
22046
+ * adapter: sourceAdapter,
22047
+ * chain: 'Ethereum'
22048
+ * },
22049
+ * tokenIn: 'NATIVE', // ETH on Ethereum, MATIC on Polygon, etc.
22050
+ * tokenOut: 'USDC',
22051
+ * amountIn: '1.5' // 1.5 ETH
22052
+ * }
22053
+ *
22054
+ * const result = swapParamsSchema.safeParse(paramsWithSymbols)
22055
+ * if (result.success) {
22056
+ * console.log('Parameters are valid')
22057
+ * } else {
22058
+ * console.error('Validation failed:', result.error)
22059
+ * }
22060
+ * ```
22061
+ */
22062
+ const swapParamsSchema = z.object({
22063
+ from: adapterContextSchema$1,
22064
+ tokenIn: swapTokenSchema,
22065
+ tokenOut: swapTokenSchema,
22066
+ amountIn: amountInSchema,
22067
+ config: swapConfigSchema.optional(),
22068
+ });
22069
+ /**
22070
+ * Schema for validating SwapKit custom fee policy.
22071
+ *
22072
+ * Validates the shape of CustomFeePolicy, which lets SDK consumers
22073
+ * provide custom fee calculation and fee-recipient resolution logic.
22074
+ *
22075
+ * - computeFee: required function that returns a fee as a string (or Promise<string>).
22076
+ * - resolveFeeRecipientAddress: required function that returns a recipient address as a
22077
+ * string (or Promise<string>).
22078
+ *
22079
+ * This schema only ensures the presence and return types of the functions; it
22080
+ * does not validate their argument types.
22081
+ *
22082
+ * @example
22083
+ * ```typescript
22084
+ * import { customFeePolicySchema } from '@circle-fin/swap-kit'
22085
+ *
22086
+ * const config = {
22087
+ * computeFee: async () => '0.1',
22088
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
22089
+ * }
22090
+ *
22091
+ * const result = customFeePolicySchema.safeParse(config)
22092
+ * // result.success === true
22093
+ * ```
22094
+ */
22095
+ const customFeePolicySchema = z
22096
+ .object({
22097
+ computeFee: z.function().returns(z.string().or(z.promise(z.string()))),
22098
+ resolveFeeRecipientAddress: z
22099
+ .function()
22100
+ .returns(z.string().or(z.promise(z.string()))),
22101
+ })
22102
+ .strict();
22103
+
22104
+ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
22105
+ /**
22106
+ * Assert that the provided value conforms to {@link CustomFeePolicy}.
22107
+ *
22108
+ * This function validates the custom fee policy configuration using the
22109
+ * customFeePolicySchema. It ensures that both required functions
22110
+ * (computeFee and resolveFeeRecipientAddress) are present and have
22111
+ * the correct return types.
22112
+ *
22113
+ * Throws a structured error with detailed validation messages if the
22114
+ * configuration is malformed. Uses state tracking to avoid duplicate
22115
+ * validations.
22116
+ *
22117
+ * @param config - The custom fee policy to validate
22118
+ * @throws \{KitError\} If the policy fails validation
22119
+ *
22120
+ * @example
22121
+ * ```typescript
22122
+ * import { assertCustomFeePolicy } from '@circle-fin/swap-kit'
22123
+ *
22124
+ * const config = {
22125
+ * computeFee: () => '0.1',
22126
+ * resolveFeeRecipientAddress: () => '0x1234567890123456789012345678901234567890',
22127
+ * }
22128
+ *
22129
+ * try {
22130
+ * assertCustomFeePolicy(config)
22131
+ * // If no error is thrown, `config` is a valid CustomFeePolicy
22132
+ * } catch (error) {
22133
+ * console.error('Invalid fee policy:', error.message)
22134
+ * }
22135
+ * ```
22136
+ */
22137
+ function assertCustomFeePolicy(config) {
22138
+ // Use validateWithStateTracking to avoid duplicate validations
22139
+ // This will skip validation if already validated by this function
22140
+ // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
22141
+ validateWithStateTracking(config, customFeePolicySchema, 'SwapKit custom fee policy', assertCustomFeePolicySymbol);
22142
+ }
22143
+
22144
+ /**
22145
+ * Symbol used to track that assertSwapParams has validated an object.
22146
+ * @internal
22147
+ */
22148
+ const ASSERT_SWAP_PARAMS_SYMBOL = Symbol('assertSwapParams');
22149
+ /**
22150
+ * Assert that the provided value conforms to the SwapParams schema.
22012
22151
  *
22013
- * // Create context with defaults
22014
- * const context = createSwapKitContext()
22015
- * ```
22152
+ * This function validates swap parameters using the provided Zod schema
22153
+ * and tracks validation state to avoid duplicate checks. It performs
22154
+ * comprehensive validation including:
22155
+ * - Adapter context structure
22156
+ * - Token specifications
22157
+ * - Amount format and range
22158
+ * - Optional configuration values
22016
22159
  *
22017
- * @example
22018
- * ```typescript
22019
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22160
+ * Throws a structured error with detailed validation messages if
22161
+ * any parameter is invalid.
22020
22162
  *
22021
- * // Create context with custom fee policy
22022
- * const context = createSwapKitContext({
22023
- * customFeePolicy: {
22024
- * computeFee: async (params) => {
22025
- * // Calculate based on swap amount
22026
- * return '0.1' // 0.1 USDC
22027
- * },
22028
- * resolveFeeRecipientAddress: async (chain, params) => {
22029
- * // Return recipient based on chain
22030
- * if (chain.type === 'solana') {
22031
- * return 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
22032
- * }
22033
- * return '0x1234567890123456789012345678901234567890'
22034
- * }
22035
- * }
22036
- * })
22037
- * ```
22163
+ * @typeParam T - The expected type after validation
22164
+ * @param params - The swap parameters to validate
22165
+ * @param schema - The Zod schema to validate against
22166
+ * @throws \{KitError\} If the parameters fail validation
22038
22167
  *
22039
22168
  * @example
22040
22169
  * ```typescript
22041
- * import { createSwapKitContext } from '@circle-fin/swap-kit'
22170
+ * import { assertSwapParams, swapParamsSchema } from '@circle-fin/swap-kit'
22042
22171
  *
22043
- * // Create context with custom providers (future use)
22044
- * const context = createSwapKitContext({
22045
- * providers: [] // Custom swap providers will be supported in future phases
22046
- * })
22172
+ * const params = {
22173
+ * from: { adapter: sourceAdapter, chain: 'Ethereum' },
22174
+ * tokenIn: 'USDC',
22175
+ * tokenOut: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
22176
+ * amountIn: '100.50'
22177
+ * }
22178
+ *
22179
+ * try {
22180
+ * assertSwapParams(params, swapParamsSchema)
22181
+ * // Parameters are valid, proceed with swap
22182
+ * } catch (error) {
22183
+ * console.error('Invalid parameters:', error.message)
22184
+ * }
22047
22185
  * ```
22048
22186
  */
22049
- function createSwapKitContext(config = {}) {
22050
- // Validate custom fee policy if provided
22051
- if (config.customFeePolicy !== undefined) {
22052
- assertCustomFeePolicy(config.customFeePolicy);
22053
- }
22054
- // Initialize default providers
22055
- const defaultProviders = getDefaultProviders();
22056
- // Merge default and custom providers
22057
- const providers = [...defaultProviders, ...(config.providers ?? [])];
22058
- // Return the initialized context
22059
- return {
22060
- providers,
22061
- customFeePolicy: config.customFeePolicy ?? undefined,
22062
- tokens: createTokenRegistry(),
22063
- };
22187
+ function assertSwapParams(params, schema) {
22188
+ // Use validateWithStateTracking to avoid duplicate validations
22189
+ // This will skip validation if already validated by this function
22190
+ // validateWithStateTracking now throws KitError directly with INPUT_VALIDATION_FAILED code
22191
+ validateWithStateTracking(params, schema, 'swap parameters', ASSERT_SWAP_PARAMS_SYMBOL);
22064
22192
  }
22065
22193
 
22066
22194
  /**
@@ -23384,6 +23512,60 @@ class Amount {
23384
23512
  }
23385
23513
  }
23386
23514
 
23515
+ /**
23516
+ * Return the stablecoin symbol that should replace NATIVE for chains whose
23517
+ * native gas token is itself a supported stablecoin.
23518
+ *
23519
+ * @param chain - The chain definition to inspect.
23520
+ * @returns `'USDC'` when the chain's native currency symbol is USDC and a
23521
+ * USDC contract address exists, `null` otherwise.
23522
+ *
23523
+ * @example
23524
+ * ```typescript
23525
+ * import { ArcTestnet, Ethereum } from '@core/chains'
23526
+ *
23527
+ * getNativeStablecoinSymbol(ArcTestnet) // 'USDC'
23528
+ * getNativeStablecoinSymbol(Ethereum) // null
23529
+ * ```
23530
+ */
23531
+ function getNativeStablecoinSymbol(chain) {
23532
+ if (chain.nativeCurrency.symbol.toUpperCase() === 'USDC' &&
23533
+ chain.usdcAddress) {
23534
+ return 'USDC';
23535
+ }
23536
+ return null;
23537
+ }
23538
+ /**
23539
+ * Canonicalize the NATIVE alias to the matching stablecoin symbol on chains
23540
+ * whose native gas token is itself a supported stablecoin.
23541
+ *
23542
+ * The comparison is case-insensitive for the NATIVE token identifier.
23543
+ * Non-NATIVE inputs and native placeholder addresses (e.g. `0xEeee...`)
23544
+ * pass through unchanged.
23545
+ *
23546
+ * @param token - The token symbol or alias to canonicalize.
23547
+ * @param chain - The chain definition used for context-specific resolution.
23548
+ * @returns The canonicalized token symbol. Returns `'USDC'` when `token` is
23549
+ * `'NATIVE'` on a native-stablecoin chain; otherwise returns `token`
23550
+ * unchanged.
23551
+ *
23552
+ * @example
23553
+ * ```typescript
23554
+ * import { ArcTestnet, Ethereum } from '@core/chains'
23555
+ *
23556
+ * canonicalizeNativeStablecoinAlias('NATIVE', ArcTestnet) // 'USDC'
23557
+ * canonicalizeNativeStablecoinAlias('native', ArcTestnet) // 'USDC'
23558
+ * canonicalizeNativeStablecoinAlias('NATIVE', Ethereum) // 'NATIVE'
23559
+ * canonicalizeNativeStablecoinAlias('USDC', ArcTestnet) // 'USDC'
23560
+ * ```
23561
+ */
23562
+ function canonicalizeNativeStablecoinAlias(token, chain) {
23563
+ if (token.toUpperCase() !== NATIVE_TOKEN) {
23564
+ return token;
23565
+ }
23566
+ return getNativeStablecoinSymbol(chain) ?? token;
23567
+ }
23568
+
23387
23569
  /**
23388
23570
  * Assert the resolved chain definition is supported by swap operations.
23389
23571
  *
@@ -23407,6 +23589,74 @@ function assertIsSwapSupportedChainDefinition(chain) {
23407
23589
  throw createValidationFailedError$1('chain', chain.chain, `Unsupported swap chain. Supported chains: ${supported}`);
23408
23590
  }
23409
23591
  }
23592
+ /**
23593
+ * Resolve the on-chain address of the stablecoin that matches the
23594
+ * native currency symbol, or `null` if there is no match.
23595
+ */
23596
+ function getNativeStablecoinAddress(chain) {
23597
+ if (getNativeStablecoinSymbol(chain) === 'USDC') {
23598
+ return chain.usdcAddress;
23599
+ }
23600
+ return null;
23601
+ }
23602
+ /**
23603
+ * Check whether a token input (symbol or address) refers to the
23604
+ * native currency's underlying stablecoin on this chain.
23605
+ */
23606
+ function isNativeEquivalent(token, nativeSymbol, nativeStablecoinAddress) {
23607
+ const upper = token.toUpperCase();
23608
+ if (upper === nativeSymbol)
23609
+ return true;
23610
+ if (upper === nativeStablecoinAddress.toUpperCase())
23611
+ return true;
23612
+ return false;
23613
+ }
23614
+ /**
23615
+ * Check whether a token string represents the native gas token,
23616
+ * either as the `"NATIVE"` alias or a well-known placeholder address.
23617
+ */
23618
+ function isNativeTokenInput(token) {
23619
+ const lower = token.toLowerCase();
23620
+ if (lower === NATIVE_TOKEN.toLowerCase())
23621
+ return true;
23622
+ return OK_NATIVE_TOKEN_ADDRESSES_EVM.some((addr) => addr.toLowerCase() === lower);
23623
+ }
23624
+ /**
23625
+ * Reject swaps between NATIVE and a token that IS the native currency.
23626
+ *
23627
+ * On chains like Arc where the native gas token is USDC, swapping
23628
+ * USDC ↔ NATIVE is a no-op because they represent the same asset.
23629
+ * This guard handles symbol inputs (`'USDC'`, `'NATIVE'`), raw
23630
+ * contract addresses (`'0x3600...'`), and native placeholder
23631
+ * addresses (`'0xEeee...'`, `'0x0000...'`).
23632
+ *
23633
+ * @param tokenIn - The input token alias or address
23634
+ * @param tokenOut - The output token alias or address
23635
+ * @param chain - The resolved chain definition
23636
+ * @returns Nothing.
23637
+ * @throws \{KitError\} When one side is NATIVE (alias or placeholder)
23638
+ * and the other matches the chain's native currency symbol or
23639
+ * contract address
23640
+ */
23641
+ function assertNativeNotEquivalent(tokenIn, tokenOut, chain) {
23642
+ const nativeStablecoinAddress = getNativeStablecoinAddress(chain);
23643
+ if (nativeStablecoinAddress === null)
23644
+ return;
23645
+ const nativeSymbol = chain.nativeCurrency.symbol.toUpperCase();
23646
+ const inIsNative = isNativeTokenInput(tokenIn);
23647
+ const outIsNative = isNativeTokenInput(tokenOut);
23648
+ const shouldReject = (inIsNative &&
23649
+ isNativeEquivalent(tokenOut, nativeSymbol, nativeStablecoinAddress)) ||
23650
+ (outIsNative &&
23651
+ isNativeEquivalent(tokenIn, nativeSymbol, nativeStablecoinAddress)) ||
23652
+ (inIsNative && outIsNative);
23653
+ if (shouldReject) {
23654
+ throw createValidationFailedError$1(inIsNative ? 'tokenIn' : 'tokenOut', inIsNative ? tokenIn : tokenOut, `On ${chain.name}, the native gas token is ${nativeSymbol}. ` +
23655
+ `Swapping between NATIVE and ${nativeSymbol} is not supported ` +
23656
+ `because they represent the same asset. ` +
23657
+ `Use ${nativeSymbol} directly instead of NATIVE.`);
23658
+ }
23659
+ }
23410
23660
  /**
23411
23661
  * Resolves the amount of a swap by formatting it according to the token's decimal places.
23412
23662
  *
@@ -23517,12 +23767,14 @@ async function resolveSwapConfig(params, chain, tokens, adapter) {
23517
23767
  * Resolves swap parameters from user input to provider-consumable format.
23518
23768
  *
23519
23769
  * Transforms SwapParams with chain identifiers and token aliases into
23520
- * ResolvedSwapParams with full chain definitions, preserved token aliases,
23770
+ * ResolvedSwapParams with full chain definitions, canonicalized token aliases,
23521
23771
  * and validated wallet addresses. Uses the TokenRegistry from context for
23522
23772
  * decimal resolution.
23523
23773
  *
23524
- * Note: Token aliases ('USDC', 'USDT', 'NATIVE') are preserved and passed
23525
- * through unchanged. Address resolution is handled by the provider layer.
23774
+ * Note: Raw user input is validated first, then `NATIVE` may be canonicalized
23775
+ * to a stablecoin symbol on chains whose native gas token is itself a
23776
+ * supported stablecoin (for example, Arc Testnet `NATIVE` → `USDC`).
23777
+ * Address resolution is handled by the provider layer.
23526
23778
  *
23527
23779
  * @typeParam TFromAdapterCapabilities - The adapter capabilities type for the source adapter
23528
23780
  * @param params - The input swap parameters to resolve
@@ -23550,8 +23802,8 @@ async function resolveSwapConfig(params, chain, tokens, adapter) {
23550
23802
  * amountIn: '100.5'
23551
23803
  * }, tokens)
23552
23804
  *
23553
- * // resolved.tokenIn === 'USDC' // Alias preserved
23554
- * // resolved.tokenOut === 'USDT' // Alias preserved
23805
+ * // resolved.tokenIn === 'USDC' // Canonicalized for provider routing
23806
+ * // resolved.tokenOut === 'USDT' // Canonicalized for provider routing
23555
23807
  * // resolved.to === wallet address from adapter
23556
23808
  * ```
23557
23809
  */
@@ -23562,16 +23814,20 @@ async function resolveSwapParams(params, tokens) {
23562
23814
  const resolvedChain = resolveChainIdentifier(params.from.chain);
23563
23815
  assertIsSwapSupportedChainDefinition(resolvedChain);
23564
23816
  const fromChain = resolvedChain;
23817
+ assertNativeNotEquivalent(params.tokenIn, params.tokenOut, fromChain);
23565
23818
  params.from.adapter.validateChainSupport(fromChain);
23566
23819
  // Cast required: SwapAdapterContext and AdapterContext differ in their
23567
23820
  // optional address field handling under exactOptionalPropertyTypes.
23568
23821
  const walletAddress = await resolveAddress(params.from);
23569
- // Pass token aliases directly to the resolved params
23570
- // Resolution to addresses will be handled by the provider
23571
- const tokenIn = params.tokenIn;
23572
- const tokenOut = params.tokenOut;
23573
- const resolvedAmount = await resolveAmount(params.amountIn, params.tokenIn, fromChain, tokens, params.from.adapter);
23574
- const resolvedConfig = await resolveSwapConfig(params, fromChain, tokens, params.from.adapter);
23822
+ // Canonicalize NATIVE after raw-input validation so native-stablecoin
23823
+ // chains use stablecoin decimals and downstream provider routing.
23824
+ const tokenIn = canonicalizeNativeStablecoinAlias(params.tokenIn, fromChain);
23825
+ const tokenOut = canonicalizeNativeStablecoinAlias(params.tokenOut, fromChain);
23826
+ const resolvedAmount = await resolveAmount(params.amountIn, tokenIn, fromChain, tokens, params.from.adapter);
23827
+ const resolvedConfig = await resolveSwapConfig({
23828
+ ...params,
23829
+ tokenOut,
23830
+ }, fromChain, tokens, params.from.adapter);
23575
23831
  return {
23576
23832
  from: {
23577
23833
  ...params.from,
@@ -23836,6 +24092,10 @@ const formatConfig = (config, outputTokenTransform) => {
23836
24092
  if (Object.keys(cloneConfig).length === 0) {
23837
24093
  return undefined;
23838
24094
  }
24095
+ // Transform stopLimit if present (minimum output amount, uses output token decimals)
24096
+ if (cloneConfig.stopLimit !== undefined) {
24097
+ cloneConfig.stopLimit = outputTokenTransform(cloneConfig.stopLimit);
24098
+ }
23839
24099
  // Handle customFee transformation
23840
24100
  if (config.customFee !== undefined) {
23841
24101
  const { amount, percentageBps, recipientAddress } = config.customFee;
@@ -23950,7 +24210,9 @@ function getNativeTokenAddress(chain) {
23950
24210
  * The quote endpoint requires resolved addresses, not aliases. This function
23951
24211
  * mirrors the provider's `resolveTokenAlias` logic using the context's
23952
24212
  * TokenRegistry:
23953
- * - "NATIVE" alias → chain-specific native token placeholder address
24213
+ * - "NATIVE" alias → native token placeholder, unless the chain's native gas
24214
+ * token is a supported stablecoin, in which case it canonicalizes to the
24215
+ * stablecoin symbol and resolves through the registry
23954
24216
  * - Native currency symbol (ETH, SOL, MON, etc.) → native token placeholder
23955
24217
  * - Known symbols (USDC, USDT, etc.) → resolved chain-specific address
23956
24218
  * - Already-an-address strings → passed through unchanged
@@ -23963,9 +24225,13 @@ function getNativeTokenAddress(chain) {
23963
24225
  */
23964
24226
  function resolveTokenForQuote(token, chain, tokens) {
23965
24227
  const upperToken = token.toUpperCase();
23966
- // Handle NATIVE alias and native currency symbols (ETH, SOL, MON, etc.)
24228
+ const nativeStablecoinSymbol = getNativeStablecoinSymbol(chain);
24229
+ // Handle native aliases and native currency symbols for standard native-token
24230
+ // chains. On native-stablecoin chains, canonicalized symbols should resolve
24231
+ // via the token registry instead of the native placeholder.
23967
24232
  if (upperToken === 'NATIVE' ||
23968
- upperToken === chain.nativeCurrency.symbol.toUpperCase()) {
24233
+ (nativeStablecoinSymbol === null &&
24234
+ upperToken === chain.nativeCurrency.symbol.toUpperCase())) {
23969
24235
  return getNativeTokenAddress(chain);
23970
24236
  }
23971
24237
  // If the token is a known symbol in the registry, resolve to address
@@ -24027,8 +24293,15 @@ async function handleOutputFeeCallback(context, params) {
24027
24293
  // Resolve token aliases to addresses for the quote API
24028
24294
  // The quote endpoint requires resolved addresses, not aliases like 'USDC'
24029
24295
  const chain = params.from.chain;
24030
- const resolvedTokenIn = resolveTokenForQuote(params.tokenIn, chain, context.tokens);
24031
- const resolvedTokenOut = resolveTokenForQuote(params.tokenOut, chain, context.tokens);
24296
+ const tokenIn = canonicalizeNativeStablecoinAlias(params.tokenIn, chain);
24297
+ const tokenOut = canonicalizeNativeStablecoinAlias(params.tokenOut, chain);
24298
+ const canonicalizedParams = {
24299
+ ...params,
24300
+ tokenIn,
24301
+ tokenOut,
24302
+ };
24303
+ const resolvedTokenIn = resolveTokenForQuote(canonicalizedParams.tokenIn, chain, context.tokens);
24304
+ const resolvedTokenOut = resolveTokenForQuote(canonicalizedParams.tokenOut, chain, context.tokens);
24032
24305
  const quoteParams = {
24033
24306
  tokenInAddress: resolvedTokenIn,
24034
24307
  tokenInChain: chain.chain, // Use enum value (e.g., "World_Chain")
@@ -24046,7 +24319,7 @@ async function handleOutputFeeCallback(context, params) {
24046
24319
  const quoteResponse = await getQuote(quoteParams);
24047
24320
  // Step 3: Build fee context from quote
24048
24321
  const feeContext = {
24049
- ...params,
24322
+ ...canonicalizedParams,
24050
24323
  type: 'output',
24051
24324
  minAmount: quoteResponse.quote.minAmount,
24052
24325
  estimatedAmount: quoteResponse.quote.estimatedAmount,
@@ -24154,8 +24427,8 @@ async function estimate(context, params) {
24154
24427
  // Return the estimate with input context fields populated
24155
24428
  return {
24156
24429
  // Input context fields
24157
- tokenIn: params.tokenIn,
24158
- tokenOut: params.tokenOut,
24430
+ tokenIn: resolvedParams.tokenIn,
24431
+ tokenOut: resolvedParams.tokenOut,
24159
24432
  amountIn: params.amountIn,
24160
24433
  chain: chainName,
24161
24434
  fromAddress: resolvedParams.from.address,
@@ -24279,11 +24552,11 @@ async function swap$1(context, params) {
24279
24552
  };
24280
24553
  const providerResult = await provider.swap(swapParams);
24281
24554
  const [tokenInDecimals, tokenOutDecimals] = await Promise.all([
24282
- getTokenDecimals(params.tokenIn, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24283
- getTokenDecimals(params.tokenOut, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24555
+ getTokenDecimals(resolvedParams.tokenIn, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24556
+ getTokenDecimals(resolvedParams.tokenOut, resolvedParams.from.chain, resolvedParams.from.adapter, context.tokens),
24284
24557
  ]);
24285
24558
  // Step 6: Format provider result to human-readable values
24286
- return formatSwapResult(providerResult, 'to-human-readable', params.tokenIn, params.tokenOut, tokenInDecimals, tokenOutDecimals);
24559
+ return formatSwapResult(providerResult, 'to-human-readable', resolvedParams.tokenIn, resolvedParams.tokenOut, tokenInDecimals, tokenOutDecimals);
24287
24560
  }
24288
24561
 
24289
24562
  /**
@@ -24349,9 +24622,10 @@ function getSupportedChains$1(context) {
24349
24622
  * This function follows an immutable pattern, returning a new context with the
24350
24623
  * wrapped policy attached. The original context remains unchanged.
24351
24624
  *
24625
+ * @typeParam TProviders - The tuple of swap providers in the context, preserved through the returned context
24352
24626
  * @param context - The SwapKitContext to configure with the custom fee policy
24353
24627
  * @param customFeePolicy - The custom fee policy containing fee calculation and recipient resolution logic
24354
- * @returns A new SwapKitContext with the custom fee policy configured
24628
+ * @returns A new SwapKitContext\<TProviders\> with the custom fee policy configured, preserving the original provider types
24355
24629
  * @throws \{ValidationError\} If the custom fee policy is invalid or missing required functions
24356
24630
  * @throws \{KitError\} If the token is not supported (not USDC, USDT, or NATIVE)
24357
24631
  *
@@ -24497,6 +24771,90 @@ function removeCustomFeePolicy(context) {
24497
24771
  };
24498
24772
  }
24499
24773
 
24774
+ /**
24775
+ * The default providers that will be used in addition to the providers provided
24776
+ * to the createSwapKitContext factory function.
24777
+ *
24778
+ * @returns An array containing the default StablecoinServiceSwapProvider
24779
+ * @internal
24780
+ */
24781
+ const getDefaultProviders = () => [new StablecoinServiceSwapProvider()];
24782
+ /**
24783
+ * Create a SwapKit context with validated configuration.
24784
+ *
24785
+ * This factory function initializes a SwapKitContext with default providers
24786
+ * and optional custom configuration. It validates any provided custom fee
24787
+ * policy and merges default and custom providers, preserving their exact
24788
+ * types for type safety.
24789
+ *
24790
+ * The function is pure and side-effect-free, returning a plain context object
24791
+ * that can be used with swap operations. Default providers are currently
24792
+ * stubbed and will be implemented in future phases.
24793
+ *
24794
+ * @typeParam TExtraProviders - Array type of additional swap providers
24795
+ * @param config - Optional configuration for the SwapKit context
24796
+ * @returns A fully initialized SwapKitContext ready for swap operations
24797
+ * @throws \{ValidationError\} If the custom fee policy is invalid
24798
+ *
24799
+ * @example
24800
+ * ```typescript
24801
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24802
+ *
24803
+ * // Create context with defaults
24804
+ * const context = createSwapKitContext()
24805
+ * ```
24806
+ *
24807
+ * @example
24808
+ * ```typescript
24809
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24810
+ *
24811
+ * // Create context with custom fee policy
24812
+ * const context = createSwapKitContext({
24813
+ * customFeePolicy: {
24814
+ * computeFee: async (params) => {
24815
+ * // Calculate based on swap amount
24816
+ * return '0.1' // 0.1 USDC
24817
+ * },
24818
+ * resolveFeeRecipientAddress: async (chain, params) => {
24819
+ * // Return recipient based on chain
24820
+ * if (chain.type === 'solana') {
24821
+ * return 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
24822
+ * }
24823
+ * return '0x1234567890123456789012345678901234567890'
24824
+ * }
24825
+ * }
24826
+ * })
24827
+ * ```
24828
+ *
24829
+ * @example
24830
+ * ```typescript
24831
+ * import { createSwapKitContext } from '@circle-fin/swap-kit'
24832
+ *
24833
+ * // Create context with custom providers (future use)
24834
+ * const context = createSwapKitContext({
24835
+ * providers: [] // Custom swap providers will be supported in future phases
24836
+ * })
24837
+ * ```
24838
+ */
24839
+ function createSwapKitContext(config = {}) {
24840
+ // Initialize default providers
24841
+ const defaultProviders = getDefaultProviders();
24842
+ // Merge default and custom providers
24843
+ const providers = [...defaultProviders, ...(config.providers ?? [])];
24844
+ // Build base context without fee policy
24845
+ const context = {
24846
+ providers,
24847
+ customFeePolicy: undefined,
24848
+ tokens: createTokenRegistry(),
24849
+ };
24850
+ // If fee policy provided, apply wrapping via setCustomFeePolicy
24851
+ // This ensures computeFee converts between human-readable and base units
24852
+ if (config.customFeePolicy !== undefined) {
24853
+ return setCustomFeePolicy(context, config.customFeePolicy);
24854
+ }
24855
+ return context;
24856
+ }
24857
+
24500
24858
  /**
24501
24859
  * A high-level class-based interface for single-chain token swap operations.
24502
24860
  *
@@ -25635,6 +25993,56 @@ const bridge = async (context, params) => {
25635
25993
  return kit.bridge(params);
25636
25994
  };
25637
25995
 
25996
+ /**
25997
+ * Retry a failed cross-chain bridge operation using the AppKit context.
25998
+ *
25999
+ * This function provides a standardized interface for retrying bridge
26000
+ * operations within the AppKit ecosystem. It delegates to the underlying
26001
+ * BridgeKit retry infrastructure while maintaining consistency with the
26002
+ * AppKit patterns and context-based architecture.
26003
+ *
26004
+ * Use this after detecting a retryable error on a failed step via
26005
+ * {@link isRetryableError} to resume a bridge that failed due to a
26006
+ * transient issue.
26007
+ *
26008
+ * @param context - The AppKit context containing action handlers
26009
+ * @param result - The bridge result from the failed operation
26010
+ * @param retryContext - The retry context with source and optional
26011
+ * destination adapters
26012
+ * @returns Promise resolving to the bridge result with transaction
26013
+ * details
26014
+ * @throws If the provider from the original result is not found
26015
+ * @throws If the retry operation fails
26016
+ *
26017
+ * @example
26018
+ * ```typescript
26019
+ * import { AppKit, isRetryableError } from '@circle-fin/app-kit'
26020
+ *
26021
+ * const kit = new AppKit()
26022
+ *
26023
+ * const result = await kit.bridge({
26024
+ * from: sourceAdapter,
26025
+ * to: { adapter: destAdapter, chain: 'Polygon' },
26026
+ * amount: '100.50',
26027
+ * })
26028
+ *
26029
+ * const failedStep = result.steps.find(s => s.error)
26030
+ * if (result.state === 'error' && failedStep?.error && isRetryableError(failedStep.error)) {
26031
+ * const retried = await kit.retryBridge(result, {
26032
+ * from: sourceAdapter,
26033
+ * to: destAdapter,
26034
+ * })
26035
+ * console.log('Retry result:', retried.state)
26036
+ * }
26037
+ * ```
26038
+ */
26039
+ const retryBridge = async (context, result, retryContext) => {
26040
+ const kit = createBridgeKit(context);
26041
+ registerActionHandlers(kit, context.actions.bridge, 'bridge');
26042
+ // Delegate to the BridgeKit for actual retry execution
26043
+ return kit.retry(result, retryContext);
26044
+ };
26045
+
25638
26046
  /**
25639
26047
  * Execute a same-chain token swap operation using the AppKit context.
25640
26048
  *
@@ -25906,6 +26314,46 @@ class AppKit {
25906
26314
  async bridge(params) {
25907
26315
  return bridge(this.context, params);
25908
26316
  }
26317
+ /**
26318
+ * Retry a failed cross-chain USDC bridge transfer.
26319
+ *
26320
+ * Resume a bridge operation that failed due to a transient error.
26321
+ * Use {@link isRetryableError} to check whether a failed step's error
26322
+ * is eligible for retry before calling this method.
26323
+ *
26324
+ * @param result - The bridge result from the failed operation
26325
+ * @param retryContext - The retry context with source and optional
26326
+ * destination adapters
26327
+ * @returns Promise resolving to the bridge result with transaction
26328
+ * details
26329
+ * @throws If the provider from the original result is not found
26330
+ * @throws If the retry operation fails
26331
+ *
26332
+ * @example
26333
+ * ```typescript
26334
+ * import { AppKit, isRetryableError } from '@circle-fin/app-kit'
26335
+ *
26336
+ * const kit = new AppKit()
26337
+ *
26338
+ * const result = await kit.bridge({
26339
+ * from: sourceAdapter,
26340
+ * to: { adapter: destAdapter, chain: 'Polygon' },
26341
+ * amount: '100.50',
26342
+ * })
26343
+ *
26344
+ * const failedStep = result.steps.find(s => s.error)
26345
+ * if (result.state === 'error' && failedStep?.error && isRetryableError(failedStep.error)) {
26346
+ * const retried = await kit.retryBridge(result, {
26347
+ * from: sourceAdapter,
26348
+ * to: destAdapter,
26349
+ * })
26350
+ * console.log('Retry result:', retried.state)
26351
+ * }
26352
+ * ```
26353
+ */
26354
+ async retryBridge(result, retryContext) {
26355
+ return retryBridge(this.context, result, retryContext);
26356
+ }
25909
26357
  /**
25910
26358
  * Estimate the bridge operation.
25911
26359
  *