@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.
@@ -658,11 +658,6 @@ class KitError extends Error {
658
658
  name: 'INPUT_UNSUPPORTED_TOKEN',
659
659
  type: 'INPUT'
660
660
  },
661
- /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
662
- code: 1008,
663
- name: 'INPUT_UNSUPPORTED_ACTION',
664
- type: 'INPUT'
665
- },
666
661
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
667
662
  code: 1009,
668
663
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -2319,6 +2314,8 @@ class KitError extends Error {
2319
2314
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2320
2315
  Blockchain["XDC"] = "XDC";
2321
2316
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2317
+ Blockchain["X_Layer"] = "X_Layer";
2318
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2322
2319
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2323
2320
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2324
2321
  })(Blockchain || (Blockchain = {}));
@@ -2372,6 +2369,7 @@ var BridgeChain;
2372
2369
  BridgeChain["Unichain"] = "Unichain";
2373
2370
  BridgeChain["World_Chain"] = "World_Chain";
2374
2371
  BridgeChain["XDC"] = "XDC";
2372
+ BridgeChain["X_Layer"] = "X_Layer";
2375
2373
  // Testnet chains with CCTPv2 support
2376
2374
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2377
2375
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2397,6 +2395,7 @@ var BridgeChain;
2397
2395
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2398
2396
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2399
2397
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2398
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2400
2399
  })(BridgeChain || (BridgeChain = {}));
2401
2400
  var UnifiedBalanceChain;
2402
2401
  (function(UnifiedBalanceChain) {
@@ -4944,7 +4943,8 @@ var EarnChain;
4944
4943
  isTestnet: true,
4945
4944
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4946
4945
  rpcEndpoints: [
4947
- 'https://rpc-amoy.polygon.technology'
4946
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4947
+ 'https://polygon-amoy.drpc.org'
4948
4948
  ],
4949
4949
  eurcAddress: null,
4950
4950
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5809,6 +5809,104 @@ var EarnChain;
5809
5809
  }
5810
5810
  });
5811
5811
 
