@circle-fin/app-kit 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -664,11 +664,6 @@ class KitError extends Error {
664
664
  name: 'INPUT_UNSUPPORTED_TOKEN',
665
665
  type: 'INPUT'
666
666
  },
667
- /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
668
- code: 1008,
669
- name: 'INPUT_UNSUPPORTED_ACTION',
670
- type: 'INPUT'
671
- },
672
667
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
673
668
  code: 1009,
674
669
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -2325,6 +2320,8 @@ class KitError extends Error {
2325
2320
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2326
2321
  Blockchain["XDC"] = "XDC";
2327
2322
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2323
+ Blockchain["X_Layer"] = "X_Layer";
2324
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2328
2325
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2329
2326
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2330
2327
  })(Blockchain || (Blockchain = {}));
@@ -2378,6 +2375,7 @@ var BridgeChain;
2378
2375
  BridgeChain["Unichain"] = "Unichain";
2379
2376
  BridgeChain["World_Chain"] = "World_Chain";
2380
2377
  BridgeChain["XDC"] = "XDC";
2378
+ BridgeChain["X_Layer"] = "X_Layer";
2381
2379
  // Testnet chains with CCTPv2 support
2382
2380
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2383
2381
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2403,6 +2401,7 @@ var BridgeChain;
2403
2401
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2404
2402
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2405
2403
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2404
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2406
2405
  })(BridgeChain || (BridgeChain = {}));
2407
2406
  var UnifiedBalanceChain;
2408
2407
  (function(UnifiedBalanceChain) {
@@ -4950,7 +4949,8 @@ var EarnChain;
4950
4949
  isTestnet: true,
4951
4950
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4952
4951
  rpcEndpoints: [
4953
- 'https://rpc-amoy.polygon.technology'
4952
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4953
+ 'https://polygon-amoy.drpc.org'
4954
4954
  ],
4955
4955
  eurcAddress: null,
4956
4956
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5815,6 +5815,104 @@ var EarnChain;
5815
5815
  }
5816
5816
  });
5817
5817
 
