@circle-fin/app-kit 1.8.0 → 1.8.1

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
  *
@@ -1898,7 +1868,7 @@ function getOptionalString(value) {
1898
1868
  * - vault-not-found, unsupported-chain, unsupported-vault,
1899
1869
  * signature-rejected, invalid-id, invalid-batch-size, position-not-registered,
1900
1870
  * withdrawal-max-exceeded, insufficient-balance, bridge prepare idempotency
1901
- * conflicts
1871
+ * conflicts, invalid vault-asset amount precision
1902
1872
  *
1903
1873
  * SERVICE errors (RETRYABLE) — try again later:
1904
1874
  * - signing-failed, provider-error, rewards-fetch-failed,
@@ -2082,6 +2052,13 @@ function getOptionalString(value) {
2082
2052
  recoverability: 'RETRYABLE'
2083
2053
  }
2084
2054
  ],
2055
+ [
2056
+ 380415,
2057
+ {
2058
+ errorDef: EarnError.INVALID_INPUT,
2059
+ recoverability: 'FATAL'
2060
+ }
2061
+ ],
2085
2062
  // Bridge (380_5XX)
2086
2063
  [
2087
2064
  380500,
@@ -2117,6 +2094,13 @@ function getOptionalString(value) {
2117
2094
  errorDef: EarnError.PROVIDER_ERROR,
2118
2095
  recoverability: 'RETRYABLE'
2119
2096
  }
2097
+ ],
2098
+ [
2099
+ 380505,
2100
+ {
2101
+ errorDef: EarnError.PROVIDER_ERROR,
2102
+ recoverability: 'FATAL'
2103
+ }
2120
2104
  ]
2121
2105
  ]);
