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