5818
+ /**
5819
+ * X Layer Mainnet chain definition
5820
+ * @remarks
5821
+ * This represents the official production network for the X Layer blockchain.
5822
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5823
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5824
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5825
+ */ const XLayer = defineChain({
5826
+ type: 'evm',
5827
+ chain: Blockchain.X_Layer,
5828
+ name: 'X Layer',
5829
+ title: 'X Layer Mainnet',
5830
+ nativeCurrency: {
5831
+ name: 'OKB',
5832
+ symbol: 'OKB',
5833
+ decimals: 18
5834
+ },
5835
+ chainId: 196,
5836
+ isTestnet: false,
5837
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5838
+ rpcEndpoints: [
5839
+ 'https://xlayerrpc.okx.com'
5840
+ ],
5841
+ eurcAddress: null,
5842
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5843
+ usdtAddress: null,
5844
+ cctp: {
5845
+ domain: 37,
5846
+ contracts: {
5847
+ v2: {
5848
+ type: 'split',
5849
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5850
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5851
+ confirmations: 65,
5852
+ fastConfirmations: 1
5853
+ }
5854
+ },
5855
+ forwarderSupported: {
5856
+ source: false,
5857
+ destination: false
5858
+ }
5859
+ },
5860
+ kitContracts: {
5861
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5862
+ }
5863
+ });
5864
+
5865
+ /**
5866
+ * X Layer Testnet chain definition
5867
+ * @remarks
5868
+ * This represents the official test network for the X Layer blockchain.
5869
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5870
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5871
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5872
+ */ const XLayerTestnet = defineChain({
5873
+ type: 'evm',
5874
+ chain: Blockchain.X_Layer_Testnet,
5875
+ name: 'X Layer Testnet',
5876
+ title: 'X Layer Testnet',
5877
+ nativeCurrency: {
5878
+ name: 'OKB',
5879
+ symbol: 'OKB',
5880
+ decimals: 18
5881
+ },
5882
+ chainId: 1952,
5883
+ isTestnet: true,
5884
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5885
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5886
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5887
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5888
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5889
+ rpcEndpoints: [
5890
+ 'https://testrpc.xlayer.tech'
5891
+ ],
5892
+ eurcAddress: null,
5893
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5894
+ usdtAddress: null,
5895
+ cctp: {
5896
+ domain: 37,
5897
+ contracts: {
5898
+ v2: {
5899
+ type: 'split',
5900
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5901
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5902
+ confirmations: 65,
5903
+ fastConfirmations: 1
5904
+ }
5905
+ },
5906
+ forwarderSupported: {
5907
+ source: false,
5908
+ destination: false
5909
+ }
5910
+ },
5911
+ kitContracts: {
5912
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5913
+ }
5914
+ });
5915
+
5818
5916
  /**
5819
5917
  * ZKSync Era Mainnet chain definition
5820
5918
  * @remarks
@@ -5934,6 +6032,8 @@ var Chains = {
5934
6032
  WorldChainSepolia: WorldChainSepolia,
5935
6033
  XDC: XDC,
5936
6034
  XDCApothem: XDCApothem,
6035
+ XLayer: XLayer,
6036
+ XLayerTestnet: XLayerTestnet,
5937
6037
  ZKSyncEra: ZKSyncEra,
5938
6038
  ZKSyncEraSepolia: ZKSyncEraSepolia
5939
6039
  };
@@ -7886,6 +7986,7 @@ function parseOrThrow(value, schema, context) {
7886
7986
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7887
7987
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7888
7988
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7989
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7889
7990
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7890
7991
  // =========================================================================
7891
7992
  // Testnets (alphabetically sorted)
@@ -7920,6 +8021,7 @@ function parseOrThrow(value, schema, context) {
7920
8021
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7921
8022
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7922
8023
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8024
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7923
8025
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7924
8026
  }
7925
8027
  };
@@ -8785,7 +8887,7 @@ function parseOrThrow(value, schema, context) {
8785
8887
  }
8786
8888
 
8787
8889
  var name = "@circle-fin/unified-balance-kit";
8788
- var version = "1.3.1";
8890
+ var version = "1.4.0";
8789
8891
  var pkg = {
8790
8892
  name: name,
8791
8893
  version: version};
@@ -12066,72 +12168,55 @@ function evmSigningData(burnIntent) {
12066
12168
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12067
12169
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12068
12170
  * signatures, so for Gateway's purposes a 7702-delegated address is
12069
- * an EOA, not an SCA.
12171
+ * an EOA, not a contract signer.
12070
12172
  *
12071
12173
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12072
12174
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12073
12175
  /**
12074
- * Assert that `address` on `chain` can sign Gateway burn intents.
12176
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
12177
+ * rather than as an EOA.
12075
12178
  *
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.
12179
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
12180
+ * check for EOAs, and — for requests that carry `contractSigner: true`
12181
+ * an offchain `isValidSignature` simulation against the signing contract
12182
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
12183
+ * declare it. This detects the contract case from on-chain bytecode.
12083
12184
  *
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.
12185
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
12186
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
12187
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
12188
+ * stays correct for them.
12087
12189
  *
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.
12093
- *
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.
12190
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
12191
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
12192
+ * genuine contract signer misreported this way is rejected by Gateway with
12193
+ * an invalid-signature error rather than silently mis-attested.
12097
12194
  *
12098
12195
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12099
- * @param address - Signer address to validate.
12196
+ * @param address - Signer address to classify.
12100
12197
  * @param chain - EVM chain where the signer lives.
12101
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
12198
+ * @returns `true` when the signer is a contract account and the transfer
12199
+ * request must set `contractSigner: true`; `false` otherwise.
12102
12200
  *
12103
12201
  * @example
12104
12202
  * ```typescript
12105
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12203
+ * import { isContractSigner } from '@core/adapter-evm'
12106
12204
  * import { Ethereum } from '@core/chains'
12107
12205
  *
12108
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12206
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12109
12207
  * ```
12110
- */ async function assertSignerIsEoa(adapter, address, chain) {
12208
+ */ async function isContractSigner(adapter, address, chain) {
12111
12209
  let code;
12112
12210
  try {
12113
12211
  code = await adapter.readBytecode(address, chain);
12114
12212
  } catch (err) {
12115
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12116
- return;
12213
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12214
+ return false;
12117
12215
  }
12118
12216
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12119
- return;
12217
+ return false;
12120
12218
  }
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
- });
12219
+ return true;
12135
12220
  }
