@circle-fin/app-kit 1.11.0 → 1.12.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.
@@ -2319,6 +2319,8 @@ class KitError extends Error {
2319
2319
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2320
2320
  Blockchain["XDC"] = "XDC";
2321
2321
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2322
+ Blockchain["X_Layer"] = "X_Layer";
2323
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2322
2324
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2323
2325
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2324
2326
  })(Blockchain || (Blockchain = {}));
@@ -2372,6 +2374,7 @@ var BridgeChain;
2372
2374
  BridgeChain["Unichain"] = "Unichain";
2373
2375
  BridgeChain["World_Chain"] = "World_Chain";
2374
2376
  BridgeChain["XDC"] = "XDC";
2377
+ BridgeChain["X_Layer"] = "X_Layer";
2375
2378
  // Testnet chains with CCTPv2 support
2376
2379
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2377
2380
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2397,6 +2400,7 @@ var BridgeChain;
2397
2400
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2398
2401
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2399
2402
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2403
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2400
2404
  })(BridgeChain || (BridgeChain = {}));
2401
2405
  var UnifiedBalanceChain;
2402
2406
  (function(UnifiedBalanceChain) {
@@ -4944,7 +4948,8 @@ var EarnChain;
4944
4948
  isTestnet: true,
4945
4949
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4946
4950
  rpcEndpoints: [
4947
- 'https://rpc-amoy.polygon.technology'
4951
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4952
+ 'https://polygon-amoy.drpc.org'
4948
4953
  ],
4949
4954
  eurcAddress: null,
4950
4955
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5809,6 +5814,104 @@ var EarnChain;
5809
5814
  }
5810
5815
  });
5811
5816
 
5817
+ /**
5818
+ * X Layer Mainnet chain definition
5819
+ * @remarks
5820
+ * This represents the official production network for the X Layer blockchain.
5821
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5822
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5823
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5824
+ */ const XLayer = defineChain({
5825
+ type: 'evm',
5826
+ chain: Blockchain.X_Layer,
5827
+ name: 'X Layer',
5828
+ title: 'X Layer Mainnet',
5829
+ nativeCurrency: {
5830
+ name: 'OKB',
5831
+ symbol: 'OKB',
5832
+ decimals: 18
5833
+ },
5834
+ chainId: 196,
5835
+ isTestnet: false,
5836
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5837
+ rpcEndpoints: [
5838
+ 'https://xlayerrpc.okx.com'
5839
+ ],
5840
+ eurcAddress: null,
5841
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5842
+ usdtAddress: null,
5843
+ cctp: {
5844
+ domain: 37,
5845
+ contracts: {
5846
+ v2: {
5847
+ type: 'split',
5848
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5849
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5850
+ confirmations: 65,
5851
+ fastConfirmations: 1
5852
+ }
5853
+ },
5854
+ forwarderSupported: {
5855
+ source: false,
5856
+ destination: false
5857
+ }
5858
+ },
5859
+ kitContracts: {
5860
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5861
+ }
5862
+ });
5863
+
5864
+ /**
5865
+ * X Layer Testnet chain definition
5866
+ * @remarks
5867
+ * This represents the official test network for the X Layer blockchain.
5868
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5869
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5870
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5871
+ */ const XLayerTestnet = defineChain({
5872
+ type: 'evm',
5873
+ chain: Blockchain.X_Layer_Testnet,
5874
+ name: 'X Layer Testnet',
5875
+ title: 'X Layer Testnet',
5876
+ nativeCurrency: {
5877
+ name: 'OKB',
5878
+ symbol: 'OKB',
5879
+ decimals: 18
5880
+ },
5881
+ chainId: 1952,
5882
+ isTestnet: true,
5883
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5884
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5885
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5886
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5887
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5888
+ rpcEndpoints: [
5889
+ 'https://testrpc.xlayer.tech'
5890
+ ],
5891
+ eurcAddress: null,
5892
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5893
+ usdtAddress: null,
5894
+ cctp: {
5895
+ domain: 37,
5896
+ contracts: {
5897
+ v2: {
5898
+ type: 'split',
5899
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5900
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5901
+ confirmations: 65,
5902
+ fastConfirmations: 1
5903
+ }
5904
+ },
5905
+ forwarderSupported: {
5906
+ source: false,
5907
+ destination: false
5908
+ }
5909
+ },
5910
+ kitContracts: {
5911
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5912
+ }
5913
+ });
5914
+
5812
5915
  /**
5813
5916
  * ZKSync Era Mainnet chain definition
5814
5917
  * @remarks
@@ -5928,6 +6031,8 @@ var Chains = /*#__PURE__*/Object.freeze({
5928
6031
  WorldChainSepolia: WorldChainSepolia,
5929
6032
  XDC: XDC,
5930
6033
  XDCApothem: XDCApothem,
6034
+ XLayer: XLayer,
6035
+ XLayerTestnet: XLayerTestnet,
5931
6036
  ZKSyncEra: ZKSyncEra,
5932
6037
  ZKSyncEraSepolia: ZKSyncEraSepolia
5933
6038
  });
@@ -7880,6 +7985,7 @@ function parseOrThrow(value, schema, context) {
7880
7985
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7881
7986
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7882
7987
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7988
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7883
7989
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7884
7990
  // =========================================================================
7885
7991
  // Testnets (alphabetically sorted)
@@ -7888,6 +7994,7 @@ function parseOrThrow(value, schema, context) {
7888
7994
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
7889
7995
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7890
7996
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
7997
+ [Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
7891
7998
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7892
7999
  [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7893
8000
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
@@ -7914,6 +8021,7 @@ function parseOrThrow(value, schema, context) {
7914
8021
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7915
8022
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7916
8023
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8024
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7917
8025
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7918
8026
  }
7919
8027
  };
@@ -8779,7 +8887,7 @@ function parseOrThrow(value, schema, context) {
8779
8887
  }
8780
8888
 
8781
8889
  var name = "@circle-fin/unified-balance-kit";
8782
- var version = "1.3.1";
8890
+ var version = "1.4.1";
8783
8891
  var pkg = {
8784
8892
  name: name,
8785
8893
  version: version};
@@ -9062,6 +9170,110 @@ var pkg = {
9062
9170
  * ```
9063
9171
  */ const USDC_DECIMALS$1 = 6;
9064
9172
 
9173
+ /**
9174
+ * Canonical list of actions that do not prepare or submit transactions.
9175
+ *
9176
+ * @internal
9177
+ */ const READ_ACTION_KEYS = [
9178
+ 'token.allowance',
9179
+ 'token.balanceOf',
9180
+ 'token.name',
9181
+ 'native.balanceOf',
9182
+ 'usdc.allowance',
9183
+ 'usdc.balanceOf',
9184
+ 'usdc.name',
9185
+ 'gateway.v1.isDelegate',
9186
+ 'gateway.v1.withdrawingBalance',
9187
+ 'gateway.v1.withdrawalBlock',
9188
+ 'gateway.v1.signBurnIntents'
9189
+ ];
9190
+ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
9191
+ /**
9192
+ * Check whether a runtime value identifies a read action.
9193
+ *
9194
+ * @param action - The value to classify.
9195
+ * @returns Whether the value is a registered read-action key.
9196
+ *
9197
+ * @example
9198
+ * ```typescript
9199
+ * import { isReadActionKey } from '@core/adapter'
9200
+ *
9201
+ * if (isReadActionKey(value)) {
9202
+ * await adapter.readAction(value, params, context)
9203
+ * }
9204
+ * ```
9205
+ *
9206
+ * @internal
9207
+ */ function isReadActionKey(action) {
9208
+ return typeof action === 'string' && READ_ACTION_KEY_SET.has(action);
9209
+ }
9210
+
9211
+ /**
9212
+ * Create the standard error for a missing or non-read action.
9213
+ *
9214
+ * @param action - The unsupported action value.
9215
+ * @returns A fatal unsupported-action error.
9216
+ *
9217
+ * @internal
9218
+ */ function createUnsupportedReadActionError(action) {
9219
+ return new KitError({
9220
+ ...InputError.UNSUPPORTED_ACTION,
9221
+ recoverability: 'FATAL',
9222
+ message: `Read action "${String(action)}" is not registered in this adapter.`
9223
+ });
9224
+ }
9225
+ /**
9226
+ * Execute a read through the adapter's dedicated read seam when available.
9227
+ *
9228
+ * @remarks
9229
+ * Fall back to the legacy `prepareAction().execute()` contract so providers
9230
+ * remain runtime-compatible with adapter versions released before `readAction`.
9231
+ * Consumers must upgrade their adapter package for reads to bypass custom
9232
+ * `prepareAction` wrappers.
9233
+ *
9234
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
9235
+ * @typeParam TActionKey - The read action key.
9236
+ * @param adapter - The adapter that owns the read action.
9237
+ * @param action - The read action to execute.
9238
+ * @param params - The parameters for the read action.
9239
+ * @param ctx - The operation context.
9240
+ * @returns The raw read-action result.
9241
+ * @throws {KitError} When `action` is not a supported read-action key.
9242
+ *
9243
+ * @example
9244
+ * ```typescript
9245
+ * import { executeAdapterReadAction } from '@core/adapter'
9246
+ * import { Ethereum } from '@core/chains'
9247
+ *
9248
+ * const allowance = await executeAdapterReadAction(
9249
+ * adapter,
9250
+ * 'token.allowance',
9251
+ * { tokenAddress, delegate },
9252
+ * { chain: Ethereum },
9253
+ * )
9254
+ * ```
9255
+ *
9256
+ * @internal
9257
+ */ async function executeAdapterReadAction(adapter, action, params, ctx) {
9258
+ if (!isReadActionKey(action)) {
9259
+ throw createUnsupportedReadActionError(action);
9260
+ }
9261
+ const runtimeAdapter = adapter;
9262
+ if (typeof runtimeAdapter.readAction === 'function') {
9263
+ return runtimeAdapter.readAction(action, params, ctx);
9264
+ }
9265
+ let request;
9266
+ try {
9267
+ request = await adapter.prepareAction(action, params, ctx);
9268
+ } catch (error) {
9269
+ if (error instanceof Error && error.message === `Action ${action} is not supported`) {
9270
+ throw createUnsupportedReadActionError(action);
9271
+ }
9272
+ throw error;
9273
+ }
9274
+ return request.execute();
9275
+ }
9276
+
9065
9277
  /**
9066
9278
  * Schema for validating hexadecimal strings with '0x' prefix.
9067
9279
  *
@@ -9271,16 +9483,15 @@ var pkg = {
9271
9483
  * ```
9272
9484
  */ const validateBalanceForTransaction = async (params)=>{
9273
9485
  const { amount, adapter, token, tokenAddress, operationContext } = params;
9274
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
9486
+ const balance = await executeAdapterReadAction(adapter, 'usdc.balanceOf', {
9275
9487
  walletAddress: operationContext.address
9276
9488
  }, operationContext);
9277
- const balance = await balancePrepared.execute();
9278
- if (BigInt(balance) < BigInt(amount)) {
9489
+ if (BigInt(String(balance)) < BigInt(amount)) {
9279
9490
  // Extract chain name from operationContext
9280
9491
  const chainName = extractChainInfo(operationContext.chain).name;
9281
9492
  // Create KitError with rich context in trace
9282
9493
  throw createInsufficientTokenBalanceError(chainName, token, {
9283
- balance: balance.toString(),
9494
+ balance: String(balance),
9284
9495
  amount,
9285
9496
  tokenAddress,
9286
9497
  walletAddress: operationContext.address
@@ -12060,72 +12271,55 @@ function evmSigningData(burnIntent) {
12060
12271
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12061
12272
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12062
12273
  * signatures, so for Gateway's purposes a 7702-delegated address is
12063
- * an EOA, not an SCA.
12274
+ * an EOA, not a contract signer.
12064
12275
  *
12065
12276
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12066
12277
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12067
12278
  /**
12068
- * Assert that `address` on `chain` can sign Gateway burn intents.
12069
- *
12070
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
12071
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
12072
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
12073
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
12074
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
12075
- * against Gateway's chainId-less domain with an opaque
12076
- * `invalid integer value <nil>/<nil> for type uint256` error.
12279
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
12280
+ * rather than as an EOA.
12077
12281
  *
12078
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
12079
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
12080
- * `ecrecover`-verifiable signatures, so Gateway accepts them.
12282
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
12283
+ * check for EOAs, and for requests that carry `contractSigner: true` —
12284
+ * an offchain `isValidSignature` simulation against the signing contract
12285
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
12286
+ * declare it. This detects the contract case from on-chain bytecode.
12081
12287
  *
12082
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
12083
- * error directing the caller to register an EOA delegate against the
12084
- * SCA and then submit the spend with the delegate EOA as the signer
12085
- * and the SCA as the source account. See the unified-balance / Gateway
12086
- * docs for the exact API.
12288
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
12289
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
12290
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
12291
+ * stays correct for them.
12087
12292
  *
12088
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
12089
- * skipped and downstream signing surfaces its own error a warning is
12090
- * logged so the skip is diagnosable.
12293
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
12294
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
12295
+ * genuine contract signer misreported this way is rejected by Gateway with
12296
+ * an invalid-signature error rather than silently mis-attested.
12091
12297
  *
12092
12298
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12093
- * @param address - Signer address to validate.
12299
+ * @param address - Signer address to classify.
12094
12300
  * @param chain - EVM chain where the signer lives.
12095
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
12301
+ * @returns `true` when the signer is a contract account and the transfer
12302
+ * request must set `contractSigner: true`; `false` otherwise.
12096
12303
  *
12097
12304
  * @example
12098
12305
  * ```typescript
12099
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12306
+ * import { isContractSigner } from '@core/adapter-evm'
12100
12307
  * import { Ethereum } from '@core/chains'
12101
12308
  *
12102
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12309
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12103
12310
  * ```
12104
- */ async function assertSignerIsEoa(adapter, address, chain) {
12311
+ */ async function isContractSigner(adapter, address, chain) {
12105
12312
  let code;
12106
12313
  try {
12107
12314
  code = await adapter.readBytecode(address, chain);
12108
12315
  } catch (err) {
12109
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12110
- return;
12316
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12317
+ return false;
12111
12318
  }
12112
12319
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12113
- return;
12320
+ return false;
12114
12321
  }
12115
- throw new KitError({
12116
- ...InputError.UNSUPPORTED_ACTION,
12117
- recoverability: 'FATAL',
12118
- message: `Gateway burn-intent signing requires an EOA signer (Gateway ` + `verifies signatures with ecrecover and does not support ERC-1271). ` + `The signer ${address} on ${chain.name} has on-chain bytecode, ` + `indicating it is a smart-contract account (SCA). Register an EOA ` + `delegate against the SCA, then submit the spend with the delegate ` + `EOA as the signer and the SCA as the source account. See DEVX-2774.`,
12119
- cause: {
12120
- trace: {
12121
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
12122
- address,
12123
- chain: chain.name,
12124
- bytecodeBytes: (code.length - 2) / 2,
12125
- bytecodePrefix: code.slice(0, 12)
12126
- }
12127
- }
12128
- });
12322
+ return true;
12129
12323
  }
12130
12324
 
12131
12325
  /**
@@ -12152,78 +12346,177 @@ function evmSigningData(burnIntent) {
12152
12346
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12153
12347
  }
12154
12348
 
12349
+ function resolveIntentChain(group, intent) {
12350
+ const sourceDomain = intent.spec.sourceDomain;
12351
+ const chain = group.chainsByDomain.get(sourceDomain);
12352
+ if (chain !== undefined) return chain;
12353
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
12354
+ }
12355
+ function normalizeSignatureResult(result) {
12356
+ if (typeof result === 'string') {
12357
+ return {
12358
+ signature: result,
12359
+ contractSigner: false
12360
+ };
12361
+ }
12362
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
12363
+ return {
12364
+ signature: result.signature,
12365
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
12366
+ };
12367
+ }
12368
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
12369
+ }
12370
+ function validateGroupIntents(intents) {
12371
+ evmSigningData(intents);
12372
+ }
12373
+ function collectChainsByDomain(group) {
12374
+ const chainsByDomain = new Map();
12375
+ for (const intent of group.intents){
12376
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
12377
+ }
12378
+ return chainsByDomain;
12379
+ }
12380
+ async function classifySignerTypes(group, chainsByDomain) {
12381
+ const { adapter, address } = group;
12382
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12383
+ // each consumer package bundles its own copy of the base class and the
12384
+ // `instanceof` identity check fails across package boundaries.
12385
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
12386
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12387
+ const signerTypes = await Promise.all([
12388
+ ...chainsByDomain
12389
+ ].map(async ([sourceDomain, sourceChain])=>{
12390
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
12391
+ return [
12392
+ sourceDomain,
12393
+ contractSigner
12394
+ ];
12395
+ }));
12396
+ return new Map(signerTypes);
12397
+ }
12398
+ function createSigningUnits(group, signerTypeByDomain) {
12399
+ const contractUnitsByDomain = new Map();
12400
+ let eoaUnit;
12401
+ for (const [index, intent] of group.intents.entries()){
12402
+ const sourceDomain = intent.spec.sourceDomain;
12403
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
12404
+ if (contractSigner) {
12405
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
12406
+ if (existingUnit === undefined) {
12407
+ contractUnitsByDomain.set(sourceDomain, {
12408
+ intents: [
12409
+ intent
12410
+ ],
12411
+ chain: resolveIntentChain(group, intent),
12412
+ contractSigner: true,
12413
+ firstIntentIndex: index
12414
+ });
12415
+ } else {
12416
+ existingUnit.intents.push(intent);
12417
+ }
12418
+ } else {
12419
+ eoaUnit ??= {
12420
+ intents: [],
12421
+ chain: resolveIntentChain(group, intent),
12422
+ contractSigner: false,
12423
+ firstIntentIndex: index
12424
+ };
12425
+ eoaUnit.intents.push(intent);
12426
+ }
12427
+ }
12428
+ const signingUnits = [
12429
+ ...contractUnitsByDomain.values()
12430
+ ];
12431
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
12432
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
12433
+ return signingUnits;
12434
+ }
12435
+ async function signUnit(group, unit) {
12436
+ const { adapter, address } = group;
12437
+ const firstIntent = unit.intents[0];
12438
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
12439
+ const operationContext = address === undefined ? {
12440
+ chain: unit.chain
12441
+ } : {
12442
+ chain: unit.chain,
12443
+ address
12444
+ };
12445
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12446
+ typedData,
12447
+ chain: unit.chain
12448
+ }, operationContext);
12449
+ const result = normalizeSignatureResult(await signRequest.execute());
12450
+ return {
12451
+ intents: unit.intents,
12452
+ signature: result.signature,
12453
+ contractSigner: result.contractSigner || unit.contractSigner
12454
+ };
12455
+ }
12456
+ async function signUnits(group, signingUnits) {
12457
+ const signedSets = [];
12458
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
12459
+ // in parallel, but one signer is asked for its chain-bound signatures in
12460
+ // source-intent order.
12461
+ for (const unit of signingUnits){
12462
+ signedSets.push(await signUnit(group, unit));
12463
+ }
12464
+ return signedSets;
12465
+ }
12155
12466
  /**
12156
- * Sign an EVM adapter group: batches all intents and produces a single
12157
- * EIP-712 ECDSA signature.
12467
+ * Sign an EVM adapter group.
12158
12468
  *
12159
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12160
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
12469
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
12470
+ * intents are grouped and signed per source chain because smart accounts
12471
+ * commonly include `chainId` in their replay-safe signature hash.
12472
+ * All returned entries can still be submitted together in one atomic Gateway
12473
+ * transfer request.
12161
12474
  *
12162
- * Before signing, asserts that the signer address is an EOA. Gateway
12163
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
12164
- * fallback), so signatures produced by smart-contract accounts (SCAs)
12165
- * cannot be verified. When an SCA is detected, a clear error is raised
12166
- * directing the caller to the delegate workflow (DEVX-2774).
12475
+ * Before signing, classifies the signer as an EOA or a contract account.
12476
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
12477
+ * signatures with ERC-1271, but it does not infer which one applies — the
12478
+ * transfer request has to declare it. The returned `contractSigner` flag
12479
+ * carries that decision through to `buildTransferRequestBody`.
12167
12480
  *
12168
12481
  * @param group - The adapter group containing the adapter, chain, and
12169
12482
  * burn intents to sign.
12170
- * @returns A signed set with the intents and the ECDSA signature.
12483
+ * @returns Signed entries with their intents, signatures, and Gateway signer
12484
+ * validation mode.
12485
+ * @throws KitError when an intent has no source-chain mapping or a signing
12486
+ * action returns an invalid signature shape.
12171
12487
  *
12172
12488
  * @example
12173
12489
  * ```typescript
12174
12490
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12175
12491
  *
12176
- * const signedSet = await signEvmIntentGroup({
12492
+ * const signedSets = await signEvmIntentGroup({
12177
12493
  * adapter: evmAdapter,
12178
12494
  * chain: ethereumChain,
12179
12495
  * intents: [burnIntent1, burnIntent2],
12496
+ * chainsByDomain: new Map([
12497
+ * [0, ethereumChain],
12498
+ * [6, baseChain],
12499
+ * ]),
12180
12500
  * address: '0x...',
12181
12501
  * })
12182
- * console.log(signedSet.signature)
12502
+ * console.log(signedSets)
12183
12503
  * ```
12184
12504
  */ async function signEvmIntentGroup(group) {
12185
- const { adapter, intents: groupIntents, chain, address } = group;
12186
- const operationContext = address === undefined ? {
12187
- chain
12188
- } : {
12189
- chain,
12190
- address
12191
- };
12192
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
12193
- // signer silently produces a signature over a wrapped hash that Gateway
12194
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
12195
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
12196
- // when we can detect bytecode at the signer address. See DEVX-2774.
12197
- //
12198
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12199
- // each consumer package bundles its own copy of the base class and the
12200
- // `instanceof` identity check fails across package boundaries.
12201
- //
12202
- // Empty string is defended against because assertSignerIsEoa would
12203
- // otherwise call eth_getCode('') on the RPC.
12204
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12205
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
12206
- await assertSignerIsEoa(adapter, address, chain);
12207
- }
12208
- const firstIntent = groupIntents[0];
12209
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
12210
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12211
- typedData,
12212
- chain
12213
- }, operationContext);
12214
- const sig = await signRequest.execute();
12215
- return {
12216
- intents: groupIntents,
12217
- signature: sig
12218
- };
12505
+ // Validate the collection before doing bytecode reads or asking a wallet
12506
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
12507
+ validateGroupIntents(group.intents);
12508
+ const chainsByDomain = collectChainsByDomain(group);
12509
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
12510
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
12511
+ return await signUnits(group, signingUnits);
12219
12512
  }
12220
12513
 
12221
12514
  /**
12222
12515
  * Add an EVM intent into the batched EVM group map.
12223
12516
  *
12224
12517
  * On EVM, all intents for the same adapter are batched into a single
12225
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
12226
- * operation.
12518
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12519
+ * while signing ERC-1271 intents separately on their source chains.
12227
12520
  *
12228
12521
  * @param intent - The burn intent to group.
12229
12522
  * @param alloc - The allocation that resolved to this intent.
@@ -12240,6 +12533,7 @@ function evmSigningData(burnIntent) {
12240
12533
  const existing = evmGroups.get(alloc.adapter);
12241
12534
  if (existing) {
12242
12535
  existing.intents.push(intent);
12536
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12243
12537
  } else {
12244
12538
  evmGroups.set(alloc.adapter, {
12245
12539
  adapter: alloc.adapter,
@@ -12247,6 +12541,12 @@ function evmSigningData(burnIntent) {
12247
12541
  intents: [
12248
12542
  intent
12249
12543
  ],
12544
+ chainsByDomain: new Map([
12545
+ [
12546
+ alloc.chain.gateway.domain,
12547
+ alloc.chain
12548
+ ]
12549
+ ]),
12250
12550
  address: alloc.sourceSigner
12251
12551
  });
12252
12552
  }
@@ -13772,7 +14072,8 @@ function throwNetworkMismatch(expected, actual) {
13772
14072
  };
13773
14073
  }
13774
14074
  /**
13775
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
14075
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
14076
+ * with every source chain retained by Gateway domain).
13776
14077
  *
13777
14078
  * @param intents - Burn intents from estimate response.
13778
14079
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16665,22 +16966,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16665
16966
  *
16666
16967
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16667
16968
  *
16969
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
16970
+ * Gateway to validate the signature with ERC-1271 (an offchain
16971
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
16972
+ * omitted for EOA signers so their payloads stay byte-identical.
16973
+ *
16668
16974
  * @param signedSets - Signed intent sets (intents + signature per signer).
16669
16975
  * @returns Array of transfer payloads for POST /v1/transfer.
16670
16976
  */ function buildTransferRequestBody(signedSets) {
16671
16977
  return signedSets.map((set)=>{
16672
16978
  const firstIntent = set.intents[0];
16979
+ const contractSigner = set.contractSigner === true ? {
16980
+ contractSigner: true
16981
+ } : {};
16673
16982
  if (set.intents.length === 1 && firstIntent) {
16674
16983
  return {
16675
16984
  burnIntent: serializeBurnIntent(firstIntent),
16676
- signature: set.signature
16985
+ signature: set.signature,
16986
+ ...contractSigner
16677
16987
  };
16678
16988
  }
16679
16989
  return {
16680
16990
  burnIntentSet: {
16681
16991
  intents: set.intents.map(serializeBurnIntent)
16682
16992
  },
16683
- signature: set.signature
16993
+ signature: set.signature,
16994
+ ...contractSigner
16684
16995
  };
16685
16996
  });
16686
16997
  }
@@ -17043,11 +17354,16 @@ const BPS_DIVISOR = 100_000n;
17043
17354
  return required;
17044
17355
  }
17045
17356
 
17357
+ function requireEvmChainsByDomain(group) {
17358
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
17359
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
17360
+ }
17046
17361
  /**
17047
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
17362
+ * Sign each adapter group: Solana one intent per signature, and EVM either
17363
+ * batched for EOAs or split by source chain for ERC-1271 signers.
17048
17364
  *
17049
17365
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17050
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17366
+ * @returns Promise of signed sets for buildTransferRequestBody.
17051
17367
  *
17052
17368
  * @example
17053
17369
  * ```typescript
@@ -17060,9 +17376,10 @@ const BPS_DIVISOR = 100_000n;
17060
17376
  if (group.chain.type === 'solana') {
17061
17377
  return signSolanaIntentGroup(group);
17062
17378
  }
17063
- return [
17064
- await signEvmIntentGroup(group)
17065
- ];
17379
+ return await signEvmIntentGroup({
17380
+ ...group,
17381
+ chainsByDomain: requireEvmChainsByDomain(group)
17382
+ });
17066
17383
  }));
17067
17384
  return nested.flat();
17068
17385
  }
@@ -17712,7 +18029,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17712
18029
  signedSetCount: signedSets.length,
17713
18030
  signatures: signedSets.map((s)=>({
17714
18031
  intentCount: s.intents.length,
17715
- signature: s.signature
18032
+ signature: s.signature,
18033
+ contractSigner: s.contractSigner === true
17716
18034
  }))
17717
18035
  }
17718
18036
  });
@@ -18167,8 +18485,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18167
18485
  chain
18168
18486
  };
18169
18487
  // Step 1: Quick check at latest block (no HTTP call)
18170
- const latestRequest = await adapter.prepareAction('gateway.v1.isDelegate', baseActionParams, operationContext);
18171
- const latestResult = await latestRequest.execute();
18488
+ const latestResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', baseActionParams, operationContext);
18172
18489
  if (String(latestResult).toLowerCase() !== 'true') {
18173
18490
  return 'none';
18174
18491
  }
@@ -18177,13 +18494,12 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18177
18494
  // Solana uses confirmed vs finalized commitment as a proxy for
18178
18495
  // Gateway finality. This is conservative — can only over-report
18179
18496
  // 'pending', never falsely report 'ready'.
18180
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
18181
- ...baseActionParams,
18182
- commitment: 'finalized'
18183
- }, operationContext);
18184
18497
  let finalizedResult;
18185
18498
  try {
18186
- finalizedResult = await finalizedRequest.execute();
18499
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
18500
+ ...baseActionParams,
18501
+ commitment: 'finalized'
18502
+ }, operationContext);
18187
18503
  } catch (error) {
18188
18504
  if (isBlockRangeError(error)) {
18189
18505
  return 'pending';
@@ -18194,10 +18510,6 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18194
18510
  }
18195
18511
  // EVM: use processedHeight from /v1/info
18196
18512
  const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain);
18197
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
18198
- ...baseActionParams,
18199
- blockNumber: processedHeight
18200
- }, operationContext);
18201
18513
  // If the RPC node lags Gateway's indexer view, the historical read at
18202
18514
  // processedHeight may throw a block-range error. This is safe to treat
18203
18515
  // as 'pending' because processedHeight comes from Gateway's /v1/info
@@ -18206,7 +18518,10 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18206
18518
  // Re-throw structural errors to avoid masking real bugs.
18207
18519
  let finalizedResult;
18208
18520
  try {
18209
- finalizedResult = await finalizedRequest.execute();
18521
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
18522
+ ...baseActionParams,
18523
+ blockNumber: processedHeight
18524
+ }, operationContext);
18210
18525
  } catch (error) {
18211
18526
  if (isBlockRangeError(error)) {
18212
18527
  return 'pending';
@@ -18267,8 +18582,8 @@ function parseAmountSafe(amount) {
18267
18582
  chain
18268
18583
  };
18269
18584
  const [withdrawingRaw, withdrawalBlockRaw] = await Promise.all([
18270
- adapter.prepareAction('gateway.v1.withdrawingBalance', readParams, operationContext).then(async (req)=>req.execute()),
18271
- adapter.prepareAction('gateway.v1.withdrawalBlock', readParams, operationContext).then(async (req)=>req.execute())
18585
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', readParams, operationContext),
18586
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawalBlock', readParams, operationContext)
18272
18587
  ]);
18273
18588
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
18274
18589
  const withdrawalBlockValue = safeBigInt(String(withdrawalBlockRaw), 'withdrawalBlock');
@@ -18308,12 +18623,11 @@ function parseAmountSafe(amount) {
18308
18623
  const tokenAddress = getTokenAddress(chain, params.token);
18309
18624
  // Read the pending balance before withdrawing — the contract resets it to 0
18310
18625
  // after withdraw() executes, so this is the only way to capture the amount.
18311
- const withdrawingBalanceReq = await adapter.prepareAction('gateway.v1.withdrawingBalance', {
18626
+ const withdrawingRaw = await executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', {
18312
18627
  token: tokenAddress,
18313
18628
  depositor: signerAddress,
18314
18629
  chain
18315
18630
  }, operationContext);
18316
- const withdrawingRaw = await withdrawingBalanceReq.execute();
18317
18631
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
18318
18632
  if (withdrawingValue === 0n) {
18319
18633
  throw new KitError({