@circle-fin/app-kit 1.10.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.
@@ -18,6 +18,17 @@
18
18
 
19
19
  'use strict';
20
20
 
21
+ // Buffer polyfill setup - executes before any other code
22
+ // Ensures globalThis.Buffer is available for Solana libraries
23
+ const { Buffer } = require('buffer');
24
+ if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
25
+ globalThis.Buffer = Buffer;
26
+ }
27
+ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
28
+ window.Buffer = Buffer;
29
+ }
30
+
31
+
21
32
  var zod = require('zod');
22
33
  var web3_js = require('@solana/web3.js');
23
34
  require('bn.js');
@@ -47,6 +58,27 @@ var bs58__default = /*#__PURE__*/_interopDefault(bs58);
47
58
  * }
48
59
  * ```
49
60
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
61
+ /**
62
+ * Return the SDK User-Agent request header only when running in Node.js.
63
+ *
64
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
65
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
66
+ * attribution header because they cannot set it reliably.
67
+ *
68
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { getNodeUserAgentHeader } from '@core/utils'
73
+ *
74
+ * const headers = {
75
+ * 'Content-Type': 'application/json',
76
+ * ...getNodeUserAgentHeader(),
77
+ * }
78
+ * ```
79
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
80
+ 'User-Agent': getUserAgent()
81
+ } : {};
50
82
  /**
51
83
  * Detect the runtime environment and return a shortened identifier.
52
84
  *
@@ -632,11 +664,6 @@ class KitError extends Error {
632
664
  name: 'INPUT_UNSUPPORTED_TOKEN',
633
665
  type: 'INPUT'
634
666
  },
635
- /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
636
- code: 1008,
637
- name: 'INPUT_UNSUPPORTED_ACTION',
638
- type: 'INPUT'
639
- },
640
667
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
641
668
  code: 1009,
642
669
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -2293,6 +2320,8 @@ class KitError extends Error {
2293
2320
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2294
2321
  Blockchain["XDC"] = "XDC";
2295
2322
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2323
+ Blockchain["X_Layer"] = "X_Layer";
2324
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2296
2325
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2297
2326
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2298
2327
  })(Blockchain || (Blockchain = {}));
@@ -2346,6 +2375,7 @@ var BridgeChain;
2346
2375
  BridgeChain["Unichain"] = "Unichain";
2347
2376
  BridgeChain["World_Chain"] = "World_Chain";
2348
2377
  BridgeChain["XDC"] = "XDC";
2378
+ BridgeChain["X_Layer"] = "X_Layer";
2349
2379
  // Testnet chains with CCTPv2 support
2350
2380
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2351
2381
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2371,6 +2401,7 @@ var BridgeChain;
2371
2401
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2372
2402
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2373
2403
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2404
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2374
2405
  })(BridgeChain || (BridgeChain = {}));
2375
2406
  var UnifiedBalanceChain;
2376
2407
  (function(UnifiedBalanceChain) {
@@ -4918,7 +4949,8 @@ var EarnChain;
4918
4949
  isTestnet: true,
4919
4950
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4920
4951
  rpcEndpoints: [
4921
- 'https://rpc-amoy.polygon.technology'
4952
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4953
+ 'https://polygon-amoy.drpc.org'
4922
4954
  ],
4923
4955
  eurcAddress: null,
4924
4956
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5783,6 +5815,104 @@ var EarnChain;
5783
5815
  }
5784
5816
  });
5785
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
+
5786
5916
  /**
5787
5917
  * ZKSync Era Mainnet chain definition
5788
5918
  * @remarks
@@ -5902,6 +6032,8 @@ var Chains = {
5902
6032
  WorldChainSepolia: WorldChainSepolia,
5903
6033
  XDC: XDC,
5904
6034
  XDCApothem: XDCApothem,
6035
+ XLayer: XLayer,
6036
+ XLayerTestnet: XLayerTestnet,
5905
6037
  ZKSyncEra: ZKSyncEra,
5906
6038
  ZKSyncEraSepolia: ZKSyncEraSepolia
5907
6039
  };
@@ -7086,13 +7218,12 @@ const swapTokenEnumSchema = zod.z.enum([
7086
7218
  headers: {
7087
7219
  ...DEFAULT_CONFIG.headers,
7088
7220
  ...config.headers ?? {},
7089
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
7090
- // This is why we use the 'X-User-Agent' header instead.
7091
- ...typeof window === 'undefined' ? {
7092
- 'User-Agent': getUserAgent()
7093
- } : {
7094
- 'X-User-Agent': getUserAgent()
7095
- }
7221
+ // Browsers forbid setting a user-agent request header, and the custom
7222
+ // fallback header the SDK used instead trips CORS preflight against the
7223
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
7224
+ // blocking the request. So send the SDK user agent only in Node;
7225
+ // browsers omit it entirely.
7226
+ ...getNodeUserAgentHeader()
7096
7227
  }
7097
7228
  };
7098
7229
  let lastError;
@@ -7855,6 +7986,7 @@ function parseOrThrow(value, schema, context) {
7855
7986
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7856
7987
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7857
7988
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7989
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7858
7990
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7859
7991
  // =========================================================================
7860
7992
  // Testnets (alphabetically sorted)
@@ -7889,6 +8021,7 @@ function parseOrThrow(value, schema, context) {
7889
8021
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7890
8022
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7891
8023
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8024
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7892
8025
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7893
8026
  }
7894
8027
  };
@@ -8364,6 +8497,7 @@ function parseOrThrow(value, schema, context) {
8364
8497
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
8365
8498
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
8366
8499
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
8500
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
8367
8501
  if (payload.errorDetails !== undefined) {
8368
8502
  const errorDetails = {
8369
8503
  ...payload.errorDetails.errorCode !== undefined && {
@@ -8434,18 +8568,15 @@ function parseOrThrow(value, schema, context) {
8434
8568
  timeoutHandle.unref();
8435
8569
  }
8436
8570
  try {
8437
- const isNode = isNodeEnvironment();
8438
- const userAgent = getUserAgent();
8439
8571
  await fetch(getLogsUrl(), {
8440
8572
  method: 'POST',
8441
8573
  headers: {
8442
8574
  'Content-Type': 'application/json',
8443
- // Browser restricts setting User-Agent; use X-User-Agent instead.
8444
- ...isNode ? {
8445
- 'User-Agent': userAgent
8446
- } : {
8447
- 'X-User-Agent': userAgent
8448
- }
8575
+ // Browsers forbid setting a user-agent request header, and the custom
8576
+ // fallback header the SDK used instead trips CORS preflight (it isn't
8577
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
8578
+ // it only in Node; browsers omit it entirely.
8579
+ ...getNodeUserAgentHeader()
8449
8580
  },
8450
8581
  body: JSON.stringify(toSafePayload(payload)),
8451
8582
  signal: controller.signal
@@ -8658,7 +8789,7 @@ function parseOrThrow(value, schema, context) {
8658
8789
  // discards the stack trace, nested `cause`, and any custom Error
8659
8790
  // properties — exactly the context an on-call needs when a
8660
8791
  // resolver-closure regression triggers this path.
8661
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
8792
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
8662
8793
  } catch {
8663
8794
  // console.warn itself throwing is the user's environment; nothing more we
8664
8795
  // can do without risking the original operation error.
@@ -8674,7 +8805,9 @@ function parseOrThrow(value, schema, context) {
8674
8805
  sdkVersion: config.sdkVersion,
8675
8806
  eventType,
8676
8807
  timestamp: new Date().toISOString(),
8677
- errorDetails,
8808
+ ...errorDetails !== undefined && {
8809
+ errorDetails
8810
+ },
8678
8811
  clientContext: buildClientContext(),
8679
8812
  ...context?.sourceChain != null && {
8680
8813
  sourceChain: context.sourceChain
@@ -8690,6 +8823,9 @@ function parseOrThrow(value, schema, context) {
8690
8823
  },
8691
8824
  ...context?.txHash != null && {
8692
8825
  txHash: context.txHash
8826
+ },
8827
+ ...context?.correlationId != null && {
8828
+ correlationId: context.correlationId
8693
8829
  }
8694
8830
  };
8695
8831
  }
@@ -8751,7 +8887,7 @@ function parseOrThrow(value, schema, context) {
8751
8887
  }
8752
8888
 
8753
8889
  var name = "@circle-fin/unified-balance-kit";
8754
- var version = "1.3.0";
8890
+ var version = "1.4.0";
8755
8891
  var pkg = {
8756
8892
  name: name,
8757
8893
  version: version};
@@ -12032,72 +12168,55 @@ function evmSigningData(burnIntent) {
12032
12168
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12033
12169
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12034
12170
  * signatures, so for Gateway's purposes a 7702-delegated address is
12035
- * an EOA, not an SCA.
12171
+ * an EOA, not a contract signer.
12036
12172
  *
12037
12173
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12038
12174
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12039
12175
  /**
12040
- * 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.
12041
12178
  *
12042
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
12043
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
12044
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
12045
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
12046
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
12047
- * against Gateway's chainId-less domain with an opaque
12048
- * `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.
12049
12184
  *
12050
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
12051
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
12052
- * `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.
12053
12189
  *
12054
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
12055
- * error directing the caller to register an EOA delegate against the
12056
- * SCA and then submit the spend with the delegate EOA as the signer
12057
- * and the SCA as the source account. See the unified-balance / Gateway
12058
- * docs for the exact API.
12059
- *
12060
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
12061
- * skipped and downstream signing surfaces its own error — a warning is
12062
- * 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.
12063
12194
  *
12064
12195
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12065
- * @param address - Signer address to validate.
12196
+ * @param address - Signer address to classify.
12066
12197
  * @param chain - EVM chain where the signer lives.
12067
- * @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.
12068
12200
  *
12069
12201
  * @example
12070
12202
  * ```typescript
12071
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12203
+ * import { isContractSigner } from '@core/adapter-evm'
12072
12204
  * import { Ethereum } from '@core/chains'
12073
12205
  *
12074
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12206
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12075
12207
  * ```
12076
- */ async function assertSignerIsEoa(adapter, address, chain) {
12208
+ */ async function isContractSigner(adapter, address, chain) {
12077
12209
  let code;
12078
12210
  try {
12079
12211
  code = await adapter.readBytecode(address, chain);
12080
12212
  } catch (err) {
12081
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12082
- 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;
12083
12215
  }
12084
12216
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12085
- return;
12217
+ return false;
12086
12218
  }
12087
- throw new KitError({
12088
- ...InputError.UNSUPPORTED_ACTION,
12089
- recoverability: 'FATAL',
12090
- 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.`,
12091
- cause: {
12092
- trace: {
12093
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
12094
- address,
12095
- chain: chain.name,
12096
- bytecodeBytes: (code.length - 2) / 2,
12097
- bytecodePrefix: code.slice(0, 12)
12098
- }
12099
- }
12100
- });
12219
+ return true;
12101
12220
  }
12102
12221
 
12103
12222
  /**
@@ -12124,78 +12243,177 @@ function evmSigningData(burnIntent) {
12124
12243
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12125
12244
  }
12126
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
+ }
12127
12363
  /**
12128
- * Sign an EVM adapter group: batches all intents and produces a single
12129
- * EIP-712 ECDSA signature.
12364
+ * Sign an EVM adapter group.
12130
12365
  *
12131
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12132
- * 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.
12133
12371
  *
12134
- * Before signing, asserts that the signer address is an EOA. Gateway
12135
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
12136
- * fallback), so signatures produced by smart-contract accounts (SCAs)
12137
- * cannot be verified. When an SCA is detected, a clear error is raised
12138
- * 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`.
12139
12377
  *
12140
12378
  * @param group - The adapter group containing the adapter, chain, and
12141
12379
  * burn intents to sign.
12142
- * @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.
12143
12384
  *
12144
12385
  * @example
12145
12386
  * ```typescript
12146
12387
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12147
12388
  *
12148
- * const signedSet = await signEvmIntentGroup({
12389
+ * const signedSets = await signEvmIntentGroup({
12149
12390
  * adapter: evmAdapter,
12150
12391
  * chain: ethereumChain,
12151
12392
  * intents: [burnIntent1, burnIntent2],
12393
+ * chainsByDomain: new Map([
12394
+ * [0, ethereumChain],
12395
+ * [6, baseChain],
12396
+ * ]),
12152
12397
  * address: '0x...',
12153
12398
  * })
12154
- * console.log(signedSet.signature)
12399
+ * console.log(signedSets)
12155
12400
  * ```
12156
12401
  */ async function signEvmIntentGroup(group) {
12157
- const { adapter, intents: groupIntents, chain, address } = group;
12158
- const operationContext = address === undefined ? {
12159
- chain
12160
- } : {
12161
- chain,
12162
- address
12163
- };
12164
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
12165
- // signer silently produces a signature over a wrapped hash that Gateway
12166
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
12167
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
12168
- // when we can detect bytecode at the signer address. See DEVX-2774.
12169
- //
12170
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12171
- // each consumer package bundles its own copy of the base class and the
12172
- // `instanceof` identity check fails across package boundaries.
12173
- //
12174
- // Empty string is defended against because assertSignerIsEoa would
12175
- // otherwise call eth_getCode('') on the RPC.
12176
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12177
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
12178
- await assertSignerIsEoa(adapter, address, chain);
12179
- }
12180
- const firstIntent = groupIntents[0];
12181
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
12182
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12183
- typedData,
12184
- chain
12185
- }, operationContext);
12186
- const sig = await signRequest.execute();
12187
- return {
12188
- intents: groupIntents,
12189
- signature: sig
12190
- };
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);
12191
12409
  }