12136
12221
 
12137
12222
  /**
@@ -12158,78 +12243,177 @@ function evmSigningData(burnIntent) {
12158
12243
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12159
12244
  }
12160
12245
 
12246
+ function resolveIntentChain(group, intent) {
12247
+ const sourceDomain = intent.spec.sourceDomain;
12248
+ const chain = group.chainsByDomain.get(sourceDomain);
12249
+ if (chain !== undefined) return chain;
12250
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
12251
+ }
12252
+ function normalizeSignatureResult(result) {
12253
+ if (typeof result === 'string') {
12254
+ return {
12255
+ signature: result,
12256
+ contractSigner: false
12257
+ };
12258
+ }
12259
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
12260
+ return {
12261
+ signature: result.signature,
12262
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
12263
+ };
12264
+ }
12265
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
12266
+ }
12267
+ function validateGroupIntents(intents) {
12268
+ evmSigningData(intents);
12269
+ }
12270
+ function collectChainsByDomain(group) {
12271
+ const chainsByDomain = new Map();
12272
+ for (const intent of group.intents){
12273
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
12274
+ }
12275
+ return chainsByDomain;
12276
+ }
12277
+ async function classifySignerTypes(group, chainsByDomain) {
12278
+ const { adapter, address } = group;
12279
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12280
+ // each consumer package bundles its own copy of the base class and the
12281
+ // `instanceof` identity check fails across package boundaries.
12282
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
12283
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12284
+ const signerTypes = await Promise.all([
12285
+ ...chainsByDomain
12286
+ ].map(async ([sourceDomain, sourceChain])=>{
12287
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
12288
+ return [
12289
+ sourceDomain,
12290
+ contractSigner
12291
+ ];
12292
+ }));
12293
+ return new Map(signerTypes);
12294
+ }
12295
+ function createSigningUnits(group, signerTypeByDomain) {
12296
+ const contractUnitsByDomain = new Map();
12297
+ let eoaUnit;
12298
+ for (const [index, intent] of group.intents.entries()){
12299
+ const sourceDomain = intent.spec.sourceDomain;
12300
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
12301
+ if (contractSigner) {
12302
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
12303
+ if (existingUnit === undefined) {
12304
+ contractUnitsByDomain.set(sourceDomain, {
12305
+ intents: [
12306
+ intent
12307
+ ],
12308
+ chain: resolveIntentChain(group, intent),
12309
+ contractSigner: true,
12310
+ firstIntentIndex: index
12311
+ });
12312
+ } else {
12313
+ existingUnit.intents.push(intent);
12314
+ }
12315
+ } else {
12316
+ eoaUnit ??= {
12317
+ intents: [],
12318
+ chain: resolveIntentChain(group, intent),
12319
+ contractSigner: false,
12320
+ firstIntentIndex: index
12321
+ };
12322
+ eoaUnit.intents.push(intent);
12323
+ }
12324
+ }
12325
+ const signingUnits = [
12326
+ ...contractUnitsByDomain.values()
12327
+ ];
12328
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
12329
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
12330
+ return signingUnits;
12331
+ }
12332
+ async function signUnit(group, unit) {
12333
+ const { adapter, address } = group;
12334
+ const firstIntent = unit.intents[0];
12335
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
12336
+ const operationContext = address === undefined ? {
12337
+ chain: unit.chain
12338
+ } : {
12339
+ chain: unit.chain,
12340
+ address
12341
+ };
12342
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12343
+ typedData,
12344
+ chain: unit.chain
12345
+ }, operationContext);
12346
+ const result = normalizeSignatureResult(await signRequest.execute());
12347
+ return {
12348
+ intents: unit.intents,
12349
+ signature: result.signature,
12350
+ contractSigner: result.contractSigner || unit.contractSigner
12351
+ };
12352
+ }
12353
+ async function signUnits(group, signingUnits) {
12354
+ const signedSets = [];
12355
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
12356
+ // in parallel, but one signer is asked for its chain-bound signatures in
12357
+ // source-intent order.
12358
+ for (const unit of signingUnits){
12359
+ signedSets.push(await signUnit(group, unit));
12360
+ }
12361
+ return signedSets;
12362
+ }
12161
12363
  /**
12162
- * Sign an EVM adapter group: batches all intents and produces a single
12163
- * EIP-712 ECDSA signature.
12364
+ * Sign an EVM adapter group.
12164
12365
  *
12165
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12166
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
12366
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
12367
+ * intents are grouped and signed per source chain because smart accounts
12368
+ * commonly include `chainId` in their replay-safe signature hash.
12369
+ * All returned entries can still be submitted together in one atomic Gateway
12370
+ * transfer request.
12167
12371
  *
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).
12372
+ * Before signing, classifies the signer as an EOA or a contract account.
12373
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
12374
+ * signatures with ERC-1271, but it does not infer which one applies — the
12375
+ * transfer request has to declare it. The returned `contractSigner` flag
12376
+ * carries that decision through to `buildTransferRequestBody`.
12173
12377
  *
12174
12378
  * @param group - The adapter group containing the adapter, chain, and
12175
12379
  * burn intents to sign.
12176
- * @returns A signed set with the intents and the ECDSA signature.
12380
+ * @returns Signed entries with their intents, signatures, and Gateway signer
12381
+ * validation mode.
12382
+ * @throws KitError when an intent has no source-chain mapping or a signing
12383
+ * action returns an invalid signature shape.
12177
12384
  *
12178
12385
  * @example
12179
12386
  * ```typescript
12180
12387
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12181
12388
  *
12182
- * const signedSet = await signEvmIntentGroup({
12389
+ * const signedSets = await signEvmIntentGroup({
12183
12390
  * adapter: evmAdapter,
12184
12391
  * chain: ethereumChain,
12185
12392
  * intents: [burnIntent1, burnIntent2],
12393
+ * chainsByDomain: new Map([
12394
+ * [0, ethereumChain],
12395
+ * [6, baseChain],
12396
+ * ]),
12186
12397
  * address: '0x...',
12187
12398
  * })
12188
- * console.log(signedSet.signature)
12399
+ * console.log(signedSets)
12189
12400
  * ```
12190
12401
  */ 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
