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