@circle-fin/app-kit 1.8.0 → 1.9.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/earn.cjs CHANGED
@@ -843,36 +843,6 @@ class KitError extends Error {
843
843
  };
844
844
  return new KitError(errorDetails);
845
845
  }
846
- /**
847
- * Creates error for unsupported token on chain.
848
- *
849
- * This error is thrown when a token is not supported on the specified chain.
850
- *
851
- * @param token - The token symbol (e.g., 'USDC', 'USDT')
852
- * @param chain - The chain name where the token is unsupported
853
- * @returns KitError with specific token support details
854
- *
855
- * @example
856
- * ```typescript
857
- * import { createUnsupportedTokenError } from '@core/errors'
858
- *
859
- * throw createUnsupportedTokenError('USDC', 'UnknownChain')
860
- * // Message: "USDC is not supported on UnknownChain"
861
- * ```
862
- */ function createUnsupportedTokenError(token, chain) {
863
- const errorDetails = {
864
- ...InputError.UNSUPPORTED_TOKEN,
865
- recoverability: 'FATAL',
866
- message: `${token} is not supported on ${chain}.`,
867
- cause: {
868
- trace: {
869
- token,
870
- chain
871
- }
872
- }
873
- };
874
- return new KitError(errorDetails);
875
- }
876
846
  /**
877
847
  * Creates error for invalid chain configuration.
878
848
  *
@@ -1799,7 +1769,7 @@ class KitError extends Error {
1799
1769
  *
1800
1770
  * Error code ranges:
1801
1771
  * - 1100-1105: INPUT errors — invalid inputs, unsupported configurations
1802
- * - 8100-8104: SERVICE errors — retryable backend/provider failures
1772
+ * - 8100-8105: SERVICE errors — retryable backend/provider failures
1803
1773
  *
1804
1774
  * @example
1805
1775
  * ```typescript
@@ -1875,6 +1845,11 @@ class KitError extends Error {
1875
1845
  code: 8104,
1876
1846
  name: 'EARN_PAUSED',
1877
1847
  type: 'SERVICE'
1848
+ },
1849
+ /** Position PnL is still reconciling and can be retried. */ POSITION_PNL_PENDING: {
1850
+ code: 8105,
1851
+ name: 'EARN_POSITION_PNL_PENDING',
1852
+ type: 'SERVICE'
1878
1853
  }
1879
1854
  };
1880
1855
 
@@ -1898,11 +1873,11 @@ function getOptionalString(value) {
1898
1873
  * - vault-not-found, unsupported-chain, unsupported-vault,
1899
1874
  * signature-rejected, invalid-id, invalid-batch-size, position-not-registered,
1900
1875
  * withdrawal-max-exceeded, insufficient-balance, bridge prepare idempotency
1901
- * conflicts
1876
+ * conflicts, invalid vault-asset amount precision
1902
1877
  *
1903
1878
  * SERVICE errors (RETRYABLE) — try again later:
1904
1879
  * - signing-failed, provider-error, rewards-fetch-failed,
1905
- * internal-error, vault-refresh-busy, off-chain-paused,
1880
+ * internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
1906
1881
  * bridge failures/status lookup failures
1907
1882
  *
1908
1883
  * Unrecognized codes fall through to `parseApiError` for HTTP-status-based
@@ -2082,6 +2057,20 @@ function getOptionalString(value) {
2082
2057
  recoverability: 'RETRYABLE'
2083
2058
  }
2084
2059
  ],
2060
+ [
2061
+ 380415,
2062
+ {
2063
+ errorDef: EarnError.INVALID_INPUT,
2064
+ recoverability: 'FATAL'
2065
+ }
2066
+ ],
2067
+ [
2068
+ 380416,
2069
+ {
2070
+ errorDef: EarnError.POSITION_PNL_PENDING,
2071
+ recoverability: 'RETRYABLE'
2072
+ }
2073
+ ],
2085
2074
  // Bridge (380_5XX)
2086
2075
  [
2087
2076
  380500,
@@ -2117,6 +2106,13 @@ function getOptionalString(value) {
2117
2106
  errorDef: EarnError.PROVIDER_ERROR,
2118
2107
  recoverability: 'RETRYABLE'
2119
2108
  }
2109
+ ],
2110
+ [
2111
+ 380505,
2112
+ {
2113
+ errorDef: EarnError.PROVIDER_ERROR,
2114
+ recoverability: 'FATAL'
2115
+ }
2120
2116
  ]
2121
2117
  ]);