- };
12402
+ // Validate the collection before doing bytecode reads or asking a wallet
12403
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
12404
+ validateGroupIntents(group.intents);
12405
+ const chainsByDomain = collectChainsByDomain(group);
12406
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
12407
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
12408
+ return await signUnits(group, signingUnits);
12225
12409
  }
12226
12410
 
12227
12411
  /**
12228
12412
  * Add an EVM intent into the batched EVM group map.
12229
12413
  *
12230
12414
  * 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.
12415
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12416
+ * while signing ERC-1271 intents separately on their source chains.
12233
12417
  *
12234
12418
  * @param intent - The burn intent to group.
12235
12419
  * @param alloc - The allocation that resolved to this intent.
@@ -12246,6 +12430,7 @@ function evmSigningData(burnIntent) {
12246
12430
  const existing = evmGroups.get(alloc.adapter);
12247
12431
  if (existing) {
12248
12432
  existing.intents.push(intent);
12433
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12249
12434
  } else {
12250
12435
  evmGroups.set(alloc.adapter, {
12251
12436
  adapter: alloc.adapter,
@@ -12253,6 +12438,12 @@ function evmSigningData(burnIntent) {
12253
12438
  intents: [
12254
12439
  intent
12255
12440
  ],
12441
+ chainsByDomain: new Map([
12442
+ [
12443
+ alloc.chain.gateway.domain,
12444
+ alloc.chain
12445
+ ]
12446
+ ]),
12256
12447
  address: alloc.sourceSigner
12257
12448
  });
12258
12449
  }
@@ -13778,7 +13969,8 @@ function throwNetworkMismatch(expected, actual) {
13778
13969
  };
13779
13970
  }
13780
13971
  /**
13781
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
13972
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
13973
+ * with every source chain retained by Gateway domain).
13782
13974
  *
13783
13975
  * @param intents - Burn intents from estimate response.
13784
13976
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16671,22 +16863,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16671
16863
  *
16672
16864
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16673
16865
  *
16866
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
16867
+ * Gateway to validate the signature with ERC-1271 (an offchain
16868
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
16869
+ * omitted for EOA signers so their payloads stay byte-identical.
16870
+ *
16674
16871
  * @param signedSets - Signed intent sets (intents + signature per signer).
16675
16872
  * @returns Array of transfer payloads for POST /v1/transfer.
16676
16873
  */ function buildTransferRequestBody(signedSets) {
16677
16874
  return signedSets.map((set)=>{
16678
16875
  const firstIntent = set.intents[0];
16876
+ const contractSigner = set.contractSigner === true ? {
16877
+ contractSigner: true
16878
+ } : {};
16679
16879
  if (set.intents.length === 1 && firstIntent) {
16680
16880
  return {
16681
16881
  burnIntent: serializeBurnIntent(firstIntent),
16682
- signature: set.signature
16882
+ signature: set.signature,
16883
+ ...contractSigner
16683
16884
  };
16684
16885
  }
16685
16886
  return {
16686
16887
  burnIntentSet: {
16687
16888
  intents: set.intents.map(serializeBurnIntent)
16688
16889
  },
16689
- signature: set.signature
16890
+ signature: set.signature,
16891
+ ...contractSigner
16690
16892
  };
16691
16893
  });
16692
16894
  }