2122
2106
  /**
@@ -2576,23 +2560,6 @@ var EarnChain;
2576
2560
  }
2577
2561
  });
2578
2562
 
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
2563
  /**
2597
2564
  * @packageDocumentation
2598
2565
  * @module SwapTokenRegistry
@@ -7269,13 +7236,20 @@ const swapTokenEnumSchema = zod.z.enum([
7269
7236
  symbol: 'EURC',
7270
7237
  decimals: 6,
7271
7238
  locators: {
7239
+ // =========================================================================
7240
+ // Mainnets
7241
+ // =========================================================================
7272
7242
  [Blockchain.Avalanche]: '0xc891EB4cbdEFf6e073e859e987815Ed1505c2ACD',
7273
7243
  [Blockchain.Base]: '0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42',
7274
7244
  [Blockchain.Ethereum]: '0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c',
7275
7245
  [Blockchain.Solana]: 'HzwqbKZw8HxMN6bF2yFZNrht3c2iXXzpKcFu7uBEDKtr',
7276
7246
  [Blockchain.World_Chain]: '0x1C60ba0A0eD1019e8Eb035E6daF4155A5cE2380B',
7247
+ // =========================================================================
7277
7248
  // Testnets
7278
- [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a'
7249
+ // =========================================================================
7250
+ [Blockchain.Arc_Testnet]: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
7251
+ [Blockchain.Base_Sepolia]: '0x808456652fdb597867f38412077A9182bf77359F',
7252
+ [Blockchain.Ethereum_Sepolia]: '0x08210F9170F89Ab7658F0B5E3fF39b0E03C594D4'
7279
7253
  }
7280
7254
  };
7281
7255
 
@@ -7485,8 +7459,7 @@ const swapTokenEnumSchema = zod.z.enum([
7485
7459
  * cirBTC (Circle Bitcoin) token definition with addresses and metadata.
7486
7460
  *
7487
7461
  * @remarks
7488
- * Built-in cirBTC definition for the TokenRegistry. Currently deployed
7489
- * on Arc Testnet.
7462
+ * Built-in cirBTC definition for the TokenRegistry.
7490
7463
  *
7491
7464
  * @example
7492
7465
  * ```typescript
@@ -7502,7 +7475,15 @@ const swapTokenEnumSchema = zod.z.enum([
7502
7475
  symbol: 'cirBTC',
7503
7476
  decimals: 8,
7504
7477
  locators: {
7505
- [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF'
7478
+ // =========================================================================
7479
+ // Mainnets
7480
+ // =========================================================================
7481
+ [Blockchain.Ethereum]: '0x72DFB2E44f59C5AD2bAFE84314E5b99a7cd5075E',
7482
+ // =========================================================================
7483
+ // Testnets
7484
+ // =========================================================================
7485
+ [Blockchain.Arc_Testnet]: '0xf0C4a4CE82A5746AbAAd9425360Ab04fbBA432BF',
7486
+ [Blockchain.Ethereum_Sepolia]: '0x3a3fe695F684Bf9b9e43CF43C2b895Ea5e392bB3'
7506
7487
  }
7507
7488
  };
7508
7489
 
@@ -7942,7 +7923,7 @@ const swapTokenEnumSchema = zod.z.enum([
7942
7923
  }
7943
7924
 
7944
7925
  var name$2 = "@circle-fin/bridge-kit";
7945
- var version$2 = "1.11.0";
7926
+ var version$2 = "1.11.1";
7946
7927
  var pkg$2 = {
7947
7928
  name: name$2,
7948
7929
  version: version$2};
@@ -8799,7 +8780,7 @@ var TransferSpeed;
8799
8780
  registerKit(`${pkg$2.name}/${pkg$2.version}`);
8800
8781
 
8801
8782
  var name$1 = "@circle-fin/swap-kit";
8802
- var version$1 = "1.3.0";
8783
+ var version$1 = "1.3.1";
8803
8784
  var pkg$1 = {
8804
8785
  name: name$1,
8805
8786
  version: version$1};
@@ -12093,11 +12074,14 @@ new Set(Object.values(Blockchain));
12093
12074
  registerKit(`${pkg$1.name}/${pkg$1.version}`);
12094
12075
 
12095
12076
  var name = "@circle-fin/earn-kit";
12096
- var version = "1.2.0";
12077
+ var version = "1.2.1";
12097
12078
  var pkg = {
12098
12079
  name: name,
12099
12080
  version: version};
12100
12081
 
12082
+ const EARN_BRIDGE_ERC3009_TOKEN_SYMBOLS = [
12083
+ 'USDC'
12084
+ ];
12101
12085
  /**
12102
12086
  * Default base URL for the Earn Service API.
12103
12087
  *
@@ -12142,20 +12126,7 @@ var pkg = {
12142
12126
  */ const EARN_BRIDGE_DESTINATION_CHAIN_TO_API = {
12143
12127
  [Blockchain.Arc_Testnet]: CHAIN_TO_API[Blockchain.Arc_Testnet]
12144
12128
  };
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 = {
12129
+ const EARN_BRIDGE_USDC_ERC3009_DOMAINS = {
12159
12130
  [Blockchain.Arbitrum_Sepolia]: {
12160
12131
  name: 'USD Coin',
12161
12132
  version: '2'
@@ -12169,6 +12140,21 @@ var pkg = {
12169
12140
  version: '2'
12170
12141
  }
12171
12142
  };
12143
+ /**
12144
+ * Expected EIP-712 domain of the ERC-3009 authorization token per
12145
+ * cross-chain-supported source token and source chain.
12146
+ *
12147
+ * The bridge service prepares an ERC-3009 payload for the deposited token. We
12148
+ * keep this table as an explicit SDK allowlist because token deployments can
12149
+ * differ by source chain and by domain name/version. These values mirror the
12150
+ * bridge service's per-chain domain table and the on-chain DOMAIN_SEPARATOR
12151
+ * inputs. Asserted before signing so a tampered prepare response cannot point
12152
+ * the signature at another token or unexpected token domain.
12153
+ *
12154
+ * @internal
12155
+ */ const EARN_BRIDGE_ERC3009_DOMAINS = {
12156
+ USDC: EARN_BRIDGE_USDC_ERC3009_DOMAINS
12157
+ };
12172
12158
  /**
12173
12159
  * Maximum tolerated remaining lifetime of a prepared ERC-3009 authorization.
12174
12160
  *
@@ -12628,6 +12614,16 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12628
12614
  return approvalTxHash;
12629
12615
  }
12630
12616
 
12617
+ /** @internal */ function isSameAddress(actual, expected) {
12618
+ return actual.toLowerCase() === expected.toLowerCase();
12619
+ }
12620
+ /** @internal */ function assertDecimalPlaces(field, value, tokenDecimals, reason) {
12621
+ const decimals = value.split('.')[1]?.length ?? 0;
12622
+ if (decimals > tokenDecimals) {
12623
+ throw createValidationFailedError(field, value, reason);
12624
+ }
12625
+ }
12626
+
12631
12627
  /**
12632
12628
  * Build the `tokenInputs` array forwarded to the adapter contract's
12633
12629
  * `execute(executeParams, tokenInputs, signature)` call.
@@ -12651,15 +12647,17 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12651
12647
  *
12652
12648
  * @example
12653
12649
  * ```typescript
12654
- * const approvedToken = assertHexAddress(
12655
- * 'chain.usdcAddress',
12656
- * chain.usdcAddress,
12657
- * )
12658
- *
12659
- * const tokenInputs = buildEarnTokenInputs(
12650
+ * const approvalToken = resolveEarnApprovalToken(
12660
12651
  * executionPayload.executionParams,
12661
- * approvedToken,
12662
12652
  * )
12653
+ *
12654
+ * const tokenInputs =
12655
+ * approvalToken === undefined
12656
+ * ? []
12657
+ * : buildEarnTokenInputs(
12658
+ * executionPayload.executionParams,
12659
+ * approvalToken,
12660
+ * )
12663
12661
  * ```
12664
12662
  *
12665
12663
  * @internal
@@ -12671,7 +12669,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12671
12669
  return;
12672
12670
  }
12673
12671
  const { tokenIn } = instruction;
12674
- if (tokenIn.toLowerCase() !== approvedToken.toLowerCase()) {
12672
+ if (!isSameAddress(tokenIn, approvedToken)) {
12675
12673
  throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
12676
12674
  }
12677
12675
  tokenInputs.push({
@@ -12683,6 +12681,33 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
12683
12681
  });
12684
12682
  return tokenInputs;
12685
12683
  }
12684
+ /**
12685
+ * Resolve the single token that needs adapter allowance from signed Earn
12686
+ * instructions.
12687
+ *
12688
+ * @param executionParams - Service-signed `ExecutionParams` forwarded to the adapter.
12689
+ * @returns The token requested by positive approval instructions, or `undefined`.
12690
+ * @throws {@link KitError} If positive approval instructions reference multiple tokens.
12691
+ *
12692
+ * @internal
12693
+ */ function resolveEarnApprovalToken(executionParams) {
12694
+ let approvedToken;
12695
+ executionParams.instructions.forEach((instruction, index)=>{
12696
+ const amount = BigInt(instruction.amountToApprove);
12697
+ if (amount <= 0n) {
12698
+ return;
12699
+ }
12700
+ const { tokenIn } = instruction;
12701
+ if (approvedToken === undefined) {
12702
+ approvedToken = tokenIn;
12703
+ return;
12704
+ }
12705
+ if (!isSameAddress(tokenIn, approvedToken)) {
12706
+ throw createValidationFailedError(`executionParams.instructions[${index.toString()}].tokenIn`, tokenIn, 'tokenIn must match the token approved for adapter spending');
12707
+ }
12708
+ });
12709
+ return approvedToken;
12710
+ }
12686
12711
 
12687
12712
  /**
12688
12713
  * Prepare an earn adapter action, execute it, wait for confirmation, and
@@ -13174,6 +13199,7 @@ const hexSignatureSchema = evmSignatureSchema;
13174
13199
  const hexAddressSchema = evmAddressSchema;
13175
13200
  // '0x' prefix + 32 bytes * 2 hex chars.
13176
13201
  const BYTES32_HEX_LENGTH = 66;
13202
+ const bridgeFeeTokenSchema = hexAddressSchema;
13177
13203
  /**
13178
13204
  * Zod schema for a non-negative uint256-like value.
13179
13205
  *
@@ -13460,14 +13486,12 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13460
13486
  *
13461
13487
  * The bridge prepare path returns one item per collected source fee (e.g.
13462
13488
  * `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.
13489
+ * forward fee), all denominated in the shared `feeToken`. Each item's `amount`
13490
+ * is a base-unit decimal string in `feeToken` units.
13467
13491
  *
13468
13492
  * @internal
13469
13493
  */ const bridgeFeeQuoteSchema = zod.z.object({
13470
- feeToken: hexAddressSchema,
13494
+ feeToken: bridgeFeeTokenSchema,
13471
13495
  items: zod.z.array(zod.z.object({
13472
13496
  type: zod.z.string(),
13473
13497
  amount: uint256LikeSchema
@@ -13483,6 +13507,12 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13483
13507
  erc3009TypedData: erc3009TypedDataSchema,
13484
13508
  feeQuote: bridgeFeeQuoteSchema
13485
13509
  }).passthrough();
13510
+ const bridgeDepositPrepareReviewSchema = zod.z.object({
13511
+ sourceChain: zod.z.string(),
13512
+ destinationChain: zod.z.string(),
13513
+ amount: amountJsonSchema,
13514
+ vaultAddress: hexAddressSchema
13515
+ }).passthrough();
13486
13516
  /**
13487
13517
  * Zod schema for the bridge deposit prepare payload.
13488
13518
  *
@@ -13494,7 +13524,7 @@ const eip712DomainSchema = eip712DomainSchema$1.extend({
13494
13524
  execId: bridgeDepositExecIdSchema,
13495
13525
  erc3009TypedData: bridgeDepositPreparedBundleSchema,
13496
13526
  expiresAt: zod.z.string().datetime(),
13497
- review: zod.z.unknown().optional()
13527
+ review: bridgeDepositPrepareReviewSchema
13498
13528
  });
13499
13529
  /**
13500
13530
  * Zod schema for the `POST /v1/earnKit/bridge/deposit/prepare` API response.
@@ -14222,7 +14252,8 @@ function toPositionInfo(data) {
14222
14252
  return {
14223
14253
  execId: response.data.execId,
14224
14254
  preparedBundle,
14225
- expiresAt: response.data.expiresAt
14255
+ expiresAt: response.data.expiresAt,
14256
+ review: response.data.review
14226
14257
  };
14227
14258
  } catch (error) {
14228
14259
  throw parseEarnApiError(error, {
@@ -14333,47 +14364,34 @@ const VALID_AFTER_CLOCK_SKEW_SECONDS = 60n;
14333
14364
  * @throws If any typed-data field does not match the expected bridge deposit intent.
14334
14365
  * @internal
14335
14366
  */ function assertBridgeDepositTypedDataMatchesIntent(intent) {
14336
- const { preparedBundle, sourceChain, sourceAddress, sourceTokenAddress, authorizationRecipient, amount, maxFee, tokenDecimals, nowSeconds = BigInt(Math.floor(Date.now() / 1000)) } = intent;
14367
+ const { preparedBundle, sourceChain, sourceAddress, sourceTokenAddress, expectedDomain, authorizationRecipient, amount, maxFee, tokenDecimals, amountRaw, nowSeconds = BigInt(Math.floor(Date.now() / 1000)) } = intent;
14337
14368
  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
14369
  assertSameAddress('preparedBundle.erc3009TypedData.domain.verifyingContract', domain.verifyingContract, sourceTokenAddress, 'verifyingContract must match the source token contract');
14351
14370
  assertSameBigInt('preparedBundle.erc3009TypedData.domain.chainId', domain.chainId, BigInt(sourceChain.chainId), 'chainId must match the source chain');
14371
+ assertSameString('preparedBundle.erc3009TypedData.domain.name', domain.name, expectedDomain.name, 'domain name must match the source token');
14372
+ assertSameString('preparedBundle.erc3009TypedData.domain.version', domain.version, expectedDomain.version, 'domain version must match the source token');
14352
14373
  assertSameAddress('preparedBundle.erc3009TypedData.message.from', message.from, sourceAddress, 'from must match the source wallet');
14353
14374
  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, {
14375
+ // Recompute the expected ERC-3009 value from locally trusted inputs. Bridge
14376
+ // source fees are paid in the source ERC-20 and are part of the authorization.
14377
+ assertDecimalPlaces('amount', amount, tokenDecimals, `amount must have at most ${String(tokenDecimals)} decimal places for the cross-chain Earn deposit source token`);
14378
+ const parsedAmount = Amount.parse(amount, {
14366
14379
  decimals: tokenDecimals
14367
14380
  }).raw;
14381
+ const principal = amountRaw === undefined ? parsedAmount : parseReviewedAmount(amountRaw, parsedAmount);
14382
+ const feeQuote = preparedBundle.feeQuote;
14368
14383
  const feeTotal = sumBridgeFeeItems(preparedBundle.feeQuote);
14384
+ assertFeeTokenMatchesSourceToken(feeQuote.feeToken, sourceTokenAddress);
14385
+ const feeDecimals = tokenDecimals;
14386
+ assertDecimalPlaces('maxFee', maxFee, feeDecimals, `maxFee must have at most ${String(feeDecimals)} decimal places for the cross-chain Earn deposit fee token`);
14369
14387
  const maxFeeRaw = Amount.parse(maxFee, {
14370
- decimals: tokenDecimals
14388
+ decimals: feeDecimals
14371
14389
  }).raw;
14372
14390
  if (feeTotal > maxFeeRaw) {
14373
14391
  throw createValidationFailedError('preparedBundle.feeQuote.items', feeTotal.toString(), 'fee total must not exceed maxFee in base units');
14374
14392
  }
14375
14393
  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');
14394
+ assertSameBigInt('preparedBundle.erc3009TypedData.message.value', message.value, expectedValue, 'value must match the requested amount plus source-token fees in base units');
14377
14395
  const window = checkErc3009ValidityWindow({
14378
14396
  validAfter: BigInt(message.validAfter),
14379
14397
  validBefore: BigInt(message.validBefore),
@@ -14447,14 +14465,19 @@ const VALID_AFTER_CLOCK_SKEW_SECONDS = 60n;
14447
14465
  /**
14448
14466
  * Sum the source-collected fee items in a prepared bundle's fee quote.
14449
14467
  *
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`.
14468
+ * Each item amount is a base-unit value in the shared `feeToken`.
14452
14469
  *
14453
14470
  * @param feeQuote - Fee quote parsed from the prepared bundle.
14454
- * @returns Total fee in source token base units (0n when there are no items).
14471
+ * @returns Total fee in fee-token base units (0n when there are no items).
14455
14472
  */ function sumBridgeFeeItems(feeQuote) {
14456
14473
  return feeQuote.items.reduce((total, item)=>total + BigInt(item.amount), 0n);
14457
14474
  }
14475
+ function assertFeeTokenMatchesSourceToken(feeToken, sourceTokenAddress) {
14476
+ if (isSameAddress(feeToken, sourceTokenAddress)) {
14477
+ return;
14478
+ }
14479
+ throw createValidationFailedError('preparedBundle.feeQuote.feeToken', feeToken, 'fee token must match the cross-chain Earn deposit source token');
14480
+ }
14458
14481
  function assertNever(value) {
14459
14482
  throw new KitError({
14460
14483
  ...EarnError.INTERNAL_ERROR,
@@ -14462,8 +14485,23 @@ function assertNever(value) {
14462
14485
  message: `Unexpected validity window reason: ${String(value)}`
14463
14486
  });
14464
14487
  }
14488
+ function parseReviewedAmount(amountRaw, parsedAmount) {
14489
+ let reviewedAmount;
14490
+ try {
14491
+ reviewedAmount = BigInt(amountRaw);
14492
+ } catch {
14493
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'amount must be a non-negative integer string');
14494
+ }
14495
+ if (reviewedAmount < 0n) {
14496
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'amount must be a non-negative integer string');
14497
+ }
14498
+ if (reviewedAmount !== parsedAmount) {
14499
+ throw createValidationFailedError('prepared.review.amount.raw', amountRaw, 'review amount must match the requested amount');
14500
+ }
14501
+ return reviewedAmount;
14502
+ }
14465
14503
  function assertSameAddress(field, actual, expected, reason) {
14466
- if (actual.toLowerCase() !== expected.toLowerCase()) {
14504
+ if (!isSameAddress(actual, expected)) {
14467
14505
  throw createValidationFailedError(field, actual, reason);
14468
14506
  }
14469
14507
  }
@@ -14472,6 +14510,11 @@ function assertSameBigInt(field, actual, expected, reason) {
14472
14510
  throw createValidationFailedError(field, actual, reason);
14473
14511
  }
14474
14512
  }
14513
+ function assertSameString(field, actual, expected, reason) {
14514
+ if (actual !== expected) {
14515
+ throw createValidationFailedError(field, actual, reason);
14516
+ }
14517
+ }
14475
14518
 
14476
14519
  /**
14477
14520
  * Build signed withdrawal instructions via the Earn Service API.
@@ -14829,6 +14872,9 @@ function toWithdrawalQuoteInfo(data) {
14829
14872
  'CONFIRMED',
14830
14873
  'COMPLETE'
14831
14874
  ]);
14875
+ // Intentionally built-ins-only: Earn bridge support is limited to SDK-known
14876
+ // token contracts plus the explicit ERC-3009 domain allowlist below.
14877
+ const TOKEN_REGISTRY = createTokenRegistry();
14832
14878
  /**
14833
14879
  * Build the typed error raised when a cross-chain wait is cancelled via its
14834
14880
  * `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
@@ -15148,24 +15194,20 @@ function finishElapsedWait(lastStatus, lastError) {
15148
15194
  }
15149
15195
  const { address, chain: apiChain, chainDefinition: chain } = await resolveAdapterContext(params.from);
15150
15196
  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
15197
  const { adapter } = params.from;
15198
+ const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15157
15199
  const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
15158
- vaultAddress: params.vaultAddress,
15200
+ vaultAddress,
15159
15201
  amount: params.amount,
15160
15202
  address,
15161
15203
  chain: apiChain,
15162
15204
  config
15163
15205
  }), ()=>undefined);
15164
15206
  validateExecutionDeadline(executionParams);
15165
- const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
15166
- const approvalToken = tokenInputs[0]?.token;
15207
+ const approvalToken = resolveEarnApprovalToken(executionParams);
15208
+ const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15167
15209
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15168
- if (!options.skipApprove && approvalToken !== undefined) {
15210
+ if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
15169
15211
  await this.runPhase(ctx, 'approve', 'approve', async ()=>approveAllowanceIfNeeded({
15170
15212
  adapter,
15171
15213
  chain,
@@ -15173,7 +15215,7 @@ function finishElapsedWait(lastStatus, lastError) {
15173
15215
  delegate: adapterContractAddress,
15174
15216
  address,
15175
15217
  requiredAllowance,
15176
- revertMessage: 'USDC approval reverted on-chain'
15218
+ revertMessage: 'Earn deposit token approval reverted on-chain'
15177
15219
  }), (txHash)=>txHash);
15178
15220
  }
15179
15221
  const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>executeEarnAction({
@@ -15192,7 +15234,7 @@ function finishElapsedWait(lastStatus, lastError) {
15192
15234
  kind: 'same-chain',
15193
15235
  txHash,
15194
15236
  explorerUrl,
15195
- vaultAddress: params.vaultAddress,
15237
+ vaultAddress,
15196
15238
  amount: params.amount
15197
15239
  };
15198
15240
  } catch (error) {
@@ -15214,13 +15256,6 @@ function finishElapsedWait(lastStatus, lastError) {
15214
15256
  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
15257
  const destinationAddress = assertHexAddress('to.recipientAddress', params.to.recipientAddress, `Destination address for chain ${destinationChain.name} must be a 0x-prefixed 20-byte hex address.`);
15216
15258
  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
15259
  const execId = params.idempotencyKey;
15225
15260
  const prepared = await this.runPhase(ctx, 'crossChainDeposit', 'prepare', async ()=>fetchBridgeDepositPrepare({
15226
15261
  execId,
@@ -15240,17 +15275,19 @@ function finishElapsedWait(lastStatus, lastError) {
15240
15275
  // the error trace, instead of ending the event stream unannounced
15241
15276
  // after the prepare success event.
15242
15277
  assertPreparedBundleMatchesTypedDataNonce(prepared.preparedBundle);
15278
+ const sourceToken = resolvePreparedBridgeSourceToken(prepared.preparedBundle, route.sourceChain);
15279
+ validatePreparedReviewAmountDecimals(prepared.review.amount.decimals, sourceToken.decimals);
15243
15280
  assertBridgeDepositTypedDataMatchesIntent({
15244
15281
  preparedBundle: prepared.preparedBundle,
15245
15282
  sourceChain: route.sourceChain,
15246
15283
  sourceAddress,
15247
- sourceTokenAddress: route.sourceTokenAddress,
15284
+ sourceTokenAddress: sourceToken.address,
15285
+ expectedDomain: sourceToken.erc3009Domain,
15248
15286
  authorizationRecipient: route.authorizationRecipient,
15249
15287
  amount: params.amount,
15250
15288
  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
15289
+ tokenDecimals: sourceToken.decimals,
15290
+ amountRaw: prepared.review.amount.raw
15254
15291
  });
15255
15292
  return signBridgeDepositAuthorization({
15256
15293
  adapter: bridgeSigningAdapter,
@@ -15473,21 +15510,17 @@ function finishElapsedWait(lastStatus, lastError) {
15473
15510
  const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
15474
15511
  try {
15475
15512
  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.`);
15513
+ const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
15481
15514
  const { executionParams, signature } = await fetchDeposit({
15482
- vaultAddress,
15515
+ vaultAddress: normalizedVaultAddress,
15483
15516
  amount,
15484
15517
  address,
15485
15518
  chain: apiChain,
15486
15519
  config
15487
15520
  });
15488
15521
  validateExecutionDeadline(executionParams);
15489
- const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
15490
- const approvalToken = tokenInputs[0]?.token;
15522
+ const approvalToken = resolveEarnApprovalToken(executionParams);
15523
+ const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
15491
15524
  const requiredAllowance = sumTokenInputAmounts(tokenInputs);
15492
15525
  return await estimateEarnQuoteGasFees({
15493
15526
  adapter,
@@ -15500,7 +15533,7 @@ function finishElapsedWait(lastStatus, lastError) {
15500
15533
  tokenInputs,
15501
15534
  signature
15502
15535
  },
15503
- approval: approvalToken !== undefined ? {
15536
+ approval: approvalToken !== undefined && requiredAllowance > 0n ? {
15504
15537
  token: approvalToken,
15505
15538
  delegate: adapterContractAddress,
15506
15539
  requiredAllowance
@@ -15744,11 +15777,6 @@ function validateCrossChainDepositRoute(sourceChain, destinationChain) {
15744
15777
  if (destinationApiChain === undefined) {
15745
15778
  throw createUnsupportedCrossChainDepositError(destinationChain, 'destination chain is not enabled for cross-chain Earn deposits');
15746
15779
  }
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
15780
  if (sourceChain.cctp == null) {
15753
15781
  throw createBridgeRouteNotConfiguredError(sourceChain, 'source chain does not support CCTP');
15754
15782
  }
@@ -15772,10 +15800,50 @@ function validateCrossChainDepositRoute(sourceChain, destinationChain) {
15772
15800
  destinationApiChain,
15773
15801
  destinationDomain: destinationChain.cctp.domain,
15774
15802
  sourceChain,
15775
- sourceTokenAddress,
15776
15803
  authorizationRecipient
15777
15804
  };
15778
15805
  }
15806
+ function resolvePreparedBridgeSourceToken(preparedBundle, sourceChain) {
15807
+ 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.`);
15808
+ let sourceToken;
15809
+ try {
15810
+ sourceToken = TOKEN_REGISTRY.resolveByAddress(sourceTokenAddress, sourceChain.chain);
15811
+ } catch {
15812
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token ${sourceTokenAddress} is not configured on source chain ${sourceChain.name} for cross-chain Earn deposits`);
15813
+ }
15814
+ 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.`);
15815
+ if (!isSameAddress(sourceTokenAddress, registryAddress)) {
15816
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token must match the configured ${sourceToken.symbol ?? 'token'} address on source chain ${sourceChain.name}`);
15817
+ }
15818
+ const { symbol } = sourceToken;
15819
+ if (symbol === undefined) {
15820
+ throw createValidationFailedError('preparedBundle.erc3009TypedData.domain.verifyingContract', sourceTokenAddress, `source token ${sourceTokenAddress} is not a registered Earn bridge token on source chain ${sourceChain.name}`);
15821
+ }
15822
+ return {
15823
+ address: sourceTokenAddress,
15824
+ erc3009Domain: resolveBridgeErc3009Domain(symbol, sourceChain),
15825
+ decimals: sourceToken.decimals
15826
+ };
15827
+ }
15828
+ function validatePreparedReviewAmountDecimals(reviewedDecimals, tokenDecimals) {
15829
+ if (reviewedDecimals !== tokenDecimals) {
15830
+ throw createValidationFailedError('prepared.review.amount.decimals', reviewedDecimals, `review amount decimals must match the cross-chain Earn deposit source token decimals ${String(tokenDecimals)}`);
15831
+ }
15832
+ }
15833
+ function resolveBridgeErc3009Domain(tokenSymbol, sourceChain) {
15834
+ if (!isEarnBridgeErc3009TokenSymbol(tokenSymbol)) {
15835
+ throw createValidationFailedError('sourceToken.symbol', tokenSymbol, `ERC-3009 domain is not configured for ${tokenSymbol} on source chain ${sourceChain.name}`);
15836
+ }
15837
+ const domainsByChain = EARN_BRIDGE_ERC3009_DOMAINS[tokenSymbol];
15838
+ const domain = domainsByChain[sourceChain.chain];
15839
+ if (domain === undefined) {
15840
+ throw createValidationFailedError('sourceToken.symbol', tokenSymbol, `ERC-3009 domain is not configured for ${tokenSymbol} on source chain ${sourceChain.name}`);
15841
+ }
15842
+ return domain;
15843
+ }
15844
+ function isEarnBridgeErc3009TokenSymbol(tokenSymbol) {
15845
+ return EARN_BRIDGE_ERC3009_TOKEN_SYMBOLS.includes(tokenSymbol);
15846
+ }
15779
15847
  function createUnsupportedCrossChainDepositError(chain, reason) {
15780
15848
  return new KitError({
15781
15849
  ...EarnError.UNSUPPORTED_CHAIN,
@@ -15848,7 +15916,6 @@ function createUnsupportedCrossChainDepositError(chain, reason) {
15848
15916
  // Untyped callers can pass anything here; without these guards a null
15849
15917
  // config crashes with a raw TypeError on `config.providers` instead of
15850
15918
  // 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
15919
  if (typeof config !== 'object' || config === null || Array.isArray(config)) {
15853
15920
  throw createValidationFailedError('config', config, 'config must be a plain object when provided');
15854
15921
  }
@@ -16215,10 +16282,9 @@ const sourceAdapterContextSchema = zod.z.object({
16215
16282
  * Schema for validating human-readable decimal amount strings.
16216
16283
  *
16217
16284
  * 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.
16285
+ * zero, negative, non-numeric, and non-canonical strings. Same-chain
16286
+ * EarnKit vaults can use different asset precisions, so the service enforces
16287
+ * the vault-specific decimal cap after resolving the vault asset.
16222
16288
  *
16223
16289
  * @internal
16224
16290
  */ const amountSchema = zod.z.string({
@@ -16227,32 +16293,15 @@ const sourceAdapterContextSchema = zod.z.object({
16227
16293
  allowZero: false,
16228
16294
  regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16229
16295
  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
16296
+ maxDecimals: 18
16249
16297
  })(zod.z.string())).refine((value)=>CANONICAL_AMOUNT_REGEX.test(value), AMOUNT_FORMAT_ERROR_MESSAGE);
16250
16298
  /**
16251
16299
  * Schema for cross-chain source fee caps.
16252
16300
  *
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.
16301
+ * Cross-chain source fees are denominated in the quote's fee token. Zero is
16302
+ * valid when the quote contains no source-collected fees; otherwise the cap
16303
+ * should be the caller-accepted total from a recent quote. The provider
16304
+ * enforces the prepared bundle's fee-token precision before signing.
16256
16305
  *
16257
16306
  * @internal
16258
16307
  */ const crossChainMaxFeeSchema = zod.z.string({
@@ -16261,7 +16310,7 @@ const sourceAdapterContextSchema = zod.z.object({
16261
16310
  allowZero: true,
16262
16311
  regexMessage: AMOUNT_FORMAT_ERROR_MESSAGE,
16263
16312
  attributeName: 'maxFee',
16264
- maxDecimals: 6
16313
+ maxDecimals: 18
16265
16314
  })(zod.z.string())).refine((value)=>CANONICAL_AMOUNT_REGEX.test(value), AMOUNT_FORMAT_ERROR_MESSAGE);
16266
16315
  /**
16267
16316
  * Build a schema for an optional non-negative decimal filter string.
@@ -16422,7 +16471,7 @@ const crossChainDepositDestinationSchema = zod.z.object({
16422
16471
  from: sourceAdapterContextSchema,
16423
16472
  to: crossChainDepositDestinationSchema,
16424
16473
  vaultAddress: vaultAddressSchema,
16425
- amount: crossChainAmountSchema,
16474
+ amount: amountSchema,
16426
16475
  maxFee: crossChainMaxFeeSchema,
16427
16476
  transferSpeed: zod.z.enum([
16428
16477
  'FAST',