12192
12410
 
12193
12411
  /**
12194
12412
  * Add an EVM intent into the batched EVM group map.
12195
12413
  *
12196
12414
  * On EVM, all intents for the same adapter are batched into a single
12197
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
12198
- * operation.
12415
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12416
+ * while signing ERC-1271 intents separately on their source chains.
12199
12417
  *
12200
12418
  * @param intent - The burn intent to group.
12201
12419
  * @param alloc - The allocation that resolved to this intent.
@@ -12212,6 +12430,7 @@ function evmSigningData(burnIntent) {
12212
12430
  const existing = evmGroups.get(alloc.adapter);
12213
12431
  if (existing) {
12214
12432
  existing.intents.push(intent);
12433
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12215
12434
  } else {
12216
12435
  evmGroups.set(alloc.adapter, {
12217
12436
  adapter: alloc.adapter,
@@ -12219,6 +12438,12 @@ function evmSigningData(burnIntent) {
12219
12438
  intents: [
12220
12439
  intent
12221
12440
  ],
12441
+ chainsByDomain: new Map([
12442
+ [
12443
+ alloc.chain.gateway.domain,
12444
+ alloc.chain
12445
+ ]
12446
+ ]),
12222
12447
  address: alloc.sourceSigner
12223
12448
  });
12224
12449
  }
@@ -13744,7 +13969,8 @@ function throwNetworkMismatch(expected, actual) {
13744
13969
  };
13745
13970
  }
13746
13971
  /**
13747
- * 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).
13748
13974
  *
13749
13975
  * @param intents - Burn intents from estimate response.
13750
13976
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16637,22 +16863,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16637
16863
  *
16638
16864
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16639
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
+ *
16640
16871
  * @param signedSets - Signed intent sets (intents + signature per signer).
16641
16872
  * @returns Array of transfer payloads for POST /v1/transfer.
16642
16873
  */ function buildTransferRequestBody(signedSets) {
16643
16874
  return signedSets.map((set)=>{
16644
16875
  const firstIntent = set.intents[0];
16876
+ const contractSigner = set.contractSigner === true ? {
16877
+ contractSigner: true
16878
+ } : {};
16645
16879
  if (set.intents.length === 1 && firstIntent) {
16646
16880
  return {
16647
16881
  burnIntent: serializeBurnIntent(firstIntent),
16648
- signature: set.signature
16882
+ signature: set.signature,
16883
+ ...contractSigner
16649
16884
  };
16650
16885
  }
16651
16886
  return {
16652
16887
  burnIntentSet: {
16653
16888
  intents: set.intents.map(serializeBurnIntent)
16654
16889
  },
16655
- signature: set.signature
16890
+ signature: set.signature,
16891
+ ...contractSigner
16656
16892
  };
16657
16893
  });
16658
16894
  }