@@ -17049,11 +17251,16 @@ const BPS_DIVISOR = 100_000n;
17049
17251
  return required;
17050
17252
  }
17051
17253
 
17254
+ function requireEvmChainsByDomain(group) {
17255
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
17256
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
17257
+ }
17052
17258
  /**
17053
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
17259
+ * Sign each adapter group: Solana one intent per signature, and EVM either
17260
+ * batched for EOAs or split by source chain for ERC-1271 signers.
17054
17261
  *
17055
17262
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17056
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17263
+ * @returns Promise of signed sets for buildTransferRequestBody.
17057
17264
  *
17058
17265
  * @example
17059
17266
  * ```typescript
@@ -17066,9 +17273,10 @@ const BPS_DIVISOR = 100_000n;
17066
17273
  if (group.chain.type === 'solana') {
17067
17274
  return signSolanaIntentGroup(group);
17068
17275
  }
17069
- return [
17070
- await signEvmIntentGroup(group)
17071
- ];
17276
+ return await signEvmIntentGroup({
17277
+ ...group,
17278
+ chainsByDomain: requireEvmChainsByDomain(group)
17279
+ });
17072
17280
  }));
17073
17281
  return nested.flat();
17074
17282
  }
@@ -17718,7 +17926,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17718
17926
  signedSetCount: signedSets.length,
17719
17927
  signatures: signedSets.map((s)=>({
17720
17928
  intentCount: s.intents.length,
17721
- signature: s.signature
17929
+ signature: s.signature,
17930
+ contractSigner: s.contractSigner === true
17722
17931
  }))
17723
17932
  }
17724
17933
  });
@@ -739,6 +739,8 @@ declare enum Blockchain {
739
739
  World_Chain_Sepolia = "World_Chain_Sepolia",
740
740
  XDC = "XDC",
741
741
  XDC_Apothem = "XDC_Apothem",
742
+ X_Layer = "X_Layer",
743
+ X_Layer_Testnet = "X_Layer_Testnet",
742
744
  ZKSync_Era = "ZKSync_Era",
743
745
  ZKSync_Sepolia = "ZKSync_Sepolia"
744
746
  }
@@ -739,6 +739,8 @@ declare enum Blockchain {
739
739
  World_Chain_Sepolia = "World_Chain_Sepolia",
740
740
  XDC = "XDC",
741
741
  XDC_Apothem = "XDC_Apothem",
742
+ X_Layer = "X_Layer",
743
+ X_Layer_Testnet = "X_Layer_Testnet",
742
744
  ZKSync_Era = "ZKSync_Era",
743
745
  ZKSync_Sepolia = "ZKSync_Sepolia"
744
746
  }
@@ -739,6 +739,8 @@ declare enum Blockchain {
739
739
  World_Chain_Sepolia = "World_Chain_Sepolia",
740
740
  XDC = "XDC",
741
741
  XDC_Apothem = "XDC_Apothem",
742
+ X_Layer = "X_Layer",
743
+ X_Layer_Testnet = "X_Layer_Testnet",
742
744
  ZKSync_Era = "ZKSync_Era",
743
745
  ZKSync_Sepolia = "ZKSync_Sepolia"
744
746
  }