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