@circle-fin/app-kit 1.11.0 → 1.12.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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # @circle-fin/app-kit
2
2
 
3
+ ## 1.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add X Layer chain definitions (mainnet and testnet) with CCTP v2 support, USDC token addresses, Viem adapter mappings, Bridge Kit exports.
8
+ - Add App Kit-level operation-scoped custom fee policy configuration and scoped
9
+ custom fee policy removal. Align the README with the current public API,
10
+ including explicit operation methods, supported send and swap tokens, and the
11
+ complete core method list.
12
+
13
+ - Same-chain Earn deposits, withdrawals, and reward claims can now surface a
14
+ verified `earn.execute` review to an adapter `onBeforeAuthorize` hook before
15
+ authorization. Use `isEarnExecuteReview` to inspect the decoded Earn operation
16
+ and exact call data; returning `reject` prevents signing.
17
+ - Support spending a unified balance from a smart contract account via Gateway's
18
+ ERC-1271 programmable authorization.
19
+
20
+ Previously, `spend` rejected any smart contract account (SCA) signer up front
21
+ and told you to register an EOA delegate against the SCA, because Gateway
22
+ validated burn-intent signatures with `ecrecover` only. Gateway now also
23
+ validates contract signatures with ERC-1271, so the SDK detects a contract
24
+ signer from its on-chain bytecode and marks the transfer request
25
+ `contractSigner: true` — multisigs, passkey wallets, Circle SCA wallets, and
26
+ other ERC-1271 accounts can authorize transfers directly.
27
+
28
+ No API change: `spend` takes the same parameters and picks the validation path
29
+ for you. EOA transfers are unaffected, and EIP-7702-delegated EOAs keep using
30
+ the `ecrecover` path since their signatures verify that way.
31
+
32
+ ERC-1271 validation is EVM-only. Solana burn intents are unchanged.
33
+
34
+ ### Patch Changes
35
+
36
+ - Batch same-chain Earn deposits and withdrawals through an adapter's shared
37
+ `supportsAtomicBatch` and `batchExecute` capabilities. Approval and execution
38
+ calls are submitted atomically when supported, with a configuration option to
39
+ force the existing sequential flow. Legacy Viem batches now request atomic
40
+ EIP-5792 execution by default.
41
+
3
42
  ## 1.11.0
4
43
 
5
44
  ### Minor Changes
package/README.md CHANGED
@@ -35,7 +35,7 @@ _Making cross-chain transfers, same-chain swaps, unified balance management, and
35
35
  - [Bridge Parameters](#bridge-parameters)
36
36
  - [Custom Fees](#custom-fees)
37
37
  - [Operation-Level Custom Fees](#operation-level-custom-fees)
38
- - [Kit-Level Fee Policies](#kit-level-fee-policies)
38
+ - [Kit-Level Custom Fee Policies](#kit-level-custom-fee-policies)
39
39
  - [Error Handling](#error-handling)
40
40
  - [Examples](#examples)
41
41
  - [Basic Cross-Chain Transfer](#basic-cross-chain-transfer)
@@ -55,19 +55,19 @@ _Making cross-chain transfers, same-chain swaps, unified balance management, and
55
55
 
56
56
  The App Kit ecosystem is Circle's open-source effort to streamline stablecoin development with SDKs that are easy to use correctly and hard to misuse. Kits are cross-framework (viem, ethers, @solana/web3) and integrate cleanly into any stack. They're opinionated with sensible defaults, but offer escape hatches for full control. A pluggable architecture makes implementation flexible, and all kits are interoperable, so they can be composed to suit a wide range of use cases.
57
57
 
58
- The App Kit provides a **unified interface** for cross-chain transfers, same-chain swaps, earn vault operations, and unified balance management, abstracting away the complexity of choosing between operations. It combines the power of Bridge Kit, Swap Kit, Earn Kit, and Unified Balance Kit into a single, cohesive API.
58
+ The App Kit provides a **unified interface** for cross-chain transfers, same-chain swaps, token sends, earn vault operations, and unified balance management. It combines the power of Bridge Kit, Swap Kit, Earn Kit, and Unified Balance Kit into a single, cohesive API with explicit methods for each operation.
59
59
 
60
60
  ### Why App Kit?
61
61
 
62
- - **🎯 Unified interface**: Single API for bridge, swap, earn, and unified balance operations
63
- - **🤖 Smart routing**: Automatically selects the appropriate operation (bridge vs swap)
62
+ - **🎯 Unified interface**: Single SDK surface for bridge, swap, send, earn, and unified balance operations
63
+ - **🧭 Explicit operation methods**: Choose `bridge`, `swap`, `send`, `earn`, or `unifiedBalance` based on the flow you are building
64
64
  - **⚡ Zero-config defaults**: Built-in reliable RPC endpoints - start building right away
65
65
  - **🔧 Bring your own infrastructure**: Seamlessly integrate with your existing setup when needed
66
66
  - **🔒 Production-ready security**: Leverages Circle's CCTPv2 for bridging and trusted swap providers
67
67
  - **🚀 Developer experience**: Complete TypeScript support, comprehensive validation, and instant connectivity
68
- - **🌍 Multi-chain support**: Bridge across **47 chains** with **1058 total bridge routes** through Circle's CCTPv2
69
- - **Mainnet (23 chains)**: Arbitrum, Avalanche, Base, Codex, Cronos, Edge, Ethereum, HyperEVM, Injective, Ink, Linea, Monad, Morph, OP Mainnet, Pharos, Plume, Polygon PoS, Sei, Solana, Sonic, Unichain, World Chain, XDC
70
- - **Testnet (24 chains)**: Arc Testnet, Arbitrum Sepolia, Avalanche Fuji, Base Sepolia, Codex Testnet, Cronos Testnet, Edge Testnet, Ethereum Sepolia, HyperEVM Testnet, Injective Testnet, Ink Testnet, Linea Sepolia, Monad Testnet, Morph Testnet, OP Sepolia, Pharos Atlantic, Plume Testnet, Polygon PoS Amoy, Sei Testnet, Solana Devnet, Sonic Testnet, Unichain Sepolia, World Chain Sepolia, XDC Apothem
68
+ - **🌍 Multi-chain support**: Bridge across **49 chains** with **1152 total bridge routes** through Circle's CCTPv2
69
+ - **Mainnet (24 chains)**: Arbitrum, Avalanche, Base, Codex, Cronos, Edge, Ethereum, HyperEVM, Injective, Ink, Linea, Monad, Morph, OP Mainnet, Pharos, Plume, Polygon PoS, Sei, Solana, Sonic, Unichain, World Chain, XDC, X Layer
70
+ - **Testnet (25 chains)**: Arc Testnet, Arbitrum Sepolia, Avalanche Fuji, Base Sepolia, Codex Testnet, Cronos Testnet, Edge Testnet, Ethereum Sepolia, HyperEVM Testnet, Injective Testnet, Ink Testnet, Linea Sepolia, Monad Testnet, Morph Testnet, OP Sepolia, Pharos Atlantic, Plume Testnet, Polygon PoS Amoy, Sei Testnet, Solana Devnet, Sonic Testnet, Unichain Sepolia, World Chain Sepolia, XDC Apothem, X Layer Testnet
71
71
  - **🔄 Swap support**: Same-chain token swaps powered by Circle's Stablecoin Service
72
72
  - **🏦 Earn support**: DeFi lending vault deposits, withdrawals, and reward claims via `kit.earn`
73
73
  - **🎯 Flexible adapters**: Supporting EVM (Viem, Ethers) and Solana (@solana/web3)
@@ -163,7 +163,7 @@ const result = await kit.bridge({
163
163
  })
164
164
  ```
165
165
 
166
- All 41 CCTPv2-supported chains are available for import.
166
+ A curated subset of 37 CCTPv2-supported chains is available for import from App Kit.
167
167
 
168
168
  ## Quick Start
169
169
 
@@ -429,13 +429,15 @@ The `token` field accepts both known aliases and custom token contract addresses
429
429
  - `'USDC'` - USD Coin (6 decimals)
430
430
  - `'USDT'` - Tether USD (6 decimals)
431
431
  - `'NATIVE'` - Chain's native currency (ETH, SOL, etc.)
432
+ - `'EURC'` - Euro Coin, on chains with `eurcAddress` configured
432
433
 
433
434
  **Custom addresses:**
434
435
 
435
436
  - EVM contract addresses (e.g., `0x6B175474E89094C44Da98b954EedeAC495271d0F`)
436
437
  - Solana SPL mint addresses
437
438
 
438
- **Note**: For swap operations, additional stablecoins (EURC, DAI, USDE, PYUSD) are supported. See [Swap Parameters](#swap-parameters) section below.
439
+ **Note**: For swap operations, additional stablecoins and wrapped assets are
440
+ supported. See [Swap Parameters](#swap-parameters) section below.
439
441
 
440
442
  **Examples**
441
443
 
@@ -498,6 +500,7 @@ interface SwapParams {
498
500
  // SupportedToken includes:
499
501
  // - Stablecoins (6 decimals): 'USDC', 'EURC', 'USDT', 'PYUSD'
500
502
  // - Stablecoins (18 decimals): 'DAI', 'USDE'
503
+ // - Wrapped assets: 'WBTC', 'WETH', 'WSOL', 'WAVAX', 'WPOL', 'CIRBTC'
501
504
  // - Native: 'NATIVE' (chain's native currency)
502
505
  ```
503
506
 
@@ -590,38 +593,61 @@ await kit.swap({
590
593
  })
591
594
  ```
592
595
 
593
- ### Kit-Level Fee Policies
596
+ ### Kit-Level Custom Fee Policies
594
597
 
595
- For dynamic fee calculation across all operations, use kit-level policies:
598
+ For dynamic fee calculation, provide operation-scoped custom fee policies at the
599
+ App Kit level. Configure only the operations that need custom fees.
596
600
 
597
601
  ```typescript
598
602
  import { AppKit } from '@circle-fin/app-kit'
599
- import { formatUnits } from 'viem'
600
603
 
601
604
  const kit = new AppKit()
602
605
 
603
606
  kit.setCustomFeePolicy({
604
- calculateFee: (params) => {
605
- // Calculate fee based on operation type and parameters
606
- const amount = Number(formatUnits(BigInt(params.amount), 6))
607
- const feePercentage = type === 'bridge' ? 0.01 : 0.005 // 1% for bridge, 0.5% for swap
608
-
609
- return (amount * feePercentage).toFixed(6)
607
+ bridge: {
608
+ computeFee: (params) => {
609
+ const amount = Number(params.amount)
610
+ return (amount * 0.01).toFixed(6)
611
+ },
612
+ resolveFeeRecipientAddress: (chain) => {
613
+ return chain.type === 'solana'
614
+ ? 'SolanaAddressBase58...'
615
+ : '0xEvmAddress...'
616
+ },
610
617
  },
611
- resolveFeeRecipientAddress: (type, info) => {
612
- // Return appropriate address based on operation type and chain
613
- return info.chain.type === 'solana'
614
- ? 'SolanaAddressBase58...'
615
- : '0xEvmAddress...'
618
+ swap: {
619
+ computeFee: (params) => {
620
+ const amount = Number(params.amountIn)
621
+ return (amount * 0.005).toFixed(6)
622
+ },
623
+ resolveFeeRecipientAddress: (chain) => {
624
+ return chain.type === 'solana'
625
+ ? 'SolanaAddressBase58...'
626
+ : '0xEvmAddress...'
627
+ },
628
+ },
629
+ unifiedBalance: {
630
+ computeFee: (params) => {
631
+ const amount = Number(params.amount)
632
+ return (amount * 0.01).toFixed(6)
633
+ },
634
+ resolveFeeRecipientAddress: (destinationChain) => {
635
+ return destinationChain.type === 'solana'
636
+ ? 'SolanaAddressBase58...'
637
+ : '0xEvmAddress...'
638
+ },
616
639
  },
617
640
  })
618
641
 
619
- // All subsequent operations will use this policy
642
+ // All subsequent configured operations will use these policies
620
643
  await kit.bridge({
621
644
  from: { adapter, chain: 'Ethereum' },
622
645
  to: { adapter, chain: 'Base' },
623
646
  amount: '1000', // Custom fee calculated automatically
624
647
  })
648
+
649
+ // Remove one operation-scoped policy without affecting the others
650
+ kit.removeCustomFeePolicy('bridge')
625
651
  ```
626
652
 
627
653
  ## Error Handling
@@ -740,12 +766,17 @@ await kit.bridge({
740
766
 
741
767
  - `kit.send(params)` - Send tokens to a recipient on the same chain
742
768
  - `kit.bridge(params)` - Execute cross-chain bridge operation
769
+ - `kit.retryBridge(result, retryContext)` - Retry a failed bridge operation
743
770
  - `kit.swap(params)` - Execute same-chain swap operation
744
771
  - `kit.estimateBridge(params)` - Get cost estimates for bridging
745
772
  - `kit.estimateSwap(params)` - Get cost estimates for swapping
746
773
  - `kit.estimateSend(params)` - Get cost estimates for send operation
774
+ - `kit.getSwapStatus(params)` - Fetch the current status of a swap
775
+ - `kit.waitForSwap(params)` - Poll until a swap reaches a terminal status
776
+ - `kit.getTokenRates(params)` - Fetch cached token USD rates
747
777
  - `kit.getSupportedChains(operationType?)` - Query supported chains by operation type (`'bridge'`, `'swap'`, `'earn'`, `'unifiedBalance'`)
748
- - `kit.setCustomFeePolicy(policy)` - Set kit-level custom fee policy
778
+ - `kit.setCustomFeePolicy(policy)` - Set operation-scoped custom fee policies
779
+ - `kit.removeCustomFeePolicy(operation)` - Remove an operation-scoped custom fee policy
749
780
  - `kit.on(event, handler)` - Listen to operation events
750
781
  - `kit.off(event, handler)` - Remove event listener
751
782
  - `kit.unifiedBalance.*` - Unified balance operations (deposit, spend, getBalances, estimateSpend, delegates, and more). See the [`@circle-fin/unified-balance-kit` README](../unified-balance-kit/README.md) for the full API.
package/bridge.cjs CHANGED
@@ -40,6 +40,7 @@ var web3_js = require('@solana/web3.js');
40
40
  require('bn.js');
41
41
  require('@coral-xyz/anchor');
42
42
  require('@noble/curves/ed25519');
43
+ require('viem');
43
44
  var keccak256 = require('@ethersproject/keccak256');
44
45
 
45
46
  function _interopDefault (e) { return e && e.__esModule ? e.default : e; }
@@ -2562,6 +2563,8 @@ class KitError extends Error {
2562
2563
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2563
2564
  Blockchain["XDC"] = "XDC";
2564
2565
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2566
+ Blockchain["X_Layer"] = "X_Layer";
2567
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2565
2568
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2566
2569
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2567
2570
  })(Blockchain || (Blockchain = {}));
@@ -2615,6 +2618,7 @@ var BridgeChain;
2615
2618
  BridgeChain["Unichain"] = "Unichain";
2616
2619
  BridgeChain["World_Chain"] = "World_Chain";
2617
2620
  BridgeChain["XDC"] = "XDC";
2621
+ BridgeChain["X_Layer"] = "X_Layer";
2618
2622
  // Testnet chains with CCTPv2 support
2619
2623
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2620
2624
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2640,6 +2644,7 @@ var BridgeChain;
2640
2644
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2641
2645
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2642
2646
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2647
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2643
2648
  })(BridgeChain || (BridgeChain = {}));
2644
2649
  var UnifiedBalanceChain;
2645
2650
  (function(UnifiedBalanceChain) {
@@ -5177,7 +5182,8 @@ var EarnChain;
5177
5182
  isTestnet: true,
5178
5183
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
5179
5184
  rpcEndpoints: [
5180
- 'https://rpc-amoy.polygon.technology'
5185
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
5186
+ 'https://polygon-amoy.drpc.org'
5181
5187
  ],
5182
5188
  eurcAddress: null,
5183
5189
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -6042,6 +6048,104 @@ var EarnChain;
6042
6048
  }
6043
6049
  });
6044
6050
 
6051
+ /**
6052
+ * X Layer Mainnet chain definition
6053
+ * @remarks
6054
+ * This represents the official production network for the X Layer blockchain.
6055
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6056
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6057
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6058
+ */ const XLayer = defineChain({
6059
+ type: 'evm',
6060
+ chain: Blockchain.X_Layer,
6061
+ name: 'X Layer',
6062
+ title: 'X Layer Mainnet',
6063
+ nativeCurrency: {
6064
+ name: 'OKB',
6065
+ symbol: 'OKB',
6066
+ decimals: 18
6067
+ },
6068
+ chainId: 196,
6069
+ isTestnet: false,
6070
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
6071
+ rpcEndpoints: [
6072
+ 'https://xlayerrpc.okx.com'
6073
+ ],
6074
+ eurcAddress: null,
6075
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
6076
+ usdtAddress: null,
6077
+ cctp: {
6078
+ domain: 37,
6079
+ contracts: {
6080
+ v2: {
6081
+ type: 'split',
6082
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
6083
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
6084
+ confirmations: 65,
6085
+ fastConfirmations: 1
6086
+ }
6087
+ },
6088
+ forwarderSupported: {
6089
+ source: false,
6090
+ destination: false
6091
+ }
6092
+ },
6093
+ kitContracts: {
6094
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
6095
+ }
6096
+ });
6097
+
6098
+ /**
6099
+ * X Layer Testnet chain definition
6100
+ * @remarks
6101
+ * This represents the official test network for the X Layer blockchain.
6102
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
6103
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
6104
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
6105
+ */ const XLayerTestnet = defineChain({
6106
+ type: 'evm',
6107
+ chain: Blockchain.X_Layer_Testnet,
6108
+ name: 'X Layer Testnet',
6109
+ title: 'X Layer Testnet',
6110
+ nativeCurrency: {
6111
+ name: 'OKB',
6112
+ symbol: 'OKB',
6113
+ decimals: 18
6114
+ },
6115
+ chainId: 1952,
6116
+ isTestnet: true,
6117
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
6118
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
6119
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
6120
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
6121
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
6122
+ rpcEndpoints: [
6123
+ 'https://testrpc.xlayer.tech'
6124
+ ],
6125
+ eurcAddress: null,
6126
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
6127
+ usdtAddress: null,
6128
+ cctp: {
6129
+ domain: 37,
6130
+ contracts: {
6131
+ v2: {
6132
+ type: 'split',
6133
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
6134
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
6135
+ confirmations: 65,
6136
+ fastConfirmations: 1
6137
+ }
6138
+ },
6139
+ forwarderSupported: {
6140
+ source: false,
6141
+ destination: false
6142
+ }
6143
+ },
6144
+ kitContracts: {
6145
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
6146
+ }
6147
+ });
6148
+
6045
6149
  /**
6046
6150
  * ZKSync Era Mainnet chain definition
6047
6151
  * @remarks
@@ -6161,6 +6265,8 @@ var Chains = {
6161
6265
  WorldChainSepolia: WorldChainSepolia,
6162
6266
  XDC: XDC,
6163
6267
  XDCApothem: XDCApothem,
6268
+ XLayer: XLayer,
6269
+ XLayerTestnet: XLayerTestnet,
6164
6270
  ZKSyncEra: ZKSyncEra,
6165
6271
  ZKSyncEraSepolia: ZKSyncEraSepolia
6166
6272
  };
@@ -8215,6 +8321,7 @@ const swapTokenEnumSchema = zod.z.enum([
8215
8321
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
8216
8322
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
8217
8323
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
8324
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
8218
8325
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
8219
8326
  // =========================================================================
8220
8327
  // Testnets (alphabetically sorted)
@@ -8249,6 +8356,7 @@ const swapTokenEnumSchema = zod.z.enum([
8249
8356
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
8250
8357
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
8251
8358
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8359
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
8252
8360
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
8253
8361
  }
8254
8362
  };
@@ -9660,7 +9768,7 @@ function resolveOptions(options) {
9660
9768
  }
9661
9769
 
9662
9770
  var name$2 = "@circle-fin/bridge-kit";
9663
- var version$3 = "1.12.2";
9771
+ var version$3 = "1.13.0";
9664
9772
  var pkg$3 = {
9665
9773
  name: name$2,
9666
9774
  version: version$3};
@@ -9697,13 +9805,21 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
9697
9805
  computeFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
9698
9806
  calculateFee: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string()))).optional(),
9699
9807
  resolveFeeRecipientAddress: zod.z.function().returns(zod.z.string().or(zod.z.promise(zod.z.string())))
9700
- }).strict().refine((data)=>{
9808
+ }).strict().superRefine((data, ctx)=>{
9701
9809
  const hasComputeFee = data.computeFee !== undefined;
9702
9810
  const hasCalculateFee = data.calculateFee !== undefined;
9703
- // XOR: exactly one must be provided
9704
- return hasComputeFee !== hasCalculateFee;
9705
- }, {
9706
- message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
9811
+ if (hasComputeFee && hasCalculateFee) {
9812
+ ctx.addIssue({
9813
+ code: zod.z.ZodIssueCode.custom,
9814
+ message: 'Provide either computeFee or calculateFee, not both. Use computeFee (recommended) for human-readable amounts.'
9815
+ });
9816
+ }
9817
+ if (!hasComputeFee && !hasCalculateFee) {
9818
+ ctx.addIssue({
9819
+ code: zod.z.ZodIssueCode.custom,
9820
+ message: 'Provide either computeFee or calculateFee. Use computeFee (recommended) for human-readable amounts.'
9821
+ });
9822
+ }
9707
9823
  });
9708
9824
  /**
9709
9825
  * Assert that the provided value conforms to {@link CustomFeePolicy}.
@@ -12464,14 +12580,32 @@ const CUSTOM_BURN_GAS_ESTIMATE_EVM = 201_525n // p99 and max are same here: 201_
12464
12580
  ;
12465
12581
  const RECEIVE_MESSAGE_GAS_ESTIMATE_EVM = 237_401n // (99p: 163_963n + max: 310_839n) / 2 = 237_401n
12466
12582
  ;
12467
- // Hard execution caps: observed max + ~30% buffer, used as gasLimit overrides on
12468
- // chains whose eth_estimateGas under-reports (e.g. Cronos EIP-7623 calldata floor).
12469
- // Kept separate from the fee-estimate averages above.
12470
- const APPROVE_GAS_LIMIT_EVM = 100_000n // ERC-20 approve observed max ~46k
12583
+ // Gas FLOORS, not ceilings kept separate from the fee-estimate averages
12584
+ // above. `executePreparedChainRequest` submits
12585
+ // max(estimate * buffer, floor), so a chain whose real cost exceeds the floor
12586
+ // is covered by its own estimate, and a chain whose estimator under-reports
12587
+ // (Cronos: returns 30_600 where the EIP-7623 calldata floor is 45_000) is
12588
+ // covered by the floor.
12589
+ //
12590
+ // Two distinct chain surcharges drive these numbers, both measured live:
12591
+ // Sei — ~+51_500 per NEWLY CREATED storage slot (73_595 vs vanilla 22_100);
12592
+ // no flat per-tx surcharge (31_535, identical to Base).
12593
+ // Edge — ~+53_200 flat on EVERY tx (84_751 vs Base 31_535); storage priced
12594
+ // normally. Edge therefore fails warm as well as cold.
12595
+ // A floor must clear the worst COLD cost, since a slot that exists at estimate
12596
+ // time can be consumed before inclusion and cost a full step more on execution.
12597
+ // Each floor is therefore derived from the worst observed estimate *after* the
12598
+ // 1.25x buffer, plus headroom — sizing it below the buffered value would leave
12599
+ // the estimate governing and defeat the point of the floor.
12600
+ //
12601
+ // The `*_GAS_LIMIT_EVM` names are kept despite these being floors: they are
12602
+ // exported, so renaming to `*_GAS_FLOOR_EVM` would be a breaking change for
12603
+ // consumers. Read "LIMIT" here as "the limit we submit", never as a ceiling.
12604
+ const APPROVE_GAS_LIMIT_EVM = 150_000n // buffered worst cold 149_355 (Edge Testnet 119_484 x 1.25) + drift headroom
12471
12605
  ;
12472
- const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 300_000n // observed max 226_506 + ~30%
12606
+ const DEPOSIT_FOR_BURN_GAS_LIMIT_EVM = 500_000n // buffered worst 474_078 (Sei 379_263 x 1.25) + ~26k headroom
12473
12607
  ;
12474
- const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839 + ~30%
12608
+ const RECEIVE_MESSAGE_GAS_LIMIT_EVM = 400_000n // observed max 310_839; clears Cronos' calldata floor ~10x
12475
12609
  ;
12476
12610
  /**
12477
12611
  * The minimum finality threshold for CCTPv2 transfers.
@@ -13936,6 +14070,63 @@ function hasPendingState(analysis, result) {
13936
14070
  return waitForPendingTransaction(pendingStep, adapter, chain);
13937
14071
  }
13938
14072
 
14073
+ /**
14074
+ * Multiplier applied to a successful gas estimate before it is submitted.
14075
+ *
14076
+ * Estimates are exact, not padded: Sei returns 109_739 for an approve that
14077
+ * consumes 107_717 (1.9% headroom). Chains that price storage in large steps
14078
+ * can exceed the estimate if state changes between estimation and inclusion,
14079
+ * so the estimate is padded before use.
14080
+ *
14081
+ * @remarks
14082
+ * This buffer alone does NOT cover Sei's ~51_500 per-new-slot step at approve
14083
+ * scale (25% of ~110_000 is only ~27_500). For approve, the FLOOR is what
14084
+ * covers a slot that exists at estimation time and is consumed before
14085
+ * inclusion — so do not lower `APPROVE_GAS_LIMIT_EVM` on the reasoning that
14086
+ * the estimate covers it. For burn the buffer does cover a step (25% of
14087
+ * ~300_000 exceeds 51_500).
14088
+ */ const GAS_ESTIMATE_BUFFER_PERCENT = 125n;
14089
+ /**
14090
+ * Resolve the gas limit for an EVM request as `max(estimate * buffer, floor)`.
14091
+ *
14092
+ * Estimates first so chains whose real cost exceeds the floor are covered by
14093
+ * their own measurement, and falls back to the floor whenever estimation is
14094
+ * unavailable or under-reports. Estimation failure is never fatal here: before
14095
+ * floors existed these requests were submitted with a pinned limit and no
14096
+ * estimate at all, so degrading to the floor is never worse than the previous
14097
+ * behaviour.
14098
+ *
14099
+ * @param request - The prepared EVM request to size a gas limit for
14100
+ * @param gasFloor - The minimum gas limit to submit, in gas units
14101
+ * @returns The gas limit to submit, in gas units
14102
+ * @throws Never — estimation failures degrade to `gasFloor`
14103
+ *
14104
+ * @example
14105
+ * ```typescript
14106
+ * const gasLimit = await resolveGasLimit(request, 150_000)
14107
+ * ```
14108
+ */ const resolveGasLimit = async (request, gasFloor)=>{
14109
+ try {
14110
+ // Deliberately called without a `fallback`: both the viem and ethers
14111
+ // adapters *return* the supplied fallback object when estimation reverts
14112
+ // rather than throwing, which would set the estimate to the floor and then
14113
+ // multiply it by the buffer below. Omitting it routes reverts through the
14114
+ // catch, so a failed estimate degrades to exactly the floor.
14115
+ const estimate = await request.estimate();
14116
+ // The arithmetic stays inside the try on purpose. `EstimatedGas.gas` is
14117
+ // typed `bigint`, but adapters are a public extension point and may be
14118
+ // implemented in plain JS, so a non-bigint `gas` would throw here
14119
+ // ("Cannot mix BigInt and other types"). Guarding it keeps the documented
14120
+ // contract — estimation never aborts a step, it degrades to the floor.
14121
+ const buffered = estimate.gas * GAS_ESTIMATE_BUFFER_PERCENT / 100n;
14122
+ // Convert before comparing: Math.max throws on BigInt operands, and gas
14123
+ // units are far below Number.MAX_SAFE_INTEGER so the narrowing is lossless.
14124
+ return Math.max(Number(buffered), gasFloor);
14125
+ } catch {
14126
+ // Estimation is best-effort; the floor is the known-safe value.
14127
+ return gasFloor;
14128
+ }
14129
+ };
13939
14130
  /**
13940
14131
  * Executes a prepared chain request and returns the result as a bridge step.
13941
14132
  *
@@ -13949,8 +14140,8 @@ function hasPendingState(analysis, result) {
13949
14140
  * - `adapter`: The adapter that will execute the transaction
13950
14141
  * - `confirmations`: The number of confirmations to wait for (defaults to 1)
13951
14142
  * - `timeout`: The timeout for the request in milliseconds
13952
- * - `gasLimit`: Optional explicit gas limit (number) forwarded to EVM execute,
13953
- * bypassing `eth_estimateGas`; ignored for non-EVM requests
14143
+ * - `gasFloor`: Optional minimum gas limit (number); the request is submitted
14144
+ * with `max(estimate * 1.25, gasFloor)`. Ignored for non-EVM requests
13954
14145
  * @returns The bridge step with the transaction details and explorer URL
13955
14146
  * @throws If the transaction execution fails
13956
14147
  *
@@ -13965,7 +14156,7 @@ function hasPendingState(analysis, result) {
13965
14156
  * })
13966
14157
  * console.log('Transaction hash:', step.txHash)
13967
14158
  * ```
13968
- */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasLimit }) {
14159
+ */ async function executePreparedChainRequest({ name, request, adapter, chain, confirmations = 1, timeout, gasFloor }) {
13969
14160
  const step = {
13970
14161
  name,
13971
14162
  state: 'pending'
@@ -13978,8 +14169,8 @@ function hasPendingState(analysis, result) {
13978
14169
  step.state = 'noop';
13979
14170
  return step;
13980
14171
  }
13981
- const txHash = request.type === 'evm' && gasLimit !== undefined ? await request.execute({
13982
- gasLimit
14172
+ const txHash = request.type === 'evm' && gasFloor !== undefined ? await request.execute({
14173
+ gasLimit: await resolveGasLimit(request, gasFloor)
13983
14174
  }) : await request.execute();
13984
14175
  step.txHash = txHash;
13985
14176
  const retryOptions = {
@@ -14053,7 +14244,7 @@ function hasPendingState(analysis, result) {
14053
14244
  adapter: params.source.adapter,
14054
14245
  chain: params.source.chain,
14055
14246
  request: await provider.approve(params.source, approvalAmount),
14056
- gasLimit: Number(APPROVE_GAS_LIMIT_EVM)
14247
+ gasFloor: Number(APPROVE_GAS_LIMIT_EVM)
14057
14248
  });
14058
14249
  }
14059
14250
 
@@ -14081,7 +14272,7 @@ function hasPendingState(analysis, result) {
14081
14272
  adapter: params.source.adapter,
14082
14273
  chain: params.source.chain,
14083
14274
  request: await provider.burn(params),
14084
- gasLimit: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
14275
+ gasFloor: Number(DEPOSIT_FOR_BURN_GAS_LIMIT_EVM)
14085
14276
  });
14086
14277
  }
14087
14278
 
@@ -14175,10 +14366,9 @@ function hasPendingState(analysis, result) {
14175
14366
  request: mintRequest,
14176
14367
  // Some chains (e.g. Cronos) enforce an EIP-7623 calldata gas floor that
14177
14368
  // eth_estimateGas does not account for, returning a below-floor value
14178
- // without reverting. Pinning to a value above the observed execution max
14179
- // (310_839) bypasses re-estimation and guarantees we clear both the floor
14180
- // and the actual execution cost.
14181
- gasLimit: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
14369
+ // without reverting. The floor covers those; chains that cost more than the
14370
+ // floor are covered by their own estimate.
14371
+ gasFloor: Number(RECEIVE_MESSAGE_GAS_LIMIT_EVM)
14182
14372
  });
14183
14373
  // Add forwarded: false for non-relayer mints
14184
14374
  return {
@@ -14701,7 +14891,7 @@ const mockAttestationMessage = {
14701
14891
  return step;
14702
14892
  }
14703
14893
 
14704
- var version$2 = "1.10.1";
14894
+ var version$2 = "1.10.2";
14705
14895
  var pkg$2 = {
14706
14896
  version: version$2};
14707
14897
 
@@ -17299,7 +17489,9 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17299
17489
  headers: context.headers
17300
17490
  }
17301
17491
  });
17302
- if (hasBoth) {
17492
+ if (context.customFeePolicy?.bridge != null) {
17493
+ kit.setCustomFeePolicy(context.customFeePolicy.bridge);
17494
+ } else if (hasBoth) {
17303
17495
  kit.setCustomFeePolicy({
17304
17496
  calculateFee: async (params)=>{
17305
17497
  const feeStr = await getFee('bridge', params);
@@ -17315,7 +17507,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
17315
17507
  };
17316
17508
 
17317
17509
  var name$1 = "@circle-fin/swap-kit";
17318
- var version$1 = "1.5.0";
17510
+ var version$1 = "1.5.1";
17319
17511
  var pkg$1 = {
17320
17512
  name: name$1,
17321
17513
  version: version$1};
@@ -19258,7 +19450,7 @@ new Set(Object.values(Blockchain));
19258
19450
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
19259
19451
 
19260
19452
  var name = "@circle-fin/earn-kit";
19261
- var version = "1.4.0";
19453
+ var version = "1.5.0";
19262
19454
  var pkg = {
19263
19455
  name: name,
19264
19456
  version: version};
@@ -20269,11 +20461,16 @@ const sourceAdapterContextSchema = zod.z.object({
20269
20461
  *
20270
20462
  * Validate the optional Kit Key field using the standard `apiKeySchema`
20271
20463
  * format (`KIT_KEY:<keyId>:<keySecret>`). When omitted, the SDK
20272
- * operates in permissionless mode.
20464
+ * operates in permissionless mode. `baseUrl` overrides the Earn Service
20465
+ * endpoint (e.g. staging); `batchTransactions: false` opts out of atomic
20466
+ * batched execution. Both are forwarded to the provider, so this `.strict()`
20467
+ * schema must accept them or a valid config object is rejected.
20273
20468
  *
20274
20469
  * @internal
20275
20470
  */ const earnConfigSchema = zod.z.object({
20276
- kitKey: apiKeySchema.optional()
20471
+ kitKey: apiKeySchema.optional(),
20472
+ baseUrl: zod.z.string().optional(),
20473
+ batchTransactions: zod.z.boolean().optional()
20277
20474
  }).strict();
20278
20475
  /**
20279
20476
  * Canonical decimal form: a leading digit with no leading zeros (a single