5812
+ /**
5813
+ * X Layer Mainnet chain definition
5814
+ * @remarks
5815
+ * This represents the official production network for the X Layer blockchain.
5816
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5817
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5818
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5819
+ */ const XLayer = defineChain({
5820
+ type: 'evm',
5821
+ chain: Blockchain.X_Layer,
5822
+ name: 'X Layer',
5823
+ title: 'X Layer Mainnet',
5824
+ nativeCurrency: {
5825
+ name: 'OKB',
5826
+ symbol: 'OKB',
5827
+ decimals: 18
5828
+ },
5829
+ chainId: 196,
5830
+ isTestnet: false,
5831
+ explorerUrl: 'https://www.oklink.com/xlayer/tx/{hash}',
5832
+ rpcEndpoints: [
5833
+ 'https://xlayerrpc.okx.com'
5834
+ ],
5835
+ eurcAddress: null,
5836
+ usdcAddress: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
5837
+ usdtAddress: null,
5838
+ cctp: {
5839
+ domain: 37,
5840
+ contracts: {
5841
+ v2: {
5842
+ type: 'split',
5843
+ tokenMessenger: '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d',
5844
+ messageTransmitter: '0x81D40F21F12A8F0E3252Bccb954D722d4c464B64',
5845
+ confirmations: 65,
5846
+ fastConfirmations: 1
5847
+ }
5848
+ },
5849
+ forwarderSupported: {
5850
+ source: false,
5851
+ destination: false
5852
+ }
5853
+ },
5854
+ kitContracts: {
5855
+ bridge: BRIDGE_CONTRACT_EVM_MAINNET
5856
+ }
5857
+ });
5858
+
5859
+ /**
5860
+ * X Layer Testnet chain definition
5861
+ * @remarks
5862
+ * This represents the official test network for the X Layer blockchain.
5863
+ * X Layer is an EVM-compatible OP Stack Layer-2 blockchain built by OKX,
5864
+ * using OKB as its native gas token. (Migrated from Polygon zkEVM/CDK to the
5865
+ * OP Stack on 2025-10-27; older docs describing it as zkEVM are obsolete.)
5866
+ */ const XLayerTestnet = defineChain({
5867
+ type: 'evm',
5868
+ chain: Blockchain.X_Layer_Testnet,
5869
+ name: 'X Layer Testnet',
5870
+ title: 'X Layer Testnet',
5871
+ nativeCurrency: {
5872
+ name: 'OKB',
5873
+ symbol: 'OKB',
5874
+ decimals: 18
5875
+ },
5876
+ chainId: 1952,
5877
+ isTestnet: true,
5878
+ // Deliberately not oklink.com (used for mainnet): viem's bundled OKLink
5879
+ // testnet URL targets the deprecated pre-rebrand chain ID 195, not this
5880
+ // chain's ID (1952). Verified against the internal chain-expansion-scripts
5881
+ // config (`v2config.sandbox.yml`) — do not "normalize" this to match mainnet.
5882
+ explorerUrl: 'https://web3.okx.com/explorer/x-layer-testnet/tx/{hash}',
5883
+ rpcEndpoints: [
5884
+ 'https://testrpc.xlayer.tech'
5885
+ ],
5886
+ eurcAddress: null,
5887
+ usdcAddress: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
5888
+ usdtAddress: null,
5889
+ cctp: {
5890
+ domain: 37,
5891
+ contracts: {
5892
+ v2: {
5893
+ type: 'split',
5894
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
5895
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
5896
+ confirmations: 65,
5897
+ fastConfirmations: 1
5898
+ }
5899
+ },
5900
+ forwarderSupported: {
5901
+ source: false,
5902
+ destination: false
5903
+ }
5904
+ },
5905
+ kitContracts: {
5906
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET
5907
+ }
5908
+ });
5909
+
5812
5910
  /**
5813
5911
  * ZKSync Era Mainnet chain definition
5814
5912
  * @remarks
@@ -5928,6 +6026,8 @@ var Chains = /*#__PURE__*/Object.freeze({
5928
6026
  WorldChainSepolia: WorldChainSepolia,
5929
6027
  XDC: XDC,
5930
6028
  XDCApothem: XDCApothem,
6029
+ XLayer: XLayer,
6030
+ XLayerTestnet: XLayerTestnet,
5931
6031
  ZKSyncEra: ZKSyncEra,
5932
6032
  ZKSyncEraSepolia: ZKSyncEraSepolia
5933
6033
  });
@@ -7880,6 +7980,7 @@ function parseOrThrow(value, schema, context) {
7880
7980
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7881
7981
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7882
7982
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7983
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7883
7984
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7884
7985
  // =========================================================================
7885
7986
  // Testnets (alphabetically sorted)
@@ -7914,6 +8015,7 @@ function parseOrThrow(value, schema, context) {
7914
8015
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7915
8016
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7916
8017
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8018
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7917
8019
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7918
8020
  }
7919
8021
  };
@@ -8779,7 +8881,7 @@ function parseOrThrow(value, schema, context) {
8779
8881
  }
8780
8882
 
8781
8883
  var name = "@circle-fin/unified-balance-kit";
8782
- var version = "1.3.1";
8884
+ var version = "1.4.0";
8783
8885
  var pkg = {
8784
8886
  name: name,
8785
8887
  version: version};
@@ -12060,72 +12162,55 @@ function evmSigningData(burnIntent) {
12060
12162
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12061
12163
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12062
12164
  * signatures, so for Gateway's purposes a 7702-delegated address is
12063
- * an EOA, not an SCA.
12165
+ * an EOA, not a contract signer.
12064
12166
  *
12065
12167
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12066
12168
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12067
12169
  /**
12068
- * Assert that `address` on `chain` can sign Gateway burn intents.
12170
+ * Determine whether `address` on `chain` signs as a contract (ERC-1271)
12171
+ * rather than as an EOA.
12069
12172
  *
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.
12173
+ * Gateway validates burn-intent signatures two ways: a static `ecrecover`
12174
+ * check for EOAs, and — for requests that carry `contractSigner: true`
12175
+ * an offchain `isValidSignature` simulation against the signing contract
12176
+ * (ERC-1271). Gateway does not infer which one to use, so the caller must
12177
+ * declare it. This detects the contract case from on-chain bytecode.
12077
12178
  *
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.
12179
+ * EIP-7702-delegated EOAs are treated as EOAs: they expose non-empty
12180
+ * bytecode (`0xef0100<delegate>`) but the underlying secp256k1 key still
12181
+ * produces `ecrecover`-verifiable signatures, so the cheaper EOA path
12182
+ * stays correct for them.
12081
12183
  *
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.
12087
- *
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.
12184
+ * If bytecode cannot be read (RPC failure, etc.) the address is reported
12185
+ * as an EOA and a warning is logged so the fallback is diagnosable. A
12186
+ * genuine contract signer misreported this way is rejected by Gateway with
12187
+ * an invalid-signature error rather than silently mis-attested.
12091
12188
  *
12092
12189
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12093
- * @param address - Signer address to validate.
12190
+ * @param address - Signer address to classify.
12094
12191
  * @param chain - EVM chain where the signer lives.
12095
- * @throws {KitError} INPUT_UNSUPPORTED_ACTION when `address` is an SCA.
12192
+ * @returns `true` when the signer is a contract account and the transfer
12193
+ * request must set `contractSigner: true`; `false` otherwise.
12096
12194
  *
12097
12195
  * @example
12098
12196
  * ```typescript
12099
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12197
+ * import { isContractSigner } from '@core/adapter-evm'
12100
12198
  * import { Ethereum } from '@core/chains'
12101
12199
  *
12102
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12200
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12103
12201
  * ```
12104
- */ async function assertSignerIsEoa(adapter, address, chain) {
12202
+ */ async function isContractSigner(adapter, address, chain) {
12105
12203
  let code;
12106
12204
  try {
12107
12205
  code = await adapter.readBytecode(address, chain);
12108
12206
  } catch (err) {
12109
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12110
- return;
12207
+ console.warn(`[gateway] isContractSigner defaulting to EOA (readBytecode failed ` + `for ${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12208
+ return false;
12111
12209
  }
12112
12210
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12113
- return;
12211
+ return false;
12114
12212
  }
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
- });
12213
+ return true;
12129
12214
  }
12130
12215
 
12131
12216
  /**
@@ -12152,78 +12237,177 @@ function evmSigningData(burnIntent) {
12152
12237
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12153
12238
  }
12154
12239
 
12240
+ function resolveIntentChain(group, intent) {
12241
+ const sourceDomain = intent.spec.sourceDomain;
12242
+ const chain = group.chainsByDomain.get(sourceDomain);
12243
+ if (chain !== undefined) return chain;
12244
+ throw createValidationFailedError$1('intent.spec.sourceDomain', sourceDomain, `No source chain found for Gateway domain ${String(sourceDomain)}`);
12245
+ }
12246
+ function normalizeSignatureResult(result) {
12247
+ if (typeof result === 'string') {
12248
+ return {
12249
+ signature: result,
12250
+ contractSigner: false
12251
+ };
12252
+ }
12253
+ if (typeof result === 'object' && result !== null && 'signature' in result && typeof result.signature === 'string') {
12254
+ return {
12255
+ signature: result.signature,
12256
+ contractSigner: 'contractSigner' in result && result.contractSigner === true
12257
+ };
12258
+ }
12259
+ throw createValidationFailedError$1('signature', result, 'must be a signature string or an object containing a signature string');
12260
+ }
12261
+ function validateGroupIntents(intents) {
12262
+ evmSigningData(intents);
12263
+ }
12264
+ function collectChainsByDomain(group) {
12265
+ const chainsByDomain = new Map();
12266
+ for (const intent of group.intents){
12267
+ chainsByDomain.set(intent.spec.sourceDomain, resolveIntentChain(group, intent));
12268
+ }
12269
+ return chainsByDomain;
12270
+ }
12271
+ async function classifySignerTypes(group, chainsByDomain) {
12272
+ const { adapter, address } = group;
12273
+ // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12274
+ // each consumer package bundles its own copy of the base class and the
12275
+ // `instanceof` identity check fails across package boundaries.
12276
+ // Empty strings are rejected to avoid calling eth_getCode('') on the RPC.
12277
+ const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12278
+ const signerTypes = await Promise.all([
12279
+ ...chainsByDomain
12280
+ ].map(async ([sourceDomain, sourceChain])=>{
12281
+ const contractSigner = hasResolvedSigner && sourceChain.type === 'evm' && isEvmAdapterLike(adapter) ? await isContractSigner(adapter, address, sourceChain) : false;
12282
+ return [
12283
+ sourceDomain,
12284
+ contractSigner
12285
+ ];
12286
+ }));
12287
+ return new Map(signerTypes);
12288
+ }
12289
+ function createSigningUnits(group, signerTypeByDomain) {
12290
+ const contractUnitsByDomain = new Map();
12291
+ let eoaUnit;
12292
+ for (const [index, intent] of group.intents.entries()){
12293
+ const sourceDomain = intent.spec.sourceDomain;
12294
+ const contractSigner = signerTypeByDomain.get(sourceDomain) ?? false;
12295
+ if (contractSigner) {
12296
+ const existingUnit = contractUnitsByDomain.get(sourceDomain);
12297
+ if (existingUnit === undefined) {
12298
+ contractUnitsByDomain.set(sourceDomain, {
12299
+ intents: [
12300
+ intent
12301
+ ],
12302
+ chain: resolveIntentChain(group, intent),
12303
+ contractSigner: true,
12304
+ firstIntentIndex: index
12305
+ });
12306
+ } else {
12307
+ existingUnit.intents.push(intent);
12308
+ }
12309
+ } else {
12310
+ eoaUnit ??= {
12311
+ intents: [],
12312
+ chain: resolveIntentChain(group, intent),
12313
+ contractSigner: false,
12314
+ firstIntentIndex: index
12315
+ };
12316
+ eoaUnit.intents.push(intent);
12317
+ }
12318
+ }
12319
+ const signingUnits = [
12320
+ ...contractUnitsByDomain.values()
12321
+ ];
12322
+ if (eoaUnit !== undefined) signingUnits.push(eoaUnit);
12323
+ signingUnits.sort((a, b)=>a.firstIntentIndex - b.firstIntentIndex);
12324
+ return signingUnits;
12325
+ }
12326
+ async function signUnit(group, unit) {
12327
+ const { adapter, address } = group;
12328
+ const firstIntent = unit.intents[0];
12329
+ const typedData = unit.intents.length === 1 && firstIntent !== undefined ? evmSigningData(firstIntent) : evmSigningData(unit.intents);
12330
+ const operationContext = address === undefined ? {
12331
+ chain: unit.chain
12332
+ } : {
12333
+ chain: unit.chain,
12334
+ address
12335
+ };
12336
+ const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12337
+ typedData,
12338
+ chain: unit.chain
12339
+ }, operationContext);
12340
+ const result = normalizeSignatureResult(await signRequest.execute());
12341
+ return {
12342
+ intents: unit.intents,
12343
+ signature: result.signature,
12344
+ contractSigner: result.contractSigner || unit.contractSigner
12345
+ };
12346
+ }
12347
+ async function signUnits(group, signingUnits) {
12348
+ const signedSets = [];
12349
+ // Keep wallet prompts deterministic. Multiple adapter groups can still sign
12350
+ // in parallel, but one signer is asked for its chain-bound signatures in
12351
+ // source-intent order.
12352
+ for (const unit of signingUnits){
12353
+ signedSets.push(await signUnit(group, unit));
12354
+ }
12355
+ return signedSets;
12356
+ }
12155
12357
  /**
12156
- * Sign an EVM adapter group: batches all intents and produces a single
12157
- * EIP-712 ECDSA signature.
12358
+ * Sign an EVM adapter group.
12158
12359
  *
12159
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12160
- * For multi-intent groups, `primaryType` is `'BurnIntentSet'`.
12360
+ * EOA intents remain batched into one EIP-712 `BurnIntentSet`. ERC-1271
12361
+ * intents are grouped and signed per source chain because smart accounts
12362
+ * commonly include `chainId` in their replay-safe signature hash.
12363
+ * All returned entries can still be submitted together in one atomic Gateway
12364
+ * transfer request.
12161
12365
  *
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).
12366
+ * Before signing, classifies the signer as an EOA or a contract account.
12367
+ * Gateway validates EOA signatures with `ecrecover` and contract-account
12368
+ * signatures with ERC-1271, but it does not infer which one applies — the
12369
+ * transfer request has to declare it. The returned `contractSigner` flag
12370
+ * carries that decision through to `buildTransferRequestBody`.
12167
12371
  *
12168
12372
  * @param group - The adapter group containing the adapter, chain, and
12169
12373
  * burn intents to sign.
12170
- * @returns A signed set with the intents and the ECDSA signature.
12374
+ * @returns Signed entries with their intents, signatures, and Gateway signer
12375
+ * validation mode.
12376
+ * @throws KitError when an intent has no source-chain mapping or a signing
12377
+ * action returns an invalid signature shape.
12171
12378
  *
12172
12379
  * @example
12173
12380
  * ```typescript
12174
12381
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12175
12382
  *
12176
- * const signedSet = await signEvmIntentGroup({
12383
+ * const signedSets = await signEvmIntentGroup({
12177
12384
  * adapter: evmAdapter,
12178
12385
  * chain: ethereumChain,
12179
12386
  * intents: [burnIntent1, burnIntent2],
12387
+ * chainsByDomain: new Map([
12388
+ * [0, ethereumChain],
12389
+ * [6, baseChain],
12390
+ * ]),
12180
12391
  * address: '0x...',
12181
12392
  * })
12182
- * console.log(signedSet.signature)
12393
+ * console.log(signedSets)
12183
12394
  * ```
12184
12395
  */ 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
- };
12396
+ // Validate the collection before doing bytecode reads or asking a wallet
12397
+ // to sign. evmSigningData owns the canonical BurnIntent validation.
12398
+ validateGroupIntents(group.intents);
12399
+ const chainsByDomain = collectChainsByDomain(group);
12400
+ const signerTypeByDomain = await classifySignerTypes(group, chainsByDomain);
12401
+ const signingUnits = createSigningUnits(group, signerTypeByDomain);
12402
+ return await signUnits(group, signingUnits);
12219
12403
  }
12220
12404
 
12221
12405
  /**
12222
12406
  * Add an EVM intent into the batched EVM group map.
12223
12407
  *
12224
12408
  * 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.
12409
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12410
+ * while signing ERC-1271 intents separately on their source chains.
12227
12411
  *
12228
12412
  * @param intent - The burn intent to group.
12229
12413
  * @param alloc - The allocation that resolved to this intent.
@@ -12240,6 +12424,7 @@ function evmSigningData(burnIntent) {
12240
12424
  const existing = evmGroups.get(alloc.adapter);
12241
12425
  if (existing) {
12242
12426
  existing.intents.push(intent);
12427
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12243
12428
  } else {
12244
12429
  evmGroups.set(alloc.adapter, {
12245
12430
  adapter: alloc.adapter,
@@ -12247,6 +12432,12 @@ function evmSigningData(burnIntent) {
12247
12432
  intents: [
12248
12433
  intent
12249
12434
  ],
12435
+ chainsByDomain: new Map([
12436
+ [
12437
+ alloc.chain.gateway.domain,
12438
+ alloc.chain
12439
+ ]
12440
+ ]),
12250
12441
  address: alloc.sourceSigner
12251
12442
  });
12252
12443
  }
@@ -13772,7 +13963,8 @@ function throwNetworkMismatch(expected, actual) {
13772
13963
  };
13773
13964
  }
13774
13965
  /**
13775
- * Group intents by adapter and chain for signing (Solana one-per-intent, EVM batched by adapter).
13966
+ * Group intents for signing (Solana one-per-intent, EVM batched by adapter
13967
+ * with every source chain retained by Gateway domain).
13776
13968
  *
13777
13969
  * @param intents - Burn intents from estimate response.
13778
13970
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16665,22 +16857,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16665
16857
  *
16666
16858
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16667
16859
  *
16860
+ * Sets flagged `contractSigner` carry `contractSigner: true`, which tells
16861
+ * Gateway to validate the signature with ERC-1271 (an offchain
16862
+ * `isValidSignature` simulation) instead of `ecrecover`. The flag is
16863
+ * omitted for EOA signers so their payloads stay byte-identical.
16864
+ *
16668
16865
  * @param signedSets - Signed intent sets (intents + signature per signer).
16669
16866
  * @returns Array of transfer payloads for POST /v1/transfer.
16670
16867
  */ function buildTransferRequestBody(signedSets) {
16671
16868
  return signedSets.map((set)=>{
16672
16869
  const firstIntent = set.intents[0];
16870
+ const contractSigner = set.contractSigner === true ? {
16871
+ contractSigner: true
16872
+ } : {};
16673
16873
  if (set.intents.length === 1 && firstIntent) {
16674
16874
  return {
16675
16875
  burnIntent: serializeBurnIntent(firstIntent),
16676
- signature: set.signature
16876
+ signature: set.signature,
16877
+ ...contractSigner
16677
16878
  };
16678
16879
  }
16679
16880
  return {
16680
16881
  burnIntentSet: {
16681
16882
  intents: set.intents.map(serializeBurnIntent)
16682
16883
  },
16683
- signature: set.signature
16884
+ signature: set.signature,
16885
+ ...contractSigner
16684
16886
  };
16685
16887
  });
16686
16888
  }
@@ -17043,11 +17245,16 @@ const BPS_DIVISOR = 100_000n;
17043
17245
  return required;
17044
17246
  }
17045
17247
 
17248
+ function requireEvmChainsByDomain(group) {
17249
+ if (group.chainsByDomain !== undefined) return group.chainsByDomain;
17250
+ throw createValidationFailedError$1('adapterGroup.chainsByDomain', group.chainsByDomain, 'must be provided for an EVM adapter group');
17251
+ }
17046
17252
  /**
17047
- * Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
17253
+ * Sign each adapter group: Solana one intent per signature, and EVM either
17254
+ * batched for EOAs or split by source chain for ERC-1271 signers.
17048
17255
  *
17049
17256
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17050
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17257
+ * @returns Promise of signed sets for buildTransferRequestBody.
17051
17258
  *
17052
17259
  * @example
17053
17260
  * ```typescript
@@ -17060,9 +17267,10 @@ const BPS_DIVISOR = 100_000n;
17060
17267
  if (group.chain.type === 'solana') {
17061
17268
  return signSolanaIntentGroup(group);
17062
17269
  }
17063
- return [
17064
- await signEvmIntentGroup(group)
17065
- ];
17270
+ return await signEvmIntentGroup({
17271
+ ...group,
17272
+ chainsByDomain: requireEvmChainsByDomain(group)
17273
+ });
17066
17274
  }));
17067
17275
  return nested.flat();
17068
17276
  }
@@ -17712,7 +17920,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17712
17920
  signedSetCount: signedSets.length,
17713
17921
  signatures: signedSets.map((s)=>({
17714
17922
  intentCount: s.intents.length,
17715
- signature: s.signature
17923
+ signature: s.signature,
17924
+ contractSigner: s.contractSigner === true
17716
17925
  }))
17717
17926
  }
17718
17927
  });