@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.
@@ -2325,6 +2325,8 @@ class KitError extends Error {
2325
2325
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2326
2326
  Blockchain["XDC"] = "XDC";
2327
2327
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2328
+ Blockchain["X_Layer"] = "X_Layer";
2329
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2328
2330
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2329
2331
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2330
2332
  })(Blockchain || (Blockchain = {}));
@@ -2378,6 +2380,7 @@ var BridgeChain;
2378
2380
  BridgeChain["Unichain"] = "Unichain";
2379
2381
  BridgeChain["World_Chain"] = "World_Chain";
2380
2382
  BridgeChain["XDC"] = "XDC";
2383
+ BridgeChain["X_Layer"] = "X_Layer";
2381
2384
  // Testnet chains with CCTPv2 support
2382
2385
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2383
2386
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2403,6 +2406,7 @@ var BridgeChain;
2403
2406
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2404
2407
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2405
2408
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2409
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2406
2410
  })(BridgeChain || (BridgeChain = {}));
2407
2411
  var UnifiedBalanceChain;
2408
2412
  (function(UnifiedBalanceChain) {
@@ -4950,7 +4954,8 @@ var EarnChain;
4950
4954
  isTestnet: true,
4951
4955
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4952
4956
  rpcEndpoints: [
4953
- 'https://rpc-amoy.polygon.technology'
4957
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4958
+ 'https://polygon-amoy.drpc.org'
4954
4959
  ],
4955
4960
  eurcAddress: null,
4956
4961
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5815,6 +5820,104 @@ var EarnChain;
5815
5820
  }
5816
5821
  });
5817
5822
 