2122
2118
  /**
@@ -2219,6 +2215,8 @@ function getOptionalString(value) {
2219
2215
  Blockchain["Celo_Alfajores_Testnet"] = "Celo_Alfajores_Testnet";
2220
2216
  Blockchain["Codex"] = "Codex";
2221
2217
  Blockchain["Codex_Testnet"] = "Codex_Testnet";
2218
+ Blockchain["Cronos"] = "Cronos";
2219
+ Blockchain["Cronos_Testnet"] = "Cronos_Testnet";
2222
2220
  Blockchain["Edge"] = "Edge";
2223
2221
  Blockchain["Edge_Testnet"] = "Edge_Testnet";
2224
2222
  Blockchain["Ethereum"] = "Ethereum";
@@ -2301,6 +2299,7 @@ var BridgeChain;
2301
2299
  BridgeChain["Avalanche"] = "Avalanche";
2302
2300
  BridgeChain["Base"] = "Base";
2303
2301
  BridgeChain["Codex"] = "Codex";
2302
+ BridgeChain["Cronos"] = "Cronos";
2304
2303
  BridgeChain["Edge"] = "Edge";
2305
2304
  BridgeChain["Ethereum"] = "Ethereum";
2306
2305
  BridgeChain["HyperEVM"] = "HyperEVM";
@@ -2325,6 +2324,7 @@ var BridgeChain;
2325
2324
  BridgeChain["Avalanche_Fuji"] = "Avalanche_Fuji";
2326
2325
  BridgeChain["Base_Sepolia"] = "Base_Sepolia";
2327
2326
  BridgeChain["Codex_Testnet"] = "Codex_Testnet";
2327
+ BridgeChain["Cronos_Testnet"] = "Cronos_Testnet";
2328
2328
  BridgeChain["Edge_Testnet"] = "Edge_Testnet";
2329
2329
  BridgeChain["Ethereum_Sepolia"] = "Ethereum_Sepolia";
2330
2330
  BridgeChain["HyperEVM_Testnet"] = "HyperEVM_Testnet";
@@ -2576,23 +2576,6 @@ var EarnChain;
2576
2576
  }
2577
2577
  });
2578
2578
 
2579
- /**
2580
- * Standard decimal places for tokens.
2581
- *
2582
- * These constants define the decimal precision used by different token types
2583
- * across the SDK. They are used for amount conversions between human-readable
2584
- * format and smallest units (base units).
2585
- *
2586
- * @remarks
2587
- * - Most stablecoins (USDC, EURC, USDT, PYUSD) use 6 decimal places
2588
- * - Some stablecoins (DAI, USDE) and most native tokens (ETH, POL, PLUME) use 18 decimals
2589
- * - The NATIVE alias uses chain-specific decimals which may vary (e.g., SOL uses 9)
2590
- */ /**
2591
- * Standard decimal places for 6-decimal tokens.
2592
- *
2593
- * Used by most stablecoins: USDC, EURC, USDT, PYUSD
2594
- */ const TOKEN_DECIMALS_6 = 6;
2595
-
2596
2579
  /**
2597
2580
  * @packageDocumentation
2598
2581
  * @module SwapTokenRegistry
@@ -3387,6 +3370,96 @@ var EarnChain;
3387
3370
  }
3388
3371
  });
3389
3372
 
3373
+ /**
3374
+ * Cronos Mainnet chain definition
3375
+ * @remarks
3376
+ * This represents the official production network for the Cronos blockchain.
3377
+ * Cronos is an EVM-compatible blockchain.
3378
+ */ const Cronos = defineChain({
3379
+ type: 'evm',
3380
+ chain: Blockchain.Cronos,
3381
+ name: 'Cronos',
3382
+ title: 'Cronos Mainnet',
3383
+ nativeCurrency: {
3384
+ name: 'Cronos',
3385
+ symbol: 'CRO',
3386
+ decimals: 18
3387
+ },
3388
+ chainId: 25,
3389
+ isTestnet: false,
3390
+ explorerUrl: 'https://cronoscan.com/tx/{hash}',
3391
+ rpcEndpoints: [
3392
+ 'https://evm.cronos.org'
3393
+ ],
3394
+ eurcAddress: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
3395
+ usdcAddress: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
3396
+ usdtAddress: null,
3397
+ cctp: {
3398
+ domain: 32,
3399
+ contracts: {
3400
+ v2: {
3401
+ type: 'split',
3402
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
3403
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
3404
+ confirmations: 1,
3405
+ fastConfirmations: 1
3406
+ }
3407
+ },
3408
+ forwarderSupported: {
3409
+ source: false,
3410
+ destination: false
3411
+ }
3412
+ },
3413
+ kitContracts: {
3414
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
3415
+ }
3416
+ });
3417
+
3418
+ /**
3419
+ * Cronos Testnet chain definition
3420
+ * @remarks
3421
+ * This represents the official test network for the Cronos blockchain.
3422
+ * Cronos is an EVM-compatible blockchain.
3423
+ */ const CronosTestnet = defineChain({
3424
+ type: 'evm',
3425
+ chain: Blockchain.Cronos_Testnet,
3426
+ name: 'Cronos Testnet',
3427
+ title: 'Cronos Testnet',
3428
+ nativeCurrency: {
3429
+ name: 'CRO',
3430
+ symbol: 'tCRO',
3431
+ decimals: 18
3432
+ },
3433
+ chainId: 338,
3434
+ isTestnet: true,
3435
+ explorerUrl: 'https://explorer.cronos.org/testnet/tx/{hash}',
3436
+ rpcEndpoints: [
3437
+ 'https://evm-t3.cronos.org'
3438
+ ],
3439
+ eurcAddress: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
3440
+ usdcAddress: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
3441
+ usdtAddress: null,
3442
+ cctp: {
3443
+ domain: 32,
3444
+ contracts: {
3445
+ v2: {
3446
+ type: 'split',
3447
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
3448
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
3449
+ confirmations: 1,
3450
+ fastConfirmations: 1
3451
+ }
3452
+ },
3453
+ forwarderSupported: {
3454
+ source: false,
3455
+ destination: false
3456
+ }
3457
+ },
3458
+ kitContracts: {
3459
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
3460
+ }
3461
+ });
3462
+
3390
3463
  /**
3391
3464
  * Edge Mainnet chain definition
3392
3465
  * @remarks
@@ -5738,6 +5811,8 @@ var Chains = {
5738
5811
  CeloAlfajoresTestnet: CeloAlfajoresTestnet,
5739
5812
  Codex: Codex,
5740
5813
  CodexTestnet: CodexTestnet,
5814
+ Cronos: Cronos,
5815
+ CronosTestnet: CronosTestnet,
5741
5816
  Edge: Edge,
5742
5817
  EdgeTestnet: EdgeTestnet,
5743
5818
  Ethereum: Ethereum,
@@ -7169,6 +7244,7 @@ const swapTokenEnumSchema = zod.z.enum([
7169
7244
  [Blockchain.Base]: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
7170
7245
  [Blockchain.Celo]: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
7171
7246
  [Blockchain.Codex]: '0xd996633a415985DBd7D6D12f4A4343E31f5037cf',
7247
+ [Blockchain.Cronos]: '0x3D7F2C478aAfdB65542BCB44bCeeC05849999d2D',
7172
7248
  [Blockchain.Edge]: '0x98d2919b9A214E6Fa5384AC81E6864bA686Ad74c',
7173
7249
  [Blockchain.Ethereum]: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
7174
7250
  [Blockchain.Hedera]: '0.0.456858',
@@ -7202,6 +7278,7 @@ const swapTokenEnumSchema = zod.z.enum([
7202
7278
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7203
7279
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
7204
7280
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7281
+ [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7205
7282
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
7206
7283
  [Blockchain.Ethereum_Sepolia]: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
7207
7284
  [Blockchain.Hedera_Testnet]: '0.0.429274',
@@ -7269,13 +7346,22 @@ const swapTokenEnumSchema = zod.z.enum([
7269
7346
  symbol: 'EURC',
7270
7347
  decimals: 6,
7271
7348
  locators: {
7349
+ // =========================================================================
7350
+ // Mainnets
7351
+ // =========================================================================
7272
7352
  [Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
7273
7353
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
7354
+ [Blockchain.Cronos]: '0xA6dE01a2d62C6B5f3525d768f34d276652C554c8',
7274
7355
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
7275
7356
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
7276
7357
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
7358
+ // =========================================================================
7277
7359
  // Testnets
7278
- [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a'
7360
+ // =========================================================================
7361
+ [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
7362
+ [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
7363
+ [Blockchain.Cronos_Testnet]: '0x31f7538adb53cF16350e6B0c89d03D91b7D12c46',
7364
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
7279
7365
  }
7280
7366
  };
7281
7367
 
@@ -7485,8 +7571,7 @@ const swapTokenEnumSchema = zod.z.enum([
7485
7571
  * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7486
7572
  *
7487
7573
  * @remarks
7488
- * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7489
- * on Arc Testnet.
7574
+ * Built-in cirBTC definition for the TokenRegistry.
7490
7575
  *
7491
7576
  * @example
7492
7577
  * ```typescript
@@ -7502,7 +7587,15 @@ const swapTokenEnumSchema = zod.z.enum([
7502
7587
  symbol: 'cirBTC',
7503
7588
  decimals: 8,
7504
7589
  locators: {
7505
- [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7590
+ // =========================================================================
7591
+ // Mainnets
7592
+ // =========================================================================
7593
+ [Blockchain.Ethereum]: '0x72DFB2E44f59C5AD2bAFE84314E5b99a7cd5075E',
7594
+ // =========================================================================
7595
+ // Testnets
7596
+ // =========================================================================
7597
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7598
+ [Blockchain.Ethereum_Sepolia]: '0x3a3fe695F684Bf9b9e43CF43C2b895Ea5e392bB3'
7506
7599
  }
7507
7600
  };
7508
7601
 
@@ -7941,11 +8034,11 @@ const swapTokenEnumSchema = zod.z.enum([
7941
8034
  return pkgName.replace('@circle-fin/', '');
7942
8035
  }
7943
8036
 
7944
- var name$2 = "@circle-fin/bridge-kit";
7945
- var version$2 = "1.11.0";
7946
- var pkg$2 = {
7947
- name: name$2,
7948
- version: version$2};
8037
+ var name$3 = "@circle-fin/bridge-kit";
8038
+ var version$3 = "1.12.0";
8039
+ var pkg$3 = {
8040
+ name: name$3,
8041
+ version: version$3};
7949
8042
 
7950
8043
  /**
7951
8044
  * Schema for validating BridgeKit custom fee policy.
@@ -8793,16 +8886,16 @@ var TransferSpeed;
8793
8886
  * @internal
8794
8887
  */ Object.values(Chains).filter((chain)=>isCCTPV2Supported(chain));