@@ -17015,11 +17251,16 @@ const BPS_DIVISOR = 100_000n;
17015
17251
  return required;
17016
17252
  }
17017
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
+ }
17018
17258
  /**
17019
- * 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.
17020
17261
  *
17021
17262
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17022
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17263
+ * @returns Promise of signed sets for buildTransferRequestBody.
17023
17264
  *
17024
17265
  * @example
17025
17266
  * ```typescript
@@ -17032,9 +17273,10 @@ const BPS_DIVISOR = 100_000n;
17032
17273
  if (group.chain.type === 'solana') {
17033
17274
  return signSolanaIntentGroup(group);
17034
17275
  }
17035
- return [
17036
- await signEvmIntentGroup(group)
17037
- ];
17276
+ return await signEvmIntentGroup({
17277
+ ...group,
17278
+ chainsByDomain: requireEvmChainsByDomain(group)
17279
+ });
17038
17280
  }));
17039
17281
  return nested.flat();
17040
17282
  }
@@ -17684,7 +17926,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17684
17926
  signedSetCount: signedSets.length,
17685
17927
  signatures: signedSets.map((s)=>({
17686
17928
  intentCount: s.intents.length,
17687
- signature: s.signature
17929
+ signature: s.signature,
17930
+ contractSigner: s.contractSigner === true
17688
17931
  }))
17689
17932
  }
17690
17933
  });
@@ -20429,7 +20672,11 @@ const removeFundParamsSchema = zod.z.object({
20429
20672
  // Remove Fund Operations
20430
20673
  // ---------------------------------------------------------------------------
20431
20674
  /**
20432
- * Kick off a delayed fund removal from an account.
20675
+ * Kick off a delayed recovery fund removal from an account.
20676
+ *
20677
+ * Use `initiateRemoveFund` only as a trustless fallback when the normal spend
20678
+ * flow is unavailable. For day-to-day movement out of a Unified Balance, use
20679
+ * `spend`.
20433
20680
  *
20434
20681
  * Validates `from` and `amount`, resolves the chain and token via
20435
20682
  * {@link resolveRemoveFundParams}, selects the matching provider, then calls
@@ -20466,7 +20713,10 @@ const removeFundParamsSchema = zod.z.object({
20466
20713
  return provider.initiateRemoveFund(resolved);
20467
20714
  }
20468
20715
  /**
20469
- * Complete a fund removal once the 7-day activation period has passed.
20716
+ * Complete a recovery fund removal once the 7-day withdrawal delay has passed.
20717
+ *
20718
+ * Use `removeFund` only as a trustless fallback when the normal spend flow is
20719
+ * unavailable. For day-to-day movement out of a Unified Balance, use `spend`.
20470
20720
  *
20471
20721
  * Validates `from`, resolves the chain and token via
20472
20722
  * {@link resolveRemoveFundParams}, selects the matching provider, then calls
@@ -20555,13 +20805,18 @@ const removeFundParamsSchema = zod.z.object({
20555
20805
  /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg.name);
20556
20806
  /**
20557
20807
  * A high-level class-based interface for cross-chain USDC deposits,
20558
- * spending, balance queries, delegation management, and withdrawals.
20808
+ * spending, balance queries, delegation management, and recovery fund removals.
20559
20809
  *
20560
20810
  * UnifiedBalanceKit provides a familiar class-based API for developers who
20561
20811
  * prefer traditional object-oriented patterns. The class maintains an
20562
20812
  * internal context and provides methods that delegate to the standalone
20563
20813
  * operation functions exported by this package.
20564
20814
  *
20815
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
20816
+ * trustless recovery path for situations where the normal spend flow is
20817
+ * unavailable, and it requires a 7-day withdrawal delay before funds can be
20818
+ * removed.
20819
+ *
20565
20820
  * @remarks
20566
20821
  * For functional usage, import and use the operations directly:
20567
20822
  * ```typescript
@@ -20782,7 +21037,11 @@ const removeFundParamsSchema = zod.z.object({
20782
21037
  });
20783
21038
  }
20784
21039
  /**
20785
- * Kick off a delayed fund removal from an account.
21040
+ * Kick off a delayed recovery fund removal from an account.
21041
+ *
21042
+ * Use this only as a trustless fallback when the normal spend flow is
21043
+ * unavailable. For day-to-day movement out of a Unified Balance, use
21044
+ * `spend`.
20786
21045
  *
20787
21046
  * @param params - The account owner's adapter context, amount, and
20788
21047
  * optional token type.
@@ -20796,7 +21055,12 @@ const removeFundParamsSchema = zod.z.object({
20796
21055
  });
20797
21056
  }
20798
21057
  /**
20799
- * Complete a fund removal once the activation period has passed.
21058
+ * Complete a recovery fund removal once the 7-day withdrawal delay has
21059
+ * passed.
21060
+ *
21061
+ * Use this only as a trustless fallback when the normal spend flow is
21062
+ * unavailable. For day-to-day movement out of a Unified Balance, use
21063
+ * `spend`.
20800
21064
  *
20801
21065
  * @param params - The account owner context matching the original
20802
21066
  * fund removal initiation.
@@ -20923,6 +21187,11 @@ registerKit(`${pkg.name}/${pkg.version}`);
20923
21187
  * Internally holds a persistent {@link UnifiedBalanceKit} instance so that
20924
21188
  * event dispatchers and custom fee policies are preserved across calls.
20925
21189
  *
21190
+ * Use {@link AppKitUnifiedBalance.spend} for normal movement out of a Unified
21191
+ * Balance. {@link AppKitUnifiedBalance.removeFund} is a trustless recovery path
21192
+ * for situations where the normal spend flow is unavailable, and it requires a
21193
+ * 7-day withdrawal delay after {@link AppKitUnifiedBalance.initiateRemoveFund}.
21194
+ *
20926
21195
  * @example
20927
21196
  * ```typescript
20928
21197
  * import { AppKit } from '@circle-fin/app-kit'
@@ -21134,7 +21403,12 @@ registerKit(`${pkg.name}/${pkg.version}`);
21134
21403
  return this.kit.removeDelegate(params);
21135
21404
  }
21136
21405
  /**
21137
- * Kick off a delayed fund removal from an account.
21406
+ * Initiate a trustless recovery removal from an account.
21407
+ *
21408
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
21409
+ * recovery path for situations where the normal spend flow is unavailable.
21410
+ * Calling this method starts the 7-day withdrawal delay before the removal can
21411
+ * be completed.
21138
21412
  *
21139
21413
  * @param params - The account owner's adapter context, amount, and token.
21140
21414
  * @returns Promise resolving to the initiation details.
@@ -21153,11 +21427,16 @@ registerKit(`${pkg.name}/${pkg.version}`);
21153
21427
  return this.kit.initiateRemoveFund(params);
21154
21428
  }
21155
21429
  /**
21156
- * Complete a fund removal once the activation period has passed.
21430
+ * Complete a trustless recovery removal after the withdrawal delay.
21431
+ *
21432
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
21433
+ * recovery path for situations where the normal spend flow is unavailable.
21434
+ * Both EVM and Solana removals require a 7-day withdrawal delay after
21435
+ * `initiateRemoveFund` before funds can be removed.
21157
21436
  *
21158
21437
  * @param params - The account owner context matching the original initiation.
21159
21438
  * @returns Promise resolving to the fund removal details.
21160
- * @throws {KitError} If the activation period has not elapsed or the
21439
+ * @throws {KitError} If the withdrawal delay has not elapsed or the
21161
21440
  * on-chain transaction fails.
21162
21441
  *
21163
21442
  * @example