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