8795
8888
 
8796
- /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg$2.name);
8889
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg$3.name);
8797
8890
 
8798
8891
  // Auto-register this kit for user agent tracking
8799
- registerKit(`${pkg$2.name}/${pkg$2.version}`);
8892
+ registerKit(`${pkg$3.name}/${pkg$3.version}`);
8800
8893
 
8801
- var name$1 = "@circle-fin/swap-kit";
8802
- var version$1 = "1.3.0";
8803
- var pkg$1 = {
8804
- name: name$1,
8805
- version: version$1};
8894
+ var name$2 = "@circle-fin/swap-kit";
8895
+ var version$2 = "1.3.2";
8896
+ var pkg$2 = {
8897
+ name: name$2,
8898
+ version: version$2};
8806
8899
 
8807
8900
  const chainIdentifierField = zod.z.custom((value)=>chainIdentifierSchema.safeParse(value).success, {
8808
8901
  message: 'chain must be a valid chain identifier'
@@ -12087,17 +12180,20 @@ new Set(Object.values(Blockchain));
12087
12180
 
12088
12181
  new Set(Object.values(Blockchain));
12089
12182
 
12090
- /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg$1.name);
12183
+ /** SDK name used in telemetry payloads. */ resolveKitSdkName(pkg$2.name);
12091
12184
 
12092
12185
  // Auto-register this kit for user agent tracking
12093
- registerKit(`${pkg$1.name}/${pkg$1.version}`);
12186
+ registerKit(`${pkg$2.name}/${pkg$2.version}`);
12094
12187
 
12095
- var name = "@circle-fin/earn-kit";
12096
- var version = "1.2.0";
12097
- var pkg = {
12098
- name: name,
12099
- version: version};
12188
+ var name$1 = "@circle-fin/earn-kit";
12189
+ var version$1 = "1.2.2";
12190
+ var pkg$1 = {
12191
+ name: name$1,
12192
+ version: version$1};
12100
12193
 
12194
+ const EARN_BRIDGE_ERC3009_TOKEN_SYMBOLS = [
12195
+ 'USDC'
12196
+ ];
12101
12197
  /**
12102
12198
  * Default base URL for the Earn Service API.
12103
12199
  *
@@ -12142,20 +12238,7 @@ var pkg = {
12142
12238
  */ const EARN_BRIDGE_DESTINATION_CHAIN_TO_API = {
12143
12239
  [Blockchain.Arc_Testnet]: CHAIN_TO_API[Blockchain.Arc_Testnet]
12144
12240
  };
12145
- /**
12146
- * Expected EIP-712 domain of the ERC-3009 authorization token per source
12147
- * chain.
12148
- *
12149
- * Cross-chain Earn deposits are USDC-only, but USDC deployments differ on the
12150
- * EIP-712 domain name: newer deployments use `USDC` while older ones (for
12151
- * example Arbitrum Sepolia) kept `USD Coin` from the FiatToken V2 upgrade.
12152
- * These values mirror the bridge service's per-chain domain table and the
12153
- * on-chain `DOMAIN_SEPARATOR` inputs. Asserted before signing so a tampered
12154
- * prepare response cannot point the signature at another token. The
12155
- * `satisfies` check forces a domain entry whenever a source chain is added.
12156
- *
12157
- * @internal
12158
- */ const EARN_BRIDGE_ERC3009_DOMAINS = {
12241
+ const EARN_BRIDGE_USDC_ERC3009_DOMAINS = {
12159
12242
  [Blockchain.Arbitrum_Sepolia]: {
12160
12243
  name: 'USD Coin',
12161
12244
  version: '2'
@@ -12169,6 +12252,21 @@ var pkg = {
12169
12252
  version: '2'
12170
12253
  }
12171
12254
  };
12255
+ /**
12256
+ * Expected EIP-712 domain of the ERC-3009 authorization token per
12257
+ * cross-chain-supported source token and source chain.
12258
+ *
12259
+ * The bridge service prepares an ERC-3009 payload for the deposited token. We
12260
+ * keep this table as an explicit SDK allowlist because token deployments can
12261
+ * differ by source chain and by domain name/version. These values mirror the
12262
+ * bridge service's per-chain domain table and the on-chain DOMAIN_SEPARATOR
12263
+ * inputs. Asserted before signing so a tampered prepare response cannot point
12264
+ * the signature at another token or unexpected token domain.
12265
+ *
12266
+ * @internal
12267
+ */ const EARN_BRIDGE_ERC3009_DOMAINS = {
12268
+ USDC: EARN_BRIDGE_USDC_ERC3009_DOMAINS
12269
+ };
12172
12270
  /**
12173
12271
  * Maximum tolerated remaining lifetime of a prepared ERC-3009 authorization.
12174
12272
  *
@@ -12628,6 +12726,16 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12628
12726
  return approvalTxHash;
12629
12727
  }
12630
12728
 
12729
+ /** @internal */ function isSameAddress(actual, expected) {
12730
+ return actual.toLowerCase() === expected.toLowerCase();
12731
+ }
12732
+ /** @internal */ function assertDecimalPlaces(field, value, tokenDecimals, reason) {
12733
+ const decimals = value.split('.')[1]?.length ?? 0;
12734
+ if (decimals > tokenDecimals) {
12735
+ throw createValidationFailedError(field, value, reason);
12736
+ }
12737
+ }
12738
+
12631
12739
  /**
12632
12740
  * Build the `tokenInputs` array forwarded to the adapter contract's
12633
12741
  * `execute(executeParams, tokenInputs, signature)` call.
@@ -12651,15 +12759,17 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12651
12759
  *
12652
12760
  * @example
12653
12761
  * ```typescript
12654
- * const approvedToken = assertHexAddress(
12655
- * 'chain.usdcAddress',
12656
- * chain.usdcAddress,
12657
- * )
12658
- *
12659
- * const tokenInputs = buildEarnTokenInputs(
12762
+ * const approvalToken = resolveEarnApprovalToken(
12660
12763
  * executionPayload.executionParams,
12661
- * approvedToken,
12662
12764
  * )
12765
+ *
12766
+ * const tokenInputs =
12767
+ * approvalToken === undefined
12768
+ * ? []
12769
+ * : buildEarnTokenInputs(
12770
+ * executionPayload.executionParams,
12771
+ * approvalToken,
12772
+ * )
12663
12773
  * ```
12664
12774
  *
12665
12775
  * @internal
@@ -12671,7 +12781,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12671
12781
  return;
12672
12782
  }
12673
12783
  const { tokenIn } = instruction;
12674
- if (tokenIn.toLowerCase() !== approvedToken.toLowerCase()) {
12784
+ if (!isSameAddress(tokenIn, approvedToken)) {
12675
12785
  throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
12676
12786
  }
12677
12787
  tokenInputs.push({
@@ -12683,6 +12793,33 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12683
12793
  });
12684
12794
  return tokenInputs;
12685
12795
  }
12796
+ /**
12797
+ * Resolve the single token that needs adapter allowance from signed Earn
12798
+ * instructions.
12799
+ *
12800
+ * @param executionParams - Service-signed `ExecutionParams` forwarded to the adapter.
12801
+ * @returns The token requested by positive approval instructions, or `undefined`.
12802
+ * @throws {@link KitError} If positive approval instructions reference multiple tokens.
12803
+ *
12804
+ * @internal
12805
+ */ function resolveEarnApprovalToken(executionParams) {
12806
+ let approvedToken;
12807
+ executionParams.instructions.forEach((instruction, index)=>{
12808
+ const amount = BigInt(instruction.amountToApprove);
12809
+ if (amount <= 0n) {
12810
+ return;
12811
+ }
12812
+ const { tokenIn } = instruction;
12813
+ if (approvedToken === undefined) {
12814
+ approvedToken = tokenIn;
12815
+ return;
12816
+ }
12817
+ if (!isSameAddress(tokenIn, approvedToken)) {
12818
+ throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
12819
+ }
12820
+ });
12821
+ return approvedToken;
12822
+ }
12686
12823
 
12687
12824
  /**
12688
12825
  * Prepare an earn adapter action, execute it, wait for confirmation, and
@@ -13174,6 +13311,7 @@ const hexSignatureSchema = evmSignatureSchema;
13174
13311
  const hexAddressSchema = evmAddressSchema;
13175
13312
  // '0x' prefix + 32 bytes * 2 hex chars.
13176
13313
  const BYTES32_HEX_LENGTH = 66;
13314
+ const bridgeFeeTokenSchema = hexAddressSchema;
13177
13315
  /**
13178
13316
  * Zod schema for a non-negative uint256-like value.
13179
13317
  *
@@ -13460,14 +13598,12 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13460
13598
  *
13461
13599
  * The bridge prepare path returns one item per collected source fee (e.g.
13462
13600
  * `PRE_FINALITY` for the fast-burn fee and `FORWARD` for the destination
13463
- * forward fee), all denominated in the shared `feeToken`. The signed
13464
- * authorization `value` is `principal + sum(items[].amount)`, so the pre-sign
13465
- * guard parses these to recompute the expected value. Each item's `amount` is
13466
- * a base-unit decimal string in `feeToken` units.
13601
+ * forward fee), all denominated in the shared `feeToken`. Each item's `amount`
13602
+ * is a base-unit decimal string in `feeToken` units.
13467
13603
  *
13468
13604
  * @internal
13469
13605
  */ const bridgeFeeQuoteSchema = zod.z.object({
13470
- feeToken: hexAddressSchema,
13606
+ feeToken: bridgeFeeTokenSchema,
13471
13607
  items: zod.z.array(zod.z.object({
13472
13608
  type: zod.z.string(),
13473
13609
  amount: uint256LikeSchema
@@ -13483,6 +13619,12 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13483
13619
  erc3009TypedData: erc3009TypedDataSchema,
13484
13620
  feeQuote: bridgeFeeQuoteSchema
13485
13621
  }).passthrough();
13622
+ const bridgeDepositPrepareReviewSchema = zod.z.object({
13623
+ sourceChain: zod.z.string(),
13624
+ destinationChain: zod.z.string(),
13625
+ amount: amountJsonSchema,
13626
+ vaultAddress: hexAddressSchema
13627
+ }).passthrough();
13486
13628
  /**
13487
13629
  * Zod schema for the bridge deposit prepare payload.
13488
13630
  *
@@ -13494,7 +13636,7 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13494
13636
  execId: bridgeDepositExecIdSchema,
13495
13637
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
13496
13638
  expiresAt: zod.z.string().datetime(),
13497
- review: zod.z.unknown().optional()
13639
+ review: bridgeDepositPrepareReviewSchema
13498
13640
  });
13499
13641
  /**
13500
13642
  * Zod schema for the `POST /v1/earnKit/bridge/deposit/prepare` API response.
@@ -13619,10 +13761,11 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13619
13761
  * Zod schema for a fee entry in an EarnKit API response.
13620
13762
  *
13621
13763
  * Shared across deposit and withdrawal responses (and reusable for real
13622
- * charged fees, not just quote estimates). `type` identifies the fee category
13623
- * for cross-chain deposit quotes this is the kits-proxy fee-quote item type
13624
- * (e.g. `'FORWARD'`, `'PRE_FINALITY'`). `status` qualifies the fee (e.g.
13625
- * `'estimated'` for a pre-sign cross-chain fee). Both are omitted on plain fees.
13764
+ * charged fees, not just quote estimates). `type` identifies the fee category.
13765
+ * For cross-chain deposit quotes this is the kits-proxy fee-quote item type
13766
+ * (e.g. `'FORWARD'`, `'PRE_FINALITY'`). For withdrawal quotes, Circle fees use
13767
+ * `type: 'circle'`. `status` qualifies the fee (e.g. `'estimated'` for a
13768
+ * pre-sign cross-chain fee). Both are omitted on plain fees.
13626
13769
  *
13627
13770
  * @internal
13628
13771
  */ const feeSchema = zod.z.object({
@@ -13894,6 +14037,60 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13894
14037
  return claimRewardsQuoteResponseSchema.safeParse(value).success;
13895
14038
  }
13896
14039
 
14040
+ var name = "@circle-fin/provider-earn-service";
14041
+ var version = "1.2.2";
14042
+ var pkg = {
14043
+ name: name,
14044
+ version: version};
14045
+
14046
+ /**
14047
+ * HTTP header name used to report the EarnKit SDK version to the backend.
14048
+ *
14049
+ * @internal
14050
+ */ const SDK_VERSION_HEADER = 'X-EarnKit-SDK-Version';
14051
+ /**
14052
+ * Resolve the SDK version string from its source components.
14053
+ *
14054
+ * Resolution order:
14055
+ * 1. The registered kit identifier (preferred) — e.g. `@circle-fin/earn-kit/1.1.0`
14056
+ * becomes `earn-kit/1.1.0`.
14057
+ * 2. The provider package itself (fallback when no kit is registered) —
14058
+ * e.g. `provider-earn-service/1.1.0`.
14059
+ * 3. The literal `unknown` when neither source is available.
14060
+ *
14061
+ * Kept as a pure function (no global / module reads) so every branch is
14062
+ * unit-testable.
14063
+ *
14064
+ * @param kitId - The registered kit identifier, or `undefined`.
14065
+ * @param providerName - The provider package name (e.g. `@circle-fin/provider-earn-service`).
14066
+ * @param providerVersion - The provider package version, or `undefined`.
14067
+ * @returns The formatted SDK version string.
14068
+ *
14069
+ * @internal
14070
+ */ function formatSdkVersion(kitId, providerName, providerVersion) {
14071
+ if (kitId !== undefined && kitId !== '') {
14072
+ // e.g. '@circle-fin/earn-kit/1.1.0' -> 'earn-kit/1.1.0'
14073
+ return resolveKitSdkName(kitId);
14074
+ }
14075
+ {
14076
+ // e.g. '@circle-fin/provider-earn-service' + '1.1.0' -> 'provider-earn-service/1.1.0'
14077
+ return `${resolveKitSdkName(providerName)}/${providerVersion}`;
14078
+ }
14079
+ }
14080
+ /**
14081
+ * Resolve the value for the {@link SDK_VERSION_HEADER} header.
14082
+ *
14083
+ * Reads the kit registered at runtime via {@link createRequestContext} and
14084
+ * falls back to this provider package's own version when no kit is registered.
14085
+ *
14086
+ * @returns The SDK version string, e.g. `earn-kit/1.1.0` or
14087
+ * `provider-earn-service/1.1.0`, or `unknown`.
14088
+ *
14089
+ * @internal
14090
+ */ function resolveSdkVersionHeader() {
14091
+ return formatSdkVersion(createRequestContext().kit, pkg.name, pkg.version);
14092
+ }
14093
+
13897
14094
  /**
13898
14095
  * Build an API polling config with optional authorization header,
13899
14096
  * and resolve the base URL (configurable for testing).
@@ -13904,9 +14101,16 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13904
14101
  * @internal
13905
14102
  */ function buildConfig(serviceConfig) {
13906
14103
  const baseUrl = serviceConfig?.baseUrl ?? EARN_SERVICE_BASE_URL;
14104
+ const sdkVersion = resolveSdkVersionHeader();
13907
14105
  if (serviceConfig?.kitKey === undefined) {
13908
14106
  return {
13909
- pollingConfig: DEFAULT_CONFIG,
14107
+ pollingConfig: {
14108
+ ...DEFAULT_CONFIG,
14109
+ headers: {
14110
+ ...DEFAULT_CONFIG.headers,
14111
+ [SDK_VERSION_HEADER]: sdkVersion
14112
+ }
14113
+ },
13910
14114
  baseUrl
13911
14115
  };
13912
14116
  }
@@ -13922,6 +14126,7 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13922
14126
  ...DEFAULT_CONFIG,
13923
14127
  headers: {
13924
14128
  ...DEFAULT_CONFIG.headers,
14129
+ [SDK_VERSION_HEADER]: sdkVersion,
13925
14130
  Authorization: `Bearer ${serviceConfig.kitKey}`
13926
14131
  }
13927
14132
  },
@@ -14222,7 +14427,8 @@ function toPositionInfo(data) {
14222
14427
  return {
14223
14428
  execId: response.data.execId,
14224
14429
  preparedBundle,
14225
- expiresAt: response.data.expiresAt
14430
+ expiresAt: response.data.expiresAt,
14431
+ review: response.data.review
14226
14432
  };
14227
14433
  } catch (error) {
14228
14434
  throw parseEarnApiError(error, {
@@ -14333,47 +14539,34 @@ const VALID_AFTER_CLOCK_SKEW_SECONDS = 60n;
14333
14539
  * @throws If any typed-data field does not match the expected bridge deposit intent.
14334
14540
  * @internal
14335
14541
  */ function assertBridgeDepositTypedDataMatchesIntent(intent) {
14336
- const { preparedBundle, sourceChain, sourceAddress, sourceTokenAddress, authorizationRecipient, amount, maxFee, tokenDecimals, nowSeconds = BigInt(Math.floor(Date.now() / 1000)) } = intent;
14542
+ const { preparedBundle, sourceChain, sourceAddress, sourceTokenAddress, expectedDomain, authorizationRecipient, amount, maxFee, tokenDecimals, amountRaw, nowSeconds = BigInt(Math.floor(Date.now() / 1000)) } = intent;
14337
14543
  const { domain, message } = preparedBundle.erc3009TypedData;
14338
- // USDC deployments differ on the EIP-712 domain name per chain, so the
14339
- // expected domain is looked up for the source chain being signed on.
14340
- const expectedDomain = EARN_BRIDGE_ERC3009_DOMAINS[sourceChain.chain];
14341
- if (expectedDomain === undefined) {
14342
- throw createValidationFailedError('sourceChain.chain', sourceChain.chain, 'no expected ERC-3009 domain is configured for the source chain');
14343
- }
14344
- if (domain.name !== expectedDomain.name) {
14345
- throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.name', domain.name, `domain name must be ${expectedDomain.name}`);
14346
- }
14347
- if (domain.version !== expectedDomain.version) {
14348
- throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.version', domain.version, `domain version must be ${expectedDomain.version}`);
14349
- }
14350
14544
  assertSameAddress('preparedBundle.erc3009TypedData.domain.verifyingContract', domain.verifyingContract, sourceTokenAddress, 'verifyingContract must match the source token contract');
14351
14545
  assertSameBigInt('preparedBundle.erc3009TypedData.domain.chainId', domain.chainId, BigInt(sourceChain.chainId), 'chainId must match the source chain');
14546
+ assertSameString('preparedBundle.erc3009TypedData.domain.name', domain.name, expectedDomain.name, 'domain name must match the source token');
14547
+ assertSameString('preparedBundle.erc3009TypedData.domain.version', domain.version, expectedDomain.version, 'domain version must match the source token');
14352
14548
  assertSameAddress('preparedBundle.erc3009TypedData.message.from', message.from, sourceAddress, 'from must match the source wallet');
14353
14549
  assertSameAddress('preparedBundle.erc3009TypedData.message.to', message.to, authorizationRecipient, 'to must match the configured authorization recipient');
14354
- // The prepare path folds the source fee into the signed authorization:
14355
- // message.value = principal + sum(feeQuote.items[].amount), all denominated
14356
- // in the source token. The fee items only add up to a meaningful total when
14357
- // they are priced in the same token as the principal, so first pin the fee
14358
- // token to the source token contract before summing.
14359
- assertSameAddress('preparedBundle.feeQuote.feeToken', preparedBundle.feeQuote.feeToken, sourceTokenAddress, 'feeQuote.feeToken must match the source token contract');
14360
- // Recompute the expected value from locally trusted inputs (the caller
14361
- // principal) plus the bundle's own fee items so the guard accepts fee-bearing
14362
- // deposits while still rejecting any other value tampering. The fee quote is
14363
- // service-supplied, so bound it to the caller-accepted quote-derived cap
14364
- // before including it in the signed value.
14365
- const principal = Amount.parse(amount, {
14550
+ // Recompute the expected ERC-3009 value from locally trusted inputs. Bridge
14551
+ // source fees are paid in the source ERC-20 and are part of the authorization.
14552
+ assertDecimalPlaces('amount', amount, tokenDecimals, `amount must have at most ${String(tokenDecimals)} decimal places for the cross-chain Earn deposit source token`);
14553
+ const parsedAmount = Amount.parse(amount, {
14366
14554
  decimals: tokenDecimals
14367
14555
  }).raw;
14556
+ const principal = amountRaw === undefined ? parsedAmount : parseReviewedAmount(amountRaw, parsedAmount);
14557
+ const feeQuote = preparedBundle.feeQuote;
14368
14558
  const feeTotal = sumBridgeFeeItems(preparedBundle.feeQuote);
14559
+ assertFeeTokenMatchesSourceToken(feeQuote.feeToken, sourceTokenAddress);
14560
+ const feeDecimals = tokenDecimals;
14561
+ assertDecimalPlaces('maxFee', maxFee, feeDecimals, `maxFee must have at most ${String(feeDecimals)} decimal places for the cross-chain Earn deposit fee token`);
14369
14562
  const maxFeeRaw = Amount.parse(maxFee, {
14370
- decimals: tokenDecimals
14563
+ decimals: feeDecimals
14371
14564
  }).raw;
14372
14565
  if (feeTotal > maxFeeRaw) {
14373
14566
  throw createValidationFailedError('preparedBundle.feeQuote.items', feeTotal.toString(), 'fee total must not exceed maxFee in base units');
14374
14567
  }
14375
14568
  const expectedValue = principal + feeTotal;
14376
- assertSameBigInt('preparedBundle.erc3009TypedData.message.value', message.value, expectedValue, 'value must match the requested amount plus the bundle fee in base units');
14569
+ assertSameBigInt('preparedBundle.erc3009TypedData.message.value', message.value, expectedValue, 'value must match the requested amount plus source-token fees in base units');
14377
14570
  const window = checkErc3009ValidityWindow({
14378
14571
  validAfter: BigInt(message.validAfter),
14379
14572
  validBefore: BigInt(message.validBefore),
@@ -14447,14 +14640,19 @@ const VALID_AFTER_CLOCK_SKEW_SECONDS = 60n;
14447
14640
  /**
14448
14641
  * Sum the source-collected fee items in a prepared bundle's fee quote.
14449
14642
  *
14450
- * Each item amount is a base-unit value in the shared `feeToken`; the total is
14451
- * what the prepare path adds to the principal when building `message.value`.
14643
+ * Each item amount is a base-unit value in the shared `feeToken`.
14452
14644
  *
14453
14645
  * @param feeQuote - Fee quote parsed from the prepared bundle.
14454
- * @returns Total fee in source token base units (0n when there are no items).
14646
+ * @returns Total fee in fee-token base units (0n when there are no items).
14455
14647
  */ function sumBridgeFeeItems(feeQuote) {
14456
14648
  return feeQuote.items.reduce((total, item)=>total + BigInt(item.amount), 0n);
14457
14649
  }
14650
+ function assertFeeTokenMatchesSourceToken(feeToken, sourceTokenAddress) {
14651
+ if (isSameAddress(feeToken, sourceTokenAddress)) {
14652
+ return;
14653
+ }
14654
+ throw createValidationFailedError('preparedBundle.feeQuote.feeToken', feeToken, 'fee token must match the cross-chain Earn deposit source token');
14655
+ }
14458
14656
  function assertNever(value) {
14459
14657
  throw new KitError({
14460
14658
  ...EarnError.INTERNAL_ERROR,
@@ -14462,8 +14660,23 @@ function assertNever(value) {
14462
14660
  message: `Unexpected validity window reason: ${String(value)}`
14463
14661
  });
14464
14662
  }
14663
+ function parseReviewedAmount(amountRaw, parsedAmount) {
14664
+ let reviewedAmount;
14665
+ try {
14666
+ reviewedAmount = BigInt(amountRaw);
14667
+ } catch {
14668
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'amount must be a non-negative integer string');
14669
+ }
14670
+ if (reviewedAmount < 0n) {
14671
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'amount must be a non-negative integer string');
14672
+ }
14673
+ if (reviewedAmount !== parsedAmount) {
14674
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'review amount must match the requested amount');
14675
+ }
14676
+ return reviewedAmount;
14677
+ }
14465
14678
  function assertSameAddress(field, actual, expected, reason) {
14466
- if (actual.toLowerCase() !== expected.toLowerCase()) {
14679
+ if (!isSameAddress(actual, expected)) {
14467
14680
  throw createValidationFailedError(field, actual, reason);
14468
14681
  }
14469
14682
  }
@@ -14472,6 +14685,11 @@ function assertSameBigInt(field, actual, expected, reason) {
14472
14685
  throw createValidationFailedError(field, actual, reason);
14473
14686
  }
14474
14687
  }
14688
+ function assertSameString(field, actual, expected, reason) {
14689
+ if (actual !== expected) {
14690
+ throw createValidationFailedError(field, actual, reason);
14691
+ }
14692
+ }
14475
14693
 
14476
14694
  /**
14477
14695
  * Build signed withdrawal instructions via the Earn Service API.
@@ -14641,9 +14859,15 @@ function toWithdrawalQuoteInfo(data) {
14641
14859
  symbol: data.asset,
14642
14860
  amount: Amount.fromJSON(data.maxWithdrawable)
14643
14861
  },
14644
- fees: data.fees.map((fee)=>({
14645
- symbol: fee.token,
14646
- amount: Amount.fromJSON(fee.amount)
14862
+ fees: data.fees.map(({ token, amount, type, status })=>({
14863
+ symbol: token,
14864
+ amount: Amount.fromJSON(amount),
14865
+ ...type !== undefined && {
14866
+ type
14867
+ },
14868
+ ...status !== undefined && {
14869
+ status
14870
+ }
14647
14871
  })),
14648
14872
  gasFees: [],
14649
14873
  // Wire format uses `warnings`, but the SDK surface uses
@@ -14829,6 +15053,9 @@ function toWithdrawalQuoteInfo(data) {
14829
15053
  'CONFIRMED',
14830
15054
  'COMPLETE'
14831
15055
  ]);
15056
+ // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
15057
+ // token contracts plus the explicit ERC-3009 domain allowlist below.
15058
+ const TOKEN_REGISTRY = createTokenRegistry();
14832
15059
  /**
14833
15060
  * Build the typed error raised when a cross-chain wait is cancelled via its
14834
15061
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -15148,24 +15375,20 @@ function finishElapsedWait(lastStatus, lastError) {
15148
15375
  }
15149
15376
  const { address, chain: apiChain, chainDefinition: chain } = await resolveAdapterContext(params.from);
15150
15377
  const adapterContractAddress = requireAdapterContract(chain);
15151
- const rawUsdcAddress = chain.usdcAddress;
15152
- if (rawUsdcAddress === null) {
15153
- throw createUnsupportedTokenError('USDC', chain.name);
15154
- }
15155
- const usdcAddress = assertHexAddress('chain.usdcAddress', rawUsdcAddress, `USDC address for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
15156
15378
  const { adapter } = params.from;
15379
+ const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15157
15380
  const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
15158
- vaultAddress: params.vaultAddress,
15381
+ vaultAddress,
15159
15382
  amount: params.amount,
15160
15383
  address,
15161
15384
  chain: apiChain,
15162
15385
  config
15163
15386
  }), ()=>undefined);
15164
15387
  validateExecutionDeadline(executionParams);
15165
- const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
15166
- const approvalToken = tokenInputs[0]?.token;
15388
+ const approvalToken = resolveEarnApprovalToken(executionParams);
15389
+ const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15167
15390
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15168
- if (!options.skipApprove && approvalToken !== undefined) {
15391
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
15169
15392
  await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
15170
15393
  adapter,
15171
15394
  chain,
@@ -15173,7 +15396,7 @@ function finishElapsedWait(lastStatus, lastError) {
15173
15396
  delegate: adapterContractAddress,
15174
15397
  address,
15175
15398
  requiredAllowance,
15176
- revertMessage: 'USDC approval reverted on-chain'
15399
+ revertMessage: 'Earn deposit token approval reverted on-chain'
15177
15400
  }), (txHash)=>txHash);
15178
15401
  }
15179
15402
  const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
@@ -15192,7 +15415,7 @@ function finishElapsedWait(lastStatus, lastError) {
15192
15415
  kind: 'same-chain',
15193
15416
  txHash,
15194
15417
  explorerUrl,
15195
- vaultAddress: params.vaultAddress,
15418
+ vaultAddress,
15196
15419
  amount: params.amount
15197
15420
  };
15198
15421
  } catch (error) {
@@ -15214,13 +15437,6 @@ function finishElapsedWait(lastStatus, lastError) {
15214
15437
  const sourceAddress = assertHexAddress('from.address', await resolveCrossChainSourceAddress(params.from, sourceChain), `Source address for chain ${sourceChain.name} must be a 0x-prefixed 20-byte hex address.`);
15215
15438
  const destinationAddress = assertHexAddress('to.recipientAddress', params.to.recipientAddress, `Destination address for chain ${destinationChain.name} must be a 0x-prefixed 20-byte hex address.`);
15216
15439
  const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15217
- // The signed ERC-3009 value is denominated in USDC's 6 decimals. More
15218
- // precise inputs would be silently truncated by Amount.parse, so the
15219
- // result would claim a larger deposit than was actually authorized.
15220
- const amountDecimals = params.amount.split('.')[1]?.length ?? 0;
15221
- if (amountDecimals > TOKEN_DECIMALS_6) {
15222
- throw createValidationFailedError('amount', params.amount, `amount must have at most ${String(TOKEN_DECIMALS_6)} decimal places for cross-chain Earn deposits`);
15223
- }
15224
15440
  const execId = params.idempotencyKey;
15225
15441
  const prepared = await this.runPhase(ctx, 'crossChainDeposit', 'prepare', async ()=>fetchBridgeDepositPrepare({
15226
15442
  execId,
@@ -15240,17 +15456,19 @@ function finishElapsedWait(lastStatus, lastError) {
15240
15456
  // the error trace, instead of ending the event stream unannounced
15241
15457
  // after the prepare success event.
15242
15458
  assertPreparedBundleMatchesTypedDataNonce(prepared.preparedBundle);
15459
+ const sourceToken = resolvePreparedBridgeSourceToken(prepared.preparedBundle, route.sourceChain);
15460
+ validatePreparedReviewAmountDecimals(prepared.review.amount.decimals, sourceToken.decimals);
15243
15461
  assertBridgeDepositTypedDataMatchesIntent({
15244
15462
  preparedBundle: prepared.preparedBundle,
15245
15463
  sourceChain: route.sourceChain,
15246
15464
  sourceAddress,
15247
- sourceTokenAddress: route.sourceTokenAddress,
15465
+ sourceTokenAddress: sourceToken.address,
15466
+ expectedDomain: sourceToken.erc3009Domain,
15248
15467
  authorizationRecipient: route.authorizationRecipient,
15249
15468
  amount: params.amount,
15250
15469
  maxFee: params.maxFee,
15251
- // Cross-chain Earn is USDC-only today. USDC uses 6 decimals on all
15252
- // configured source chains.
15253
- tokenDecimals: TOKEN_DECIMALS_6
15470
+ tokenDecimals: sourceToken.decimals,
15471
+ amountRaw: prepared.review.amount.raw
15254
15472
  });
15255
15473
  return signBridgeDepositAuthorization({
15256
15474
  adapter: bridgeSigningAdapter,
@@ -15473,21 +15691,17 @@ function finishElapsedWait(lastStatus, lastError) {
15473
15691
  const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15474
15692
  try {
15475
15693
  const adapterContractAddress = requireAdapterContract(chain);
15476
- const rawUsdcAddress = chain.usdcAddress;
15477
- if (rawUsdcAddress === null) {
15478
- throw createUnsupportedTokenError('USDC', chain.name);
15479
- }
15480
- const usdcAddress = assertHexAddress('chain.usdcAddress', rawUsdcAddress, `USDC address for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
15694
+ const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15481
15695
  const { executionParams, signature } = await fetchDeposit({
15482
- vaultAddress,
15696
+ vaultAddress: normalizedVaultAddress,
15483
15697
  amount,
15484
15698
  address,
15485
15699
  chain: apiChain,
15486
15700
  config
15487
15701
  });
15488
15702
  validateExecutionDeadline(executionParams);
15489
- const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
15490
- const approvalToken = tokenInputs[0]?.token;
15703
+ const approvalToken = resolveEarnApprovalToken(executionParams);
15704
+ const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15491
15705
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15492
15706
  return await estimateEarnQuoteGasFees({
15493
15707
  adapter,
@@ -15500,7 +15714,7 @@ function finishElapsedWait(lastStatus, lastError) {
15500
15714
  tokenInputs,
15501
15715
  signature
15502
15716
  },
15503
- approval: approvalToken !== undefined ? {
15717
+ approval: approvalToken !== undefined && requiredAllowance > 0n ? {
15504
15718
  token: approvalToken,
15505
15719
  delegate: adapterContractAddress,
15506
15720
  requiredAllowance
@@ -15744,11 +15958,6 @@ function validateCrossChainDepositRoute(sourceChain, destinationChain) {
15744
15958
  if (destinationApiChain === undefined) {
15745
15959
  throw createUnsupportedCrossChainDepositError(destinationChain, 'destination chain is not enabled for cross-chain Earn deposits');
15746
15960
  }
15747
- const sourceTokenAddressRaw = sourceChain.usdcAddress;
15748
- if (sourceTokenAddressRaw === null) {
15749
- throw createUnsupportedTokenError('USDC', sourceChain.name);
15750
- }
15751
- const sourceTokenAddress = assertHexAddress('sourceChain.usdcAddress', sourceTokenAddressRaw, `USDC address for chain ${sourceChain.name} must be a 0x-prefixed 20-byte hex address.`);
15752
15961
  if (sourceChain.cctp == null) {
15753
15962
  throw createBridgeRouteNotConfiguredError(sourceChain, 'source chain does not support CCTP');
15754
15963
  }
@@ -15772,10 +15981,50 @@ function validateCrossChainDepositRoute(sourceChain, destinationChain) {
15772
15981
  destinationApiChain,
15773
15982
  destinationDomain: destinationChain.cctp.domain,
15774
15983
  sourceChain,
15775
- sourceTokenAddress,
15776
15984
  authorizationRecipient
15777
15985
  };
15778
15986
  }
15987
+ function resolvePreparedBridgeSourceToken(preparedBundle, sourceChain) {
15988
+ const sourceTokenAddress = assertHexAddress('preparedBundle.erc3009TypedData.domain.verifyingContract', preparedBundle.erc3009TypedData.domain.verifyingContract, `Source token for chain ${sourceChain.name} must be a 0x-prefixed 20-byte hex address.`);
15989
+ let sourceToken;
15990
+ try {
15991
+ sourceToken = TOKEN_REGISTRY.resolveByAddress(sourceTokenAddress, sourceChain.chain);
15992
+ } catch {
15993
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token ${sourceTokenAddress} is not configured on source chain ${sourceChain.name} for cross-chain Earn deposits`);
15994
+ }
15995
+ const registryAddress = assertHexAddress('sourceToken.locator', sourceToken.locator, `Source token ${sourceToken.symbol ?? sourceTokenAddress} for chain ${sourceChain.name} must be a 0x-prefixed 20-byte hex address.`);
15996
+ if (!isSameAddress(sourceTokenAddress, registryAddress)) {
15997
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token must match the configured ${sourceToken.symbol ?? 'token'} address on source chain ${sourceChain.name}`);
15998
+ }
15999
+ const { symbol } = sourceToken;
16000
+ if (symbol === undefined) {
16001
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token ${sourceTokenAddress} is not a registered Earn bridge token on source chain ${sourceChain.name}`);
16002
+ }
16003
+ return {
16004
+ address: sourceTokenAddress,
16005
+ erc3009Domain: resolveBridgeErc3009Domain(symbol, sourceChain),
16006
+ decimals: sourceToken.decimals
16007
+ };
16008
+ }
16009
+ function validatePreparedReviewAmountDecimals(reviewedDecimals, tokenDecimals) {
16010
+ if (reviewedDecimals !== tokenDecimals) {
16011
+ throw createValidationFailedError('prepared.review.amount.decimals', reviewedDecimals, `review amount decimals must match the cross-chain Earn deposit source token decimals ${String(tokenDecimals)}`);
16012
+ }
16013
+ }
16014
+ function resolveBridgeErc3009Domain(tokenSymbol, sourceChain) {
16015
+ if (!isEarnBridgeErc3009TokenSymbol(tokenSymbol)) {
16016
+ throw createValidationFailedError('sourceToken.symbol', tokenSymbol, `ERC-3009 domain is not configured for ${tokenSymbol} on source chain ${sourceChain.name}`);
16017
+ }
16018
+ const domainsByChain = EARN_BRIDGE_ERC3009_DOMAINS[tokenSymbol];
16019
+ const domain = domainsByChain[sourceChain.chain];
16020
+ if (domain === undefined) {
16021
+ throw createValidationFailedError('sourceToken.symbol', tokenSymbol, `ERC-3009 domain is not configured for ${tokenSymbol} on source chain ${sourceChain.name}`);
16022
+ }
16023
+ return domain;
16024
+ }
16025
+ function isEarnBridgeErc3009TokenSymbol(tokenSymbol) {
16026
+ return EARN_BRIDGE_ERC3009_TOKEN_SYMBOLS.includes(tokenSymbol);
16027
+ }
15779
16028
  function createUnsupportedCrossChainDepositError(chain, reason) {
15780
16029
  return new KitError({
15781
16030
  ...EarnError.UNSUPPORTED_CHAIN,
@@ -15848,7 +16097,6 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
15848
16097
  // Untyped callers can pass anything here; without these guards a null
15849
16098
  // config crashes with a raw TypeError on `config.providers` instead of
15850
16099
  // the structured validation error every other public surface throws.
15851
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- JS consumers may pass null or a non-object config
15852
16100
  if (typeof config !== 'object' || config === null || Array.isArray(config)) {
15853
16101
  throw createValidationFailedError('config', config, 'config must be a plain object when provided');
15854
16102
  }
@@ -16215,10 +16463,9 @@ const sourceAdapterContextSchema = zod.z.object({
16215
16463
  * Schema for validating human-readable decimal amount strings.
16216
16464
  *
16217
16465
  * Accept positive decimal strings like '100', '100.50', '0.001'. Reject
16218
- * zero, negative, non-numeric, and non-canonical strings. EarnKit
16219
- * same-chain amounts are USDC/EURC (6 decimals), so precision is capped at
16220
- * 6 to match the Earn Service and fail fast locally instead of leaking
16221
- * 7+ decimal-place inputs to the server.
16466
+ * zero, negative, non-numeric, and non-canonical strings. Same-chain
16467
+ * EarnKit vaults can use different asset precisions, so the service enforces
16468
+ * the vault-specific decimal cap after resolving the vault asset.
16222
16469
  *
16223
16470
  * @internal
16224
16471
  */ const amountSchema = zod.z.string({
@@ -16227,32 +16474,15 @@ const sourceAdapterContextSchema = zod.z.object({
16227
16474
  allowZero: false,
16228
16475
  regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16229
16476
  attributeName: 'amount',
16230
- maxDecimals: 6
16231
- })(zod.z.string())).refine((value)=>CANONICAL_AMOUNT_REGEX.test(value), AMOUNT_FORMAT_ERROR_MESSAGE);
16232
- /**
16233
- * Schema for cross-chain deposit amount strings.
16234
- *
16235
- * Cross-chain Earn deposits are USDC-only, and the signed ERC-3009 value is
16236
- * denominated in USDC's 6 decimals. Reject more precise inputs up front so
16237
- * the signed value always equals the requested amount instead of a silent
16238
- * truncation. Non-canonical forms (leading dot, leading zeros) are rejected
16239
- * too, matching the Earn Service.
16240
- *
16241
- * @internal
16242
- */ const crossChainAmountSchema = zod.z.string({
16243
- required_error: 'amount is required'
16244
- }).min(1, 'amount is required').pipe(createDecimalStringValidator({
16245
- allowZero: false,
16246
- regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16247
- attributeName: 'amount',
16248
- maxDecimals: 6
16477
+ maxDecimals: 18
16249
16478
  })(zod.z.string())).refine((value)=>CANONICAL_AMOUNT_REGEX.test(value), AMOUNT_FORMAT_ERROR_MESSAGE);
16250
16479
  /**
16251
16480
  * Schema for cross-chain source fee caps.
16252
16481
  *
16253
- * Cross-chain source fees are denominated in USDC base units. Zero is valid
16254
- * when the quote contains no source-collected fees; otherwise the cap should be
16255
- * the caller-accepted total from a recent quote.
16482
+ * Cross-chain source fees are denominated in the quote's fee token. Zero is
16483
+ * valid when the quote contains no source-collected fees; otherwise the cap
16484
+ * should be the caller-accepted total from a recent quote. The provider
16485
+ * enforces the prepared bundle's fee-token precision before signing.
16256
16486
  *
16257
16487
  * @internal
16258
16488
  */ const crossChainMaxFeeSchema = zod.z.string({
@@ -16261,7 +16491,7 @@ const sourceAdapterContextSchema = zod.z.object({
16261
16491
  allowZero: true,
16262
16492
  regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16263
16493
  attributeName: 'maxFee',
16264
- maxDecimals: 6
16494
+ maxDecimals: 18
16265
16495
  })(zod.z.string())).refine((value)=>CANONICAL_AMOUNT_REGEX.test(value), AMOUNT_FORMAT_ERROR_MESSAGE);
16266
16496
  /**
16267
16497
  * Build a schema for an optional non-negative decimal filter string.
@@ -16422,7 +16652,7 @@ const crossChainDepositDestinationSchema = zod.z.object({
16422
16652
  from: sourceAdapterContextSchema,
16423
16653
  to: crossChainDepositDestinationSchema,
16424
16654
  vaultAddress: vaultAddressSchema,
16425
- amount: crossChainAmountSchema,
16655
+ amount: amountSchema,
16426
16656
  maxFee: crossChainMaxFeeSchema,
16427
16657
  transferSpeed: zod.z.enum([
16428
16658
  'FAST',
@@ -17846,7 +18076,7 @@ function formatRetryResult(operation, result) {
17846
18076
  }
17847
18077
 
17848
18078
  // Auto-register this kit for user agent tracking
17849
- registerKit(`${pkg.name}/${pkg.version}`);
18079
+ registerKit(`${pkg$1.name}/${pkg$1.version}`);
17850
18080
 
17851
18081
  /**
17852
18082
  * Create an EarnKit instance for AppKit earn operations.