5823
+ /**
5824
+ * X Layer Mainnet chain definition
5825
+ * @remarks
5826
+ * This represents the official production network for the X Layer blockchain.
5827
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5828
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5829
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5830
+ */ const XLayer = defineChain({
5831
+ type: 'evm',
5832
+ chain: Blockchain.X_Layer,
5833
+ name: 'X Layer',
5834
+ title: 'X Layer Mainnet',
5835
+ nativeCurrency: {
5836
+ name: 'OKB',
5837
+ symbol: 'OKB',
5838
+ decimals: 18
5839
+ },
5840
+ chainId: 196,
5841
+ isTestnet: false,
5842
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5843
+ rpcEndpoints: [
5844
+ 'https://xlayerrpc.okx.com'
5845
+ ],
5846
+ eurcAddress: null,
5847
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5848
+ usdtAddress: null,
5849
+ cctp: {
5850
+ domain: 37,
5851
+ contracts: {
5852
+ v2: {
5853
+ type: 'split',
5854
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5855
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5856
+ confirmations: 65,
5857
+ fastConfirmations: 1
5858
+ }
5859
+ },
5860
+ forwarderSupported: {
5861
+ source: false,
5862
+ destination: false
5863
+ }
5864
+ },
5865
+ kitContracts: {
5866
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5867
+ }
5868
+ });
5869
+
5870
+ /**
5871
+ * X Layer Testnet chain definition
5872
+ * @remarks
5873
+ * This represents the official test network for the X Layer blockchain.
5874
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5875
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5876
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5877
+ */ const XLayerTestnet = defineChain({
5878
+ type: 'evm',
5879
+ chain: Blockchain.X_Layer_Testnet,
5880
+ name: 'X Layer Testnet',
5881
+ title: 'X Layer Testnet',
5882
+ nativeCurrency: {
5883
+ name: 'OKB',
5884
+ symbol: 'OKB',
5885
+ decimals: 18
5886
+ },
5887
+ chainId: 1952,
5888
+ isTestnet: true,
5889
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5890
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5891
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5892
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5893
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5894
+ rpcEndpoints: [
5895
+ 'https://testrpc.xlayer.tech'
5896
+ ],
5897
+ eurcAddress: null,
5898
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5899
+ usdtAddress: null,
5900
+ cctp: {
5901
+ domain: 37,
5902
+ contracts: {
5903
+ v2: {
5904
+ type: 'split',
5905
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5906
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5907
+ confirmations: 65,
5908
+ fastConfirmations: 1
5909
+ }
5910
+ },
5911
+ forwarderSupported: {
5912
+ source: false,
5913
+ destination: false
5914
+ }
5915
+ },
5916
+ kitContracts: {
5917
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5918
+ }
5919
+ });
5920
+
5818
5921
  /**
5819
5922
  * ZKSync Era Mainnet chain definition
5820
5923
  * @remarks
@@ -5934,6 +6037,8 @@ var Chains = {
5934
6037
  WorldChainSepolia: WorldChainSepolia,
5935
6038
  XDC: XDC,
5936
6039
  XDCApothem: XDCApothem,
6040
+ XLayer: XLayer,
6041
+ XLayerTestnet: XLayerTestnet,
5937
6042
  ZKSyncEra: ZKSyncEra,
5938
6043
  ZKSyncEraSepolia: ZKSyncEraSepolia
5939
6044
  };
@@ -7886,6 +7991,7 @@ function parseOrThrow(value, schema, context) {
7886
7991
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7887
7992
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7888
7993
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7994
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7889
7995
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7890
7996
  // =========================================================================
7891
7997
  // Testnets (alphabetically sorted)
@@ -7894,6 +8000,7 @@ function parseOrThrow(value, schema, context) {
7894
8000
  [Blockchain.Arbitrum_Sepolia]: '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d',
7895
8001
  [Blockchain.Avalanche_Fuji]: '0x5425890298aed601595a70AB815c96711a31Bc65',
7896
8002
  [Blockchain.Base_Sepolia]: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
8003
+ [Blockchain.Celo_Alfajores_Testnet]: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B',
7897
8004
  [Blockchain.Codex_Testnet]: '0x6d7f141b6819C2c9CC2f818e6ad549E7Ca090F8f',
7898
8005
  [Blockchain.Cronos_Testnet]: '0xEb33dc5fac03833e132593659e1dE7256aB59794',
7899
8006
  [Blockchain.Edge_Testnet]: '0x2d9F7CAD728051AA35Ecdc472a14cf8cDF5CFD6B',
@@ -7920,6 +8027,7 @@ function parseOrThrow(value, schema, context) {
7920
8027
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7921
8028
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7922
8029
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8030
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7923
8031
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7924
8032
  }
7925
8033
  };
@@ -8785,7 +8893,7 @@ function parseOrThrow(value, schema, context) {
8785
8893
  }
8786
8894
 
8787
8895
  var name = "@circle-fin/unified-balance-kit";
8788
- var version = "1.3.1";
8896
+ var version = "1.4.1";
8789
8897
  var pkg = {
8790
8898
  name: name,
8791
8899
  version: version};
@@ -9068,6 +9176,110 @@ var pkg = {
9068
9176
  * ```
9069
9177
  */ const USDC_DECIMALS$1 = 6;
9070
9178
 
9179
+ /**
9180
+ * Canonical list of actions that do not prepare or submit transactions.
9181
+ *
9182
+ * @internal
9183
+ */ const READ_ACTION_KEYS = [
9184
+ 'token.allowance',
9185
+ 'token.balanceOf',
9186
+ 'token.name',
9187
+ 'native.balanceOf',
9188
+ 'usdc.allowance',
9189
+ 'usdc.balanceOf',
9190
+ 'usdc.name',
9191
+ 'gateway.v1.isDelegate',
9192
+ 'gateway.v1.withdrawingBalance',
9193
+ 'gateway.v1.withdrawalBlock',
9194
+ 'gateway.v1.signBurnIntents'
9195
+ ];
9196
+ const READ_ACTION_KEY_SET = new Set(READ_ACTION_KEYS);
9197
+ /**
9198
+ * Check whether a runtime value identifies a read action.
9199
+ *
9200
+ * @param action - The value to classify.
9201
+ * @returns Whether the value is a registered read-action key.
9202
+ *
9203
+ * @example
9204
+ * ```typescript
9205
+ * import { isReadActionKey } from '@core/adapter'
9206
+ *
9207
+ * if (isReadActionKey(value)) {
9208
+ * await adapter.readAction(value, params, context)
9209
+ * }
9210
+ * ```
9211
+ *
9212
+ * @internal
9213
+ */ function isReadActionKey(action) {
9214
+ return typeof action === 'string' && READ_ACTION_KEY_SET.has(action);
9215
+ }
9216
+
9217
+ /**
9218
+ * Create the standard error for a missing or non-read action.
9219
+ *
9220
+ * @param action - The unsupported action value.
9221
+ * @returns A fatal unsupported-action error.
9222
+ *
9223
+ * @internal
9224
+ */ function createUnsupportedReadActionError(action) {
9225
+ return new KitError({
9226
+ ...InputError.UNSUPPORTED_ACTION,
9227
+ recoverability: 'FATAL',
9228
+ message: `Read action "${String(action)}" is not registered in this adapter.`
9229
+ });
9230
+ }
9231
+ /**
9232
+ * Execute a read through the adapter's dedicated read seam when available.
9233
+ *
9234
+ * @remarks
9235
+ * Fall back to the legacy `prepareAction().execute()` contract so providers
9236
+ * remain runtime-compatible with adapter versions released before `readAction`.
9237
+ * Consumers must upgrade their adapter package for reads to bypass custom
9238
+ * `prepareAction` wrappers.
9239
+ *
9240
+ * @typeParam TAdapterCapabilities - The adapter capabilities type.
9241
+ * @typeParam TActionKey - The read action key.
9242
+ * @param adapter - The adapter that owns the read action.
9243
+ * @param action - The read action to execute.
9244
+ * @param params - The parameters for the read action.
9245
+ * @param ctx - The operation context.
9246
+ * @returns The raw read-action result.
9247
+ * @throws {KitError} When `action` is not a supported read-action key.
9248
+ *
9249
+ * @example
9250
+ * ```typescript
9251
+ * import { executeAdapterReadAction } from '@core/adapter'
9252
+ * import { Ethereum } from '@core/chains'
9253
+ *
9254
+ * const allowance = await executeAdapterReadAction(
9255
+ * adapter,
9256
+ * 'token.allowance',
9257
+ * { tokenAddress, delegate },
9258
+ * { chain: Ethereum },
9259
+ * )
9260
+ * ```
9261
+ *
9262
+ * @internal
9263
+ */ async function executeAdapterReadAction(adapter, action, params, ctx) {
9264
+ if (!isReadActionKey(action)) {
9265
+ throw createUnsupportedReadActionError(action);
9266
+ }
9267
+ const runtimeAdapter = adapter;
9268
+ if (typeof runtimeAdapter.readAction === 'function') {
9269
+ return runtimeAdapter.readAction(action, params, ctx);
9270
+ }
9271
+ let request;
9272
+ try {
9273
+ request = await adapter.prepareAction(action, params, ctx);
9274
+ } catch (error) {
9275
+ if (error instanceof Error && error.message === `Action ${action} is not supported`) {
9276
+ throw createUnsupportedReadActionError(action);
9277
+ }
9278
+ throw error;
9279
+ }
9280
+ return request.execute();
9281
+ }
9282
+
9071
9283
  /**
9072
9284
  * Schema for validating hexadecimal strings with '0x' prefix.
9073
9285
  *
@@ -9277,16 +9489,15 @@ var pkg = {
9277
9489
  * ```
9278
9490
  */ const validateBalanceForTransaction = async (params)=>{
9279
9491
  const { amount, adapter, token, tokenAddress, operationContext } = params;
9280
- const balancePrepared = await adapter.prepareAction('usdc.balanceOf', {
9492
+ const balance = await executeAdapterReadAction(adapter, 'usdc.balanceOf', {
9281
9493
  walletAddress: operationContext.address
9282
9494
  }, operationContext);
9283
- const balance = await balancePrepared.execute();
9284
- if (BigInt(balance) < BigInt(amount)) {
9495
+ if (BigInt(String(balance)) < BigInt(amount)) {
9285
9496
  // Extract chain name from operationContext
9286
9497
  const chainName = extractChainInfo(operationContext.chain).name;
9287
9498
  // Create KitError with rich context in trace
9288
9499
  throw createInsufficientTokenBalanceError(chainName, token, {
9289
- balance: balance.toString(),
9500
+ balance: String(balance),
9290
9501
  amount,
9291
9502
  tokenAddress,
9292
9503
  walletAddress: operationContext.address
@@ -12066,72 +12277,55 @@ function evmSigningData(burnIntent) {
12066
12277
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12067
12278
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12068
12279
  * signatures, so for Gateway's purposes a 7702-delegated address is
12069
- * an EOA, not an SCA.
12280
+ * an EOA, not a contract signer.
12070
12281
  *
12071
12282
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12072
12283
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12073
12284
  /**
12074
- * Assert that `address` on `chain` can sign Gateway burn intents.
12075
- *
12076
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
12077
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
12078
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
12079
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
12080
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
12081
- * against Gateway's chainId-less domain with an opaque
12082
- * `invalid integer value <nil>/<nil> for type uint256` error.
12285
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
12286
+ * rather than as an EOA.
12083
12287
  *
12084
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
12085
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
12086
- * `ecrecover`-verifiable signatures, so Gateway accepts them.
12288
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
12289
+ * check for EOAs, and for requests that carry `contractSigner: true` —
12290
+ * an offchain `isValidSignature` simulation against the signing contract
12291
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
12292
+ * declare it. This detects the contract case from on-chain bytecode.
12087
12293
  *
12088
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
12089
- * error directing the caller to register an EOA delegate against the
12090
- * SCA and then submit the spend with the delegate EOA as the signer
12091
- * and the SCA as the source account. See the unified-balance / Gateway
12092
- * docs for the exact API.
12294
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
12295
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
12296
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
12297
+ * stays correct for them.
12093
12298
  *
12094
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
12095
- * skipped and downstream signing surfaces its own error a warning is
12096
- * logged so the skip is diagnosable.
12299
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
12300
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
12301
+ * genuine contract signer misreported this way is rejected by Gateway with
12302
+ * an invalid-signature error rather than silently mis-attested.
12097
12303
  *
12098
12304
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12099
- * @param address - Signer address to validate.
12305
+ * @param address - Signer address to classify.
12100
12306
  * @param chain - EVM chain where the signer lives.
12101
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
12307
+ * @returns `true` when the signer is a contract account and the transfer
12308
+ * request must set `contractSigner: true`; `false` otherwise.
12102
12309
  *
12103
12310
  * @example
12104
12311
  * ```typescript
12105
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12312
+ * import { isContractSigner } from '@core/adapter-evm'
12106
12313
  * import { Ethereum } from '@core/chains'
12107
12314
  *
12108
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12315
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12109
12316
  * ```
12110
- */ async function assertSignerIsEoa(adapter, address, chain) {
12317
+ */ async function isContractSigner(adapter, address, chain) {
12111
12318
  let code;
12112
12319
  try {
12113
12320
  code = await adapter.readBytecode(address, chain);
12114
12321
  } catch (err) {
12115
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12116
- return;
12322
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12323
+ return false;
12117
12324
  }
12118
12325
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12119
- return;
12326
+ return false;
12120
12327
  }
12121
- throw new KitError({
12122
- ...InputError.UNSUPPORTED_ACTION,
12123
- recoverability: 'FATAL',
12124
- 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.`,
12125
- cause: {
12126
- trace: {
12127
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
12128
- address,
12129
- chain: chain.name,
12130
- bytecodeBytes: (code.length - 2) / 2,
12131
- bytecodePrefix: code.slice(0, 12)
12132
- }
12133
- }
12134
- });
12328
+ return true;
12135
12329
  }
12136
12330
 
12137
12331
  /**
@@ -12158,78 +12352,177 @@ function evmSigningData(burnIntent) {
12158
12352
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12159
12353
  }
12160
12354
 
12355
+ function resolveIntentChain(group, intent) {
12356
+ const sourceDomain = intent.spec.sourceDomain;
12357
+ const chain = group.chainsByDomain.get(sourceDomain);
12358
+ if (chain !== undefined) return chain;
12359
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
12360
+ }
12361
+ function normalizeSignatureResult(result) {
12362
+ if (typeof result === 'string') {
12363
+ return {
12364
+ signature: result,
12365
+ contractSigner: false
12366
+ };
12367
+ }
12368
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
12369
+ return {
12370
+ signature: result.signature,
12371
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
12372
+ };
12373
+ }
12374
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
12375
+ }
12376
+ function validateGroupIntents(intents) {
12377
+ evmSigningData(intents);
12378
+ }
12379
+ function collectChainsByDomain(group) {
12380
+ const chainsByDomain = new Map();
12381
+ for (const intent of group.intents){
12382
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
12383
+ }
12384
+ return chainsByDomain;
12385
+ }
12386
+ async function classifySignerTypes(group, chainsByDomain) {
12387
+ const { adapter, address } = group;
12388
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12389
+ // each consumer package bundles its own copy of the base class and the
12390
+ // `instanceof` identity check fails across package boundaries.
12391
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
12392
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12393
+ const signerTypes = await Promise.all([
12394
+ ...chainsByDomain
12395
+ ].map(async ([sourceDomain, sourceChain])=>{
12396
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
12397
+ return [
12398
+ sourceDomain,
12399
+ contractSigner
12400
+ ];
12401
+ }));
12402
+ return new Map(signerTypes);
12403
+ }
12404
+ function createSigningUnits(group, signerTypeByDomain) {
12405
+ const contractUnitsByDomain = new Map();
12406
+ let eoaUnit;
12407
+ for (const [index, intent] of group.intents.entries()){
12408
+ const sourceDomain = intent.spec.sourceDomain;
12409
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
12410
+ if (contractSigner) {
12411
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
12412
+ if (existingUnit === undefined) {
12413
+ contractUnitsByDomain.set(sourceDomain, {
12414
+ intents: [
12415
+ intent
12416
+ ],
12417
+ chain: resolveIntentChain(group, intent),
12418
+ contractSigner: true,
12419
+ firstIntentIndex: index
12420
+ });
12421
+ } else {
12422
+ existingUnit.intents.push(intent);
12423
+ }
12424
+ } else {
12425
+ eoaUnit ??= {
12426
+ intents: [],
12427
+ chain: resolveIntentChain(group, intent),
12428
+ contractSigner: false,
12429
+ firstIntentIndex: index
12430
+ };
12431
+ eoaUnit.intents.push(intent);
12432
+ }
12433
+ }
12434
+ const signingUnits = [
12435
+ ...contractUnitsByDomain.values()
12436
+ ];
12437
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
12438
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
12439
+ return signingUnits;
12440
+ }
12441
+ async function signUnit(group, unit) {
12442
+ const { adapter, address } = group;
12443
+ const firstIntent = unit.intents[0];
12444
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
12445
+ const operationContext = address === undefined ? {
12446
+ chain: unit.chain
12447
+ } : {
12448
+ chain: unit.chain,
12449
+ address
12450
+ };
12451
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12452
+ typedData,
12453
+ chain: unit.chain
12454
+ }, operationContext);
12455
+ const result = normalizeSignatureResult(await signRequest.execute());
12456
+ return {
12457
+ intents: unit.intents,
12458
+ signature: result.signature,
12459
+ contractSigner: result.contractSigner || unit.contractSigner
12460
+ };
12461
+ }
12462
+ async function signUnits(group, signingUnits) {
12463
+ const signedSets = [];
12464
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
12465
+ // in parallel, but one signer is asked for its chain-bound signatures in
12466
+ // source-intent order.
12467
+ for (const unit of signingUnits){
12468
+ signedSets.push(await signUnit(group, unit));
12469
+ }
12470
+ return signedSets;
12471
+ }
12161
12472
  /**
12162
- * Sign an EVM adapter group: batches all intents and produces a single
12163
- * EIP-712 ECDSA signature.
12473
+ * Sign an EVM adapter group.
12164
12474
  *
12165
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12166
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
12475
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
12476
+ * intents are grouped and signed per source chain because smart accounts
12477
+ * commonly include `chainId` in their replay-safe signature hash.
12478
+ * All returned entries can still be submitted together in one atomic Gateway
12479
+ * transfer request.
12167
12480
  *
12168
- * Before signing, asserts that the signer address is an EOA. Gateway
12169
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
12170
- * fallback), so signatures produced by smart-contract accounts (SCAs)
12171
- * cannot be verified. When an SCA is detected, a clear error is raised
12172
- * directing the caller to the delegate workflow (DEVX-2774).
12481
+ * Before signing, classifies the signer as an EOA or a contract account.
12482
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
12483
+ * signatures with ERC-1271, but it does not infer which one applies — the
12484
+ * transfer request has to declare it. The returned `contractSigner` flag
12485
+ * carries that decision through to `buildTransferRequestBody`.
12173
12486
  *
12174
12487
  * @param group - The adapter group containing the adapter, chain, and
12175
12488
  * burn intents to sign.
12176
- * @returns A signed set with the intents and the ECDSA signature.
12489
+ * @returns Signed entries with their intents, signatures, and Gateway signer
12490
+ * validation mode.
12491
+ * @throws KitError when an intent has no source-chain mapping or a signing
12492
+ * action returns an invalid signature shape.
12177
12493
  *
12178
12494
  * @example
12179
12495
  * ```typescript
12180
12496
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12181
12497
  *
12182
- * const signedSet = await signEvmIntentGroup({
12498
+ * const signedSets = await signEvmIntentGroup({
12183
12499
  * adapter: evmAdapter,
12184
12500
  * chain: ethereumChain,
12185
12501
  * intents: [burnIntent1, burnIntent2],
12502
+ * chainsByDomain: new Map([
12503
+ * [0, ethereumChain],
12504
+ * [6, baseChain],
12505
+ * ]),
12186
12506
  * address: '0x...',
12187
12507
  * })
12188
- * console.log(signedSet.signature)
12508
+ * console.log(signedSets)
12189
12509
  * ```
12190
12510
  */ async function signEvmIntentGroup(group) {
12191
- const { adapter, intents: groupIntents, chain, address } = group;
12192
- const operationContext = address === undefined ? {
12193
- chain
12194
- } : {
12195
- chain,
12196
- address
12197
- };
12198
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
12199
- // signer silently produces a signature over a wrapped hash that Gateway
12200
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
12201
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
12202
- // when we can detect bytecode at the signer address. See DEVX-2774.
12203
- //
12204
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12205
- // each consumer package bundles its own copy of the base class and the
12206
- // `instanceof` identity check fails across package boundaries.
12207
- //
12208
- // Empty string is defended against because assertSignerIsEoa would
12209
- // otherwise call eth_getCode('') on the RPC.
12210
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12211
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
12212
- await assertSignerIsEoa(adapter, address, chain);
12213
- }
12214
- const firstIntent = groupIntents[0];
12215
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
12216
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12217
- typedData,
12218
- chain
12219
- }, operationContext);
12220
- const sig = await signRequest.execute();
12221
- return {
12222
- intents: groupIntents,
12223
- signature: sig
12224
- };
12511
+ // Validate the collection before doing bytecode reads or asking a wallet
12512
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
12513
+ validateGroupIntents(group.intents);
12514
+ const chainsByDomain = collectChainsByDomain(group);
12515
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
12516
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
12517
+ return await signUnits(group, signingUnits);
12225
12518
  }
12226
12519
 
12227
12520
  /**
12228
12521
  * Add an EVM intent into the batched EVM group map.
12229
12522
  *
12230
12523
  * On EVM, all intents for the same adapter are batched into a single
12231
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
12232
- * operation.
12524
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12525
+ * while signing ERC-1271 intents separately on their source chains.
12233
12526
  *
12234
12527
  * @param intent - The burn intent to group.
12235
12528
  * @param alloc - The allocation that resolved to this intent.
@@ -12246,6 +12539,7 @@ function evmSigningData(burnIntent) {
12246
12539
  const existing = evmGroups.get(alloc.adapter);
12247
12540
  if (existing) {
12248
12541
  existing.intents.push(intent);
12542
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12249
12543
  } else {
12250
12544
  evmGroups.set(alloc.adapter, {
12251
12545
  adapter: alloc.adapter,
@@ -12253,6 +12547,12 @@ function evmSigningData(burnIntent) {
12253
12547
  intents: [
12254
12548
  intent
12255
12549
  ],
12550
+ chainsByDomain: new Map([
12551
+ [
12552
+ alloc.chain.gateway.domain,
12553
+ alloc.chain
12554
+ ]
12555
+ ]),
12256
12556
  address: alloc.sourceSigner
12257
12557
  });
12258
12558
  }
@@ -13778,7 +14078,8 @@ function throwNetworkMismatch(expected, actual) {
13778
14078
  };
13779
14079
  }
13780
14080
  /**
13781
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
14081
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
14082
+ * with every source chain retained by Gateway domain).
13782
14083
  *
13783
14084
  * @param intents - Burn intents from estimate response.
13784
14085
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16671,22 +16972,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16671
16972
  *
16672
16973
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16673
16974
  *
16975
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
16976
+ * Gateway to validate the signature with ERC-1271 (an offchain
16977
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
16978
+ * omitted for EOA signers so their payloads stay byte-identical.
16979
+ *
16674
16980
  * @param signedSets - Signed intent sets (intents + signature per signer).
16675
16981
  * @returns Array of transfer payloads for POST /v1/transfer.
16676
16982
  */ function buildTransferRequestBody(signedSets) {
16677
16983
  return signedSets.map((set)=>{
16678
16984
  const firstIntent = set.intents[0];
16985
+ const contractSigner = set.contractSigner === true ? {
16986
+ contractSigner: true
16987
+ } : {};
16679
16988
  if (set.intents.length === 1 && firstIntent) {
16680
16989
  return {
16681
16990
  burnIntent: serializeBurnIntent(firstIntent),
16682
- signature: set.signature
16991
+ signature: set.signature,
16992
+ ...contractSigner
16683
16993
  };
16684
16994
  }
16685
16995
  return {
16686
16996
  burnIntentSet: {
16687
16997
  intents: set.intents.map(serializeBurnIntent)
16688
16998
  },
16689
- signature: set.signature
16999
+ signature: set.signature,
17000
+ ...contractSigner
16690
17001
  };
16691
17002
  });
16692
17003
  }
@@ -17049,11 +17360,16 @@ const BPS_DIVISOR = 100_000n;
17049
17360
  return required;
17050
17361
  }
17051
17362
 
17363
+ function requireEvmChainsByDomain(group) {
17364
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
17365
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
17366
+ }
17052
17367
  /**
17053
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
17368
+ * Sign each adapter group: Solana one intent per signature, and EVM either
17369
+ * batched for EOAs or split by source chain for ERC-1271 signers.
17054
17370
  *
17055
17371
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17056
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17372
+ * @returns Promise of signed sets for buildTransferRequestBody.
17057
17373
  *
17058
17374
  * @example
17059
17375
  * ```typescript
@@ -17066,9 +17382,10 @@ const BPS_DIVISOR = 100_000n;
17066
17382
  if (group.chain.type === 'solana') {
17067
17383
  return signSolanaIntentGroup(group);
17068
17384
  }
17069
- return [
17070
- await signEvmIntentGroup(group)
17071
- ];
17385
+ return await signEvmIntentGroup({
17386
+ ...group,
17387
+ chainsByDomain: requireEvmChainsByDomain(group)
17388
+ });
17072
17389
  }));
17073
17390
  return nested.flat();
17074
17391
  }
@@ -17718,7 +18035,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17718
18035
  signedSetCount: signedSets.length,
17719
18036
  signatures: signedSets.map((s)=>({
17720
18037
  intentCount: s.intents.length,
17721
- signature: s.signature
18038
+ signature: s.signature,
18039
+ contractSigner: s.contractSigner === true
17722
18040
  }))
17723
18041
  }
17724
18042
  });
@@ -18173,8 +18491,7 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18173
18491
  chain
18174
18492
  };
18175
18493
  // Step 1: Quick check at latest block (no HTTP call)
18176
- const latestRequest = await adapter.prepareAction('gateway.v1.isDelegate', baseActionParams, operationContext);
18177
- const latestResult = await latestRequest.execute();
18494
+ const latestResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', baseActionParams, operationContext);
18178
18495
  if (String(latestResult).toLowerCase() !== 'true') {
18179
18496
  return 'none';
18180
18497
  }
@@ -18183,13 +18500,12 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18183
18500
  // Solana uses confirmed vs finalized commitment as a proxy for
18184
18501
  // Gateway finality. This is conservative — can only over-report
18185
18502
  // 'pending', never falsely report 'ready'.
18186
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
18187
- ...baseActionParams,
18188
- commitment: 'finalized'
18189
- }, operationContext);
18190
18503
  let finalizedResult;
18191
18504
  try {
18192
- finalizedResult = await finalizedRequest.execute();
18505
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
18506
+ ...baseActionParams,
18507
+ commitment: 'finalized'
18508
+ }, operationContext);
18193
18509
  } catch (error) {
18194
18510
  if (isBlockRangeError(error)) {
18195
18511
  return 'pending';
@@ -18200,10 +18516,6 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18200
18516
  }
18201
18517
  // EVM: use processedHeight from /v1/info
18202
18518
  const processedHeight = await getProcessedHeight(chain.isTestnet, chain.gateway.domain);
18203
- const finalizedRequest = await adapter.prepareAction('gateway.v1.isDelegate', {
18204
- ...baseActionParams,
18205
- blockNumber: processedHeight
18206
- }, operationContext);
18207
18519
  // If the RPC node lags Gateway's indexer view, the historical read at
18208
18520
  // processedHeight may throw a block-range error. This is safe to treat
18209
18521
  // as 'pending' because processedHeight comes from Gateway's /v1/info
@@ -18212,7 +18524,10 @@ function assertNotSelfDelegation(chain, signerAddress, delegateAddress, action)
18212
18524
  // Re-throw structural errors to avoid masking real bugs.
18213
18525
  let finalizedResult;
18214
18526
  try {
18215
- finalizedResult = await finalizedRequest.execute();
18527
+ finalizedResult = await executeAdapterReadAction(adapter, 'gateway.v1.isDelegate', {
18528
+ ...baseActionParams,
18529
+ blockNumber: processedHeight
18530
+ }, operationContext);
18216
18531
  } catch (error) {
18217
18532
  if (isBlockRangeError(error)) {
18218
18533
  return 'pending';
@@ -18273,8 +18588,8 @@ function parseAmountSafe(amount) {
18273
18588
  chain
18274
18589
  };
18275
18590
  const [withdrawingRaw, withdrawalBlockRaw] = await Promise.all([
18276
- adapter.prepareAction('gateway.v1.withdrawingBalance', readParams, operationContext).then(async (req)=>req.execute()),
18277
- adapter.prepareAction('gateway.v1.withdrawalBlock', readParams, operationContext).then(async (req)=>req.execute())
18591
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', readParams, operationContext),
18592
+ executeAdapterReadAction(adapter, 'gateway.v1.withdrawalBlock', readParams, operationContext)
18278
18593
  ]);
18279
18594
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
18280
18595
  const withdrawalBlockValue = safeBigInt(String(withdrawalBlockRaw), 'withdrawalBlock');
@@ -18314,12 +18629,11 @@ function parseAmountSafe(amount) {
18314
18629
  const tokenAddress = getTokenAddress(chain, params.token);
18315
18630
  // Read the pending balance before withdrawing — the contract resets it to 0
18316
18631
  // after withdraw() executes, so this is the only way to capture the amount.
18317
- const withdrawingBalanceReq = await adapter.prepareAction('gateway.v1.withdrawingBalance', {
18632
+ const withdrawingRaw = await executeAdapterReadAction(adapter, 'gateway.v1.withdrawingBalance', {
18318
18633
  token: tokenAddress,
18319
18634
  depositor: signerAddress,
18320
18635
  chain
18321
18636
  }, operationContext);
18322
- const withdrawingRaw = await withdrawingBalanceReq.execute();
18323
18637
  const withdrawingValue = safeBigInt(String(withdrawingRaw), 'withdrawingBalance');
18324
18638
  if (withdrawingValue === 0n) {
18325
18639
  throw new KitError({