@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.
@@ -16,6 +16,17 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ // Buffer polyfill setup - executes before any other code
20
+ // Ensures globalThis.Buffer is available for Solana libraries
21
+ import { Buffer } from 'buffer';
22
+ if (typeof globalThis !== 'undefined' && typeof globalThis.Buffer === 'undefined') {
23
+ globalThis.Buffer = Buffer;
24
+ }
25
+ if (typeof window !== 'undefined' && typeof window.Buffer === 'undefined') {
26
+ window.Buffer = Buffer;
27
+ }
28
+
29
+
19
30
  import { z } from 'zod';
20
31
  import { PublicKey } from '@solana/web3.js';
21
32
  import 'bn.js';
@@ -41,6 +52,27 @@ import { getAddress } from '@ethersproject/address';
41
52
  * }
42
53
  * ```
43
54
  */ const isNodeEnvironment = ()=>typeof process !== 'undefined' && typeof process.versions === 'object' && typeof process.versions.node === 'string';
55
+ /**
56
+ * Return the SDK User-Agent request header only when running in Node.js.
57
+ *
58
+ * Browsers forbid manually setting `User-Agent`, and a custom fallback header
59
+ * can trigger CORS preflight. Non-Node server runtimes also omit this optional
60
+ * attribution header because they cannot set it reliably.
61
+ *
62
+ * @returns A User-Agent header in Node.js, or an empty object otherwise.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * import { getNodeUserAgentHeader } from '@core/utils'
67
+ *
68
+ * const headers = {
69
+ * 'Content-Type': 'application/json',
70
+ * ...getNodeUserAgentHeader(),
71
+ * }
72
+ * ```
73
+ */ const getNodeUserAgentHeader = ()=>isNodeEnvironment() ? {
74
+ 'User-Agent': getUserAgent()
75
+ } : {};
44
76
  /**
45
77
  * Detect the runtime environment and return a shortened identifier.
46
78
  *
@@ -626,11 +658,6 @@ class KitError extends Error {
626
658
  name: 'INPUT_UNSUPPORTED_TOKEN',
627
659
  type: 'INPUT'
628
660
  },
629
- /** Action not supported by this adapter / ecosystem */ UNSUPPORTED_ACTION: {
630
- code: 1008,
631
- name: 'INPUT_UNSUPPORTED_ACTION',
632
- type: 'INPUT'
633
- },
634
661
  /** No route satisfies the slippage or minimum-output constraint */ SLIPPAGE_CONSTRAINT_NOT_MET: {
635
662
  code: 1009,
636
663
  name: 'INPUT_SLIPPAGE_CONSTRAINT_NOT_MET',
@@ -2287,6 +2314,8 @@ class KitError extends Error {
2287
2314
  Blockchain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2288
2315
  Blockchain["XDC"] = "XDC";
2289
2316
  Blockchain["XDC_Apothem"] = "XDC_Apothem";
2317
+ Blockchain["X_Layer"] = "X_Layer";
2318
+ Blockchain["X_Layer_Testnet"] = "X_Layer_Testnet";
2290
2319
  Blockchain["ZKSync_Era"] = "ZKSync_Era";
2291
2320
  Blockchain["ZKSync_Sepolia"] = "ZKSync_Sepolia";
2292
2321
  })(Blockchain || (Blockchain = {}));
@@ -2340,6 +2369,7 @@ var BridgeChain;
2340
2369
  BridgeChain["Unichain"] = "Unichain";
2341
2370
  BridgeChain["World_Chain"] = "World_Chain";
2342
2371
  BridgeChain["XDC"] = "XDC";
2372
+ BridgeChain["X_Layer"] = "X_Layer";
2343
2373
  // Testnet chains with CCTPv2 support
2344
2374
  BridgeChain["Arc_Testnet"] = "Arc_Testnet";
2345
2375
  BridgeChain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
@@ -2365,6 +2395,7 @@ var BridgeChain;
2365
2395
  BridgeChain["Unichain_Sepolia"] = "Unichain_Sepolia";
2366
2396
  BridgeChain["World_Chain_Sepolia"] = "World_Chain_Sepolia";
2367
2397
  BridgeChain["XDC_Apothem"] = "XDC_Apothem";
2398
+ BridgeChain["X_Layer_Testnet"] = "X_Layer_Testnet";
2368
2399
  })(BridgeChain || (BridgeChain = {}));
2369
2400
  var UnifiedBalanceChain;
2370
2401
  (function(UnifiedBalanceChain) {
@@ -4912,7 +4943,8 @@ var EarnChain;
4912
4943
  isTestnet: true,
4913
4944
  explorerUrl: 'https://amoy.polygonscan.com/tx/{hash}',
4914
4945
  rpcEndpoints: [
4915
- 'https://rpc-amoy.polygon.technology'
4946
+ 'https://polygon-amoy-bor-rpc.publicnode.com',
4947
+ 'https://polygon-amoy.drpc.org'
4916
4948
  ],
4917
4949
  eurcAddress: null,
4918
4950
  usdcAddress: '0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582',
@@ -5777,6 +5809,104 @@ var EarnChain;
5777
5809
  }
5778
5810
  });
5779
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
+
5780
5910
  /**
5781
5911
  * ZKSync Era Mainnet chain definition
5782
5912
  * @remarks
@@ -5896,6 +6026,8 @@ var Chains = /*#__PURE__*/Object.freeze({
5896
6026
  WorldChainSepolia: WorldChainSepolia,
5897
6027
  XDC: XDC,
5898
6028
  XDCApothem: XDCApothem,
6029
+ XLayer: XLayer,
6030
+ XLayerTestnet: XLayerTestnet,
5899
6031
  ZKSyncEra: ZKSyncEra,
5900
6032
  ZKSyncEraSepolia: ZKSyncEraSepolia
5901
6033
  });
@@ -7080,13 +7212,12 @@ const swapTokenEnumSchema = z.enum([
7080
7212
  headers: {
7081
7213
  ...DEFAULT_CONFIG.headers,
7082
7214
  ...config.headers ?? {},
7083
- // In browser environments, directly setting the 'User-Agent' or similar headers is restricted and may be ignored or cause errors.
7084
- // This is why we use the 'X-User-Agent' header instead.
7085
- ...typeof window === 'undefined' ? {
7086
- 'User-Agent': getUserAgent()
7087
- } : {
7088
- 'X-User-Agent': getUserAgent()
7089
- }
7215
+ // Browsers forbid setting a user-agent request header, and the custom
7216
+ // fallback header the SDK used instead trips CORS preflight against the
7217
+ // Circle APIs (it isn't in their `Access-Control-Allow-Headers`),
7218
+ // blocking the request. So send the SDK user agent only in Node;
7219
+ // browsers omit it entirely.
7220
+ ...getNodeUserAgentHeader()
7090
7221
  }
7091
7222
  };
7092
7223
  let lastError;
@@ -7849,6 +7980,7 @@ function parseOrThrow(value, schema, context) {
7849
7980
  [Blockchain.Unichain]: '0x078D782b760474a361dDA0AF3839290b0EF57AD6',
7850
7981
  [Blockchain.World_Chain]: '0x79A02482A880bCE3F13e09Da970dC34db4CD24d1',
7851
7982
  [Blockchain.XDC]: '0xfA2958CB79b0491CC627c1557F441eF849Ca8eb1',
7983
+ [Blockchain.X_Layer]: '0xB6CEceAB302E2E4948951eE7843FC24E92933061',
7852
7984
  [Blockchain.ZKSync_Era]: '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4',
7853
7985
  // =========================================================================
7854
7986
  // Testnets (alphabetically sorted)
@@ -7883,6 +8015,7 @@ function parseOrThrow(value, schema, context) {
7883
8015
  [Blockchain.Unichain_Sepolia]: '0x31d0220469e10c4E71834a79b1f276d740d3768F',
7884
8016
  [Blockchain.World_Chain_Sepolia]: '0x66145f38cBAC35Ca6F1Dfb4914dF98F1614aeA88',
7885
8017
  [Blockchain.XDC_Apothem]: '0xb5AB69F7bBada22B28e79C8FFAECe55eF1c771D4',
8018
+ [Blockchain.X_Layer_Testnet]: '0xDec90b78111Ba2fc6FC6d84d8B9ec159A2d4b9B3',
7886
8019
  [Blockchain.ZKSync_Sepolia]: '0xAe045DE5638162fa134807Cb558E15A3F5A7F853'
7887
8020
  }
7888
8021
  };
@@ -8358,6 +8491,7 @@ function parseOrThrow(value, schema, context) {
8358
8491
  if (payload.tokenIn !== undefined) safe['tokenIn'] = payload.tokenIn;
8359
8492
  if (payload.tokenOut !== undefined) safe['tokenOut'] = payload.tokenOut;
8360
8493
  if (payload.txHash !== undefined) safe['txHash'] = payload.txHash;
8494
+ if (payload.correlationId !== undefined) safe['correlationId'] = payload.correlationId;
8361
8495
  if (payload.errorDetails !== undefined) {
8362
8496
  const errorDetails = {
8363
8497
  ...payload.errorDetails.errorCode !== undefined && {
@@ -8428,18 +8562,15 @@ function parseOrThrow(value, schema, context) {
8428
8562
  timeoutHandle.unref();
8429
8563
  }
8430
8564
  try {
8431
- const isNode = isNodeEnvironment();
8432
- const userAgent = getUserAgent();
8433
8565
  await fetch(getLogsUrl(), {
8434
8566
  method: 'POST',
8435
8567
  headers: {
8436
8568
  'Content-Type': 'application/json',
8437
- // Browser restricts setting User-Agent; use X-User-Agent instead.
8438
- ...isNode ? {
8439
- 'User-Agent': userAgent
8440
- } : {
8441
- 'X-User-Agent': userAgent
8442
- }
8569
+ // Browsers forbid setting a user-agent request header, and the custom
8570
+ // fallback header the SDK used instead trips CORS preflight (it isn't
8571
+ // in the telemetry endpoint's `Access-Control-Allow-Headers`), so send
8572
+ // it only in Node; browsers omit it entirely.
8573
+ ...getNodeUserAgentHeader()
8443
8574
  },
8444
8575
  body: JSON.stringify(toSafePayload(payload)),
8445
8576
  signal: controller.signal
@@ -8652,7 +8783,7 @@ function parseOrThrow(value, schema, context) {
8652
8783
  // discards the stack trace, nested `cause`, and any custom Error
8653
8784
  // properties — exactly the context an on-call needs when a
8654
8785
  // resolver-closure regression triggers this path.
8655
- console.warn(`[stablecoin-kits telemetry] dropped error event '${eventType}':`, cause);
8786
+ console.warn(`[stablecoin-kits telemetry] dropped event '${eventType}':`, cause);
8656
8787
  } catch {
8657
8788
  // console.warn itself throwing is the user's environment; nothing more we
8658
8789
  // can do without risking the original operation error.
@@ -8668,7 +8799,9 @@ function parseOrThrow(value, schema, context) {
8668
8799
  sdkVersion: config.sdkVersion,
8669
8800
  eventType,
8670
8801
  timestamp: new Date().toISOString(),
8671
- errorDetails,
8802
+ ...errorDetails !== undefined && {
8803
+ errorDetails
8804
+ },
8672
8805
  clientContext: buildClientContext(),
8673
8806
  ...context?.sourceChain != null && {
8674
8807
  sourceChain: context.sourceChain
@@ -8684,6 +8817,9 @@ function parseOrThrow(value, schema, context) {
8684
8817
  },
8685
8818
  ...context?.txHash != null && {
8686
8819
  txHash: context.txHash
8820
+ },
8821
+ ...context?.correlationId != null && {
8822
+ correlationId: context.correlationId
8687
8823
  }
8688
8824
  };
8689
8825
  }
@@ -8745,7 +8881,7 @@ function parseOrThrow(value, schema, context) {
8745
8881
  }
8746
8882
 
8747
8883
  var name = "@circle-fin/unified-balance-kit";
8748
- var version = "1.3.0";
8884
+ var version = "1.4.0";
8749
8885
  var pkg = {
8750
8886
  name: name,
8751
8887
  version: version};
@@ -12026,72 +12162,55 @@ function evmSigningData(burnIntent) {
12026
12162
  * `0xef0100` followed by the 20-byte delegate address (23 bytes total).
12027
12163
  * The underlying secp256k1 key still produces `ecrecover`-verifiable
12028
12164
  * signatures, so for Gateway's purposes a 7702-delegated address is
12029
- * an EOA, not an SCA.
12165
+ * an EOA, not a contract signer.
12030
12166
  *
12031
12167
  * Spec: https://eips.ethereum.org/EIPS/eip-7702
12032
12168
  */ const EIP_7702_DELEGATION_PREFIX = '0xef0100';
12033
12169
  /**
12034
- * 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.
12035
12172
  *
12036
- * Gateway verifies burn-intent signatures with plain `ecrecover` (see
12037
- * `evm-gateway-contracts/src/lib/EIP712Domain.sol`). Smart-contract
12038
- * accounts (SCAs) produce signatures over wrapped hashes (ERC-1271 /
12039
- * ERC-6492 / ERC-6900 replay-safe hashes) that Gateway cannot verify.
12040
- * Additionally, the Circle Wallets backend rejects SCA typed-data signing
12041
- * against Gateway's chainId-less domain with an opaque
12042
- * `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.
12043
12178
  *
12044
- * EIP-7702-delegated EOAs are exempt: they expose non-empty bytecode
12045
- * (`0xef0100<delegate>`) but the underlying secp256k1 key still produces
12046
- * `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.
12047
12183
  *
12048
- * When the signer is a true SCA, raises an `INPUT_UNSUPPORTED_ACTION`
12049
- * error directing the caller to register an EOA delegate against the
12050
- * SCA and then submit the spend with the delegate EOA as the signer
12051
- * and the SCA as the source account. See the unified-balance / Gateway
12052
- * docs for the exact API.
12053
- *
12054
- * If bytecode cannot be read (RPC failure, etc.) the pre-check is
12055
- * skipped and downstream signing surfaces its own error — a warning is
12056
- * 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.
12057
12188
  *
12058
12189
  * @param adapter - Anything exposing {@link EvmAdapterLike.readBytecode}.
12059
- * @param address - Signer address to validate.
12190
+ * @param address - Signer address to classify.
12060
12191
  * @param chain - EVM chain where the signer lives.
12061
- * @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.
12062
12194
  *
12063
12195
  * @example
12064
12196
  * ```typescript
12065
- * import { assertSignerIsEoa } from '@core/adapter-evm'
12197
+ * import { isContractSigner } from '@core/adapter-evm'
12066
12198
  * import { Ethereum } from '@core/chains'
12067
12199
  *
12068
- * await assertSignerIsEoa(adapter, '0xabc...', Ethereum)
12200
+ * const useErc1271 = await isContractSigner(adapter, '0xabc...', Ethereum)
12069
12201
  * ```
12070
- */ async function assertSignerIsEoa(adapter, address, chain) {
12202
+ */ async function isContractSigner(adapter, address, chain) {
12071
12203
  let code;
12072
12204
  try {
12073
12205
  code = await adapter.readBytecode(address, chain);
12074
12206
  } catch (err) {
12075
- console.warn(`[gateway] assertSignerIsEoa skipped (readBytecode failed for ` + `${address} on ${chain.name}): ` + (err instanceof Error ? err.message : String(err)));
12076
- 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;
12077
12209
  }
12078
12210
  if (code === undefined || code === '0x' || code.toLowerCase().startsWith(EIP_7702_DELEGATION_PREFIX)) {
12079
- return;
12211
+ return false;
12080
12212
  }
12081
- throw new KitError({
12082
- ...InputError.UNSUPPORTED_ACTION,
12083
- recoverability: 'FATAL',
12084
- 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.`,
12085
- cause: {
12086
- trace: {
12087
- operation: 'signEvmIntentGroup.assertSignerIsEoa',
12088
- address,
12089
- chain: chain.name,
12090
- bytecodeBytes: (code.length - 2) / 2,
12091
- bytecodePrefix: code.slice(0, 12)
12092
- }
12093
- }
12094
- });
12213
+ return true;
12095
12214
  }
12096
12215
 
12097
12216
  /**
@@ -12118,78 +12237,177 @@ function evmSigningData(burnIntent) {
12118
12237
  return typeof value === 'object' && value !== null && 'readBytecode' in value && typeof value.readBytecode === 'function';
12119
12238
  }
12120
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
+ }
12121
12357
  /**
12122
- * Sign an EVM adapter group: batches all intents and produces a single
12123
- * EIP-712 ECDSA signature.
12358
+ * Sign an EVM adapter group.
12124
12359
  *
12125
- * For a single-intent group, `primaryType` is `'BurnIntent'`.
12126
- * 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.
12127
12365
  *
12128
- * Before signing, asserts that the signer address is an EOA. Gateway
12129
- * verifies burn-intent signatures with plain `ecrecover` (no ERC-1271
12130
- * fallback), so signatures produced by smart-contract accounts (SCAs)
12131
- * cannot be verified. When an SCA is detected, a clear error is raised
12132
- * 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`.
12133
12371
  *
12134
12372
  * @param group - The adapter group containing the adapter, chain, and
12135
12373
  * burn intents to sign.
12136
- * @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.
12137
12378
  *
12138
12379
  * @example
12139
12380
  * ```typescript
12140
12381
  * import { signEvmIntentGroup } from '@core/adapter-evm'
12141
12382
  *
12142
- * const signedSet = await signEvmIntentGroup({
12383
+ * const signedSets = await signEvmIntentGroup({
12143
12384
  * adapter: evmAdapter,
12144
12385
  * chain: ethereumChain,
12145
12386
  * intents: [burnIntent1, burnIntent2],
12387
+ * chainsByDomain: new Map([
12388
+ * [0, ethereumChain],
12389
+ * [6, baseChain],
12390
+ * ]),
12146
12391
  * address: '0x...',
12147
12392
  * })
12148
- * console.log(signedSet.signature)
12393
+ * console.log(signedSets)
12149
12394
  * ```
12150
12395
  */ async function signEvmIntentGroup(group) {
12151
- const { adapter, intents: groupIntents, chain, address } = group;
12152
- const operationContext = address === undefined ? {
12153
- chain
12154
- } : {
12155
- chain,
12156
- address
12157
- };
12158
- // Gateway verifies burn-intent signatures with plain ecrecover. An SCA
12159
- // signer silently produces a signature over a wrapped hash that Gateway
12160
- // cannot verify, and Circle Wallets' KMS rejects the typed data up front
12161
- // with an opaque `<nil>/<nil>` error. Short-circuit with a clear message
12162
- // when we can detect bytecode at the signer address. See DEVX-2774.
12163
- //
12164
- // Duck-typed on readBytecode rather than `instanceof EvmAdapter` because
12165
- // each consumer package bundles its own copy of the base class and the
12166
- // `instanceof` identity check fails across package boundaries.
12167
- //
12168
- // Empty string is defended against because assertSignerIsEoa would
12169
- // otherwise call eth_getCode('') on the RPC.
12170
- const hasResolvedSigner = typeof address === 'string' && address.length > 0;
12171
- if (hasResolvedSigner && chain.type === 'evm' && isEvmAdapterLike(adapter)) {
12172
- await assertSignerIsEoa(adapter, address, chain);
12173
- }
12174
- const firstIntent = groupIntents[0];
12175
- const typedData = groupIntents.length === 1 && firstIntent ? evmSigningData(firstIntent) : evmSigningData(groupIntents);
12176
- const signRequest = await adapter.prepareAction('gateway.v1.signBurnIntents', {
12177
- typedData,
12178
- chain
12179
- }, operationContext);
12180
- const sig = await signRequest.execute();
12181
- return {
12182
- intents: groupIntents,
12183
- signature: sig
12184
- };
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);
12185
12403
  }
12186
12404
 
12187
12405
  /**
12188
12406
  * Add an EVM intent into the batched EVM group map.
12189
12407
  *
12190
12408
  * On EVM, all intents for the same adapter are batched into a single
12191
- * group so that they can be signed in one EIP-712 `BurnIntentSet`
12192
- * operation.
12409
+ * group. The signing step uses `chainsByDomain` to preserve EOA batching
12410
+ * while signing ERC-1271 intents separately on their source chains.
12193
12411
  *
12194
12412
  * @param intent - The burn intent to group.
12195
12413
  * @param alloc - The allocation that resolved to this intent.
@@ -12206,6 +12424,7 @@ function evmSigningData(burnIntent) {
12206
12424
  const existing = evmGroups.get(alloc.adapter);
12207
12425
  if (existing) {
12208
12426
  existing.intents.push(intent);
12427
+ existing.chainsByDomain.set(alloc.chain.gateway.domain, alloc.chain);
12209
12428
  } else {
12210
12429
  evmGroups.set(alloc.adapter, {
12211
12430
  adapter: alloc.adapter,
@@ -12213,6 +12432,12 @@ function evmSigningData(burnIntent) {
12213
12432
  intents: [
12214
12433
  intent
12215
12434
  ],
12435
+ chainsByDomain: new Map([
12436
+ [
12437
+ alloc.chain.gateway.domain,
12438
+ alloc.chain
12439
+ ]
12440
+ ]),
12216
12441
  address: alloc.sourceSigner
12217
12442
  });
12218
12443
  }
@@ -13738,7 +13963,8 @@ function throwNetworkMismatch(expected, actual) {
13738
13963
  };
13739
13964
  }
13740
13965
  /**
13741
- * 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).
13742
13968
  *
13743
13969
  * @param intents - Burn intents from estimate response.
13744
13970
  * @param allocations - Normalized allocations used to map domain → adapter/chain.
@@ -16631,22 +16857,32 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
16631
16857
  *
16632
16858
  * Single-intent sets become one burnIntent + signature; multi-intent sets become burnIntentSet + signature.
16633
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
+ *
16634
16865
  * @param signedSets - Signed intent sets (intents + signature per signer).
16635
16866
  * @returns Array of transfer payloads for POST /v1/transfer.
16636
16867
  */ function buildTransferRequestBody(signedSets) {
16637
16868
  return signedSets.map((set)=>{
16638
16869
  const firstIntent = set.intents[0];
16870
+ const contractSigner = set.contractSigner === true ? {
16871
+ contractSigner: true
16872
+ } : {};
16639
16873
  if (set.intents.length === 1 && firstIntent) {
16640
16874
  return {
16641
16875
  burnIntent: serializeBurnIntent(firstIntent),
16642
- signature: set.signature
16876
+ signature: set.signature,
16877
+ ...contractSigner
16643
16878
  };
16644
16879
  }
16645
16880
  return {
16646
16881
  burnIntentSet: {
16647
16882
  intents: set.intents.map(serializeBurnIntent)
16648
16883
  },
16649
- signature: set.signature
16884
+ signature: set.signature,
16885
+ ...contractSigner
16650
16886
  };
16651
16887
  });
16652
16888
  }
@@ -17009,11 +17245,16 @@ const BPS_DIVISOR = 100_000n;
17009
17245
  return required;
17010
17246
  }
17011
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
+ }
17012
17252
  /**
17013
- * 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.
17014
17255
  *
17015
17256
  * @param adapterGroups - Groups from groupIntentsByAdapter.
17016
- * @returns Promise of signed sets (intents + signature) for buildTransferRequestBody.
17257
+ * @returns Promise of signed sets for buildTransferRequestBody.
17017
17258
  *
17018
17259
  * @example
17019
17260
  * ```typescript
@@ -17026,9 +17267,10 @@ const BPS_DIVISOR = 100_000n;
17026
17267
  if (group.chain.type === 'solana') {
17027
17268
  return signSolanaIntentGroup(group);
17028
17269
  }
17029
- return [
17030
- await signEvmIntentGroup(group)
17031
- ];
17270
+ return await signEvmIntentGroup({
17271
+ ...group,
17272
+ chainsByDomain: requireEvmChainsByDomain(group)
17273
+ });
17032
17274
  }));
17033
17275
  return nested.flat();
17034
17276
  }
@@ -17678,7 +17920,8 @@ async function runSpendNormalPath(params, destChain, useForwarder, dispatcher, s
17678
17920
  signedSetCount: signedSets.length,
17679
17921
  signatures: signedSets.map((s)=>({
17680
17922
  intentCount: s.intents.length,
17681
- signature: s.signature
17923
+ signature: s.signature,
17924
+ contractSigner: s.contractSigner === true
17682
17925
  }))
17683
17926
  }
17684
17927
  });
@@ -20423,7 +20666,11 @@ const removeFundParamsSchema = z.object({
20423
20666
  // Remove Fund Operations
20424
20667
  // ---------------------------------------------------------------------------
20425
20668
  /**
20426
- * Kick off a delayed fund removal from an account.
20669
+ * Kick off a delayed recovery fund removal from an account.
20670
+ *
20671
+ * Use `initiateRemoveFund` only as a trustless fallback when the normal spend
20672
+ * flow is unavailable. For day-to-day movement out of a Unified Balance, use
20673
+ * `spend`.
20427
20674
  *
20428
20675
  * Validates `from` and `amount`, resolves the chain and token via
20429
20676
  * {@link resolveRemoveFundParams}, selects the matching provider, then calls
@@ -20460,7 +20707,10 @@ const removeFundParamsSchema = z.object({
20460
20707
  return provider.initiateRemoveFund(resolved);
20461
20708
  }
20462
20709
  /**
20463
- * Complete a fund removal once the 7-day activation period has passed.
20710
+ * Complete a recovery fund removal once the 7-day withdrawal delay has passed.
20711
+ *
20712
+ * Use `removeFund` only as a trustless fallback when the normal spend flow is
20713
+ * unavailable. For day-to-day movement out of a Unified Balance, use `spend`.
20464
20714
  *
20465
20715
  * Validates `from`, resolves the chain and token via
20466
20716
  * {@link resolveRemoveFundParams}, selects the matching provider, then calls
@@ -20549,13 +20799,18 @@ const removeFundParamsSchema = z.object({
20549
20799
  /** SDK name used in telemetry payloads. */ const SDK_NAME = resolveKitSdkName(pkg.name);
20550
20800
  /**
20551
20801
  * A high-level class-based interface for cross-chain USDC deposits,
20552
- * spending, balance queries, delegation management, and withdrawals.
20802
+ * spending, balance queries, delegation management, and recovery fund removals.
20553
20803
  *
20554
20804
  * UnifiedBalanceKit provides a familiar class-based API for developers who
20555
20805
  * prefer traditional object-oriented patterns. The class maintains an
20556
20806
  * internal context and provides methods that delegate to the standalone
20557
20807
  * operation functions exported by this package.
20558
20808
  *
20809
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
20810
+ * trustless recovery path for situations where the normal spend flow is
20811
+ * unavailable, and it requires a 7-day withdrawal delay before funds can be
20812
+ * removed.
20813
+ *
20559
20814
  * @remarks
20560
20815
  * For functional usage, import and use the operations directly:
20561
20816
  * ```typescript
@@ -20776,7 +21031,11 @@ const removeFundParamsSchema = z.object({
20776
21031
  });
20777
21032
  }
20778
21033
  /**
20779
- * Kick off a delayed fund removal from an account.
21034
+ * Kick off a delayed recovery fund removal from an account.
21035
+ *
21036
+ * Use this only as a trustless fallback when the normal spend flow is
21037
+ * unavailable. For day-to-day movement out of a Unified Balance, use
21038
+ * `spend`.
20780
21039
  *
20781
21040
  * @param params - The account owner's adapter context, amount, and
20782
21041
  * optional token type.
@@ -20790,7 +21049,12 @@ const removeFundParamsSchema = z.object({
20790
21049
  });
20791
21050
  }
20792
21051
  /**
20793
- * Complete a fund removal once the activation period has passed.
21052
+ * Complete a recovery fund removal once the 7-day withdrawal delay has
21053
+ * passed.
21054
+ *
21055
+ * Use this only as a trustless fallback when the normal spend flow is
21056
+ * unavailable. For day-to-day movement out of a Unified Balance, use
21057
+ * `spend`.
20794
21058
  *
20795
21059
  * @param params - The account owner context matching the original
20796
21060
  * fund removal initiation.
@@ -20917,6 +21181,11 @@ registerKit(`${pkg.name}/${pkg.version}`);
20917
21181
  * Internally holds a persistent {@link UnifiedBalanceKit} instance so that
20918
21182
  * event dispatchers and custom fee policies are preserved across calls.
20919
21183
  *
21184
+ * Use {@link AppKitUnifiedBalance.spend} for normal movement out of a Unified
21185
+ * Balance. {@link AppKitUnifiedBalance.removeFund} is a trustless recovery path
21186
+ * for situations where the normal spend flow is unavailable, and it requires a
21187
+ * 7-day withdrawal delay after {@link AppKitUnifiedBalance.initiateRemoveFund}.
21188
+ *
20920
21189
  * @example
20921
21190
  * ```typescript
20922
21191
  * import { AppKit } from '@circle-fin/app-kit'
@@ -21128,7 +21397,12 @@ registerKit(`${pkg.name}/${pkg.version}`);
21128
21397
  return this.kit.removeDelegate(params);
21129
21398
  }
21130
21399
  /**
21131
- * Kick off a delayed fund removal from an account.
21400
+ * Initiate a trustless recovery removal from an account.
21401
+ *
21402
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
21403
+ * recovery path for situations where the normal spend flow is unavailable.
21404
+ * Calling this method starts the 7-day withdrawal delay before the removal can
21405
+ * be completed.
21132
21406
  *
21133
21407
  * @param params - The account owner's adapter context, amount, and token.
21134
21408
  * @returns Promise resolving to the initiation details.
@@ -21147,11 +21421,16 @@ registerKit(`${pkg.name}/${pkg.version}`);
21147
21421
  return this.kit.initiateRemoveFund(params);
21148
21422
  }
21149
21423
  /**
21150
- * Complete a fund removal once the activation period has passed.
21424
+ * Complete a trustless recovery removal after the withdrawal delay.
21425
+ *
21426
+ * Use `spend` for normal movement out of a Unified Balance. `removeFund` is a
21427
+ * recovery path for situations where the normal spend flow is unavailable.
21428
+ * Both EVM and Solana removals require a 7-day withdrawal delay after
21429
+ * `initiateRemoveFund` before funds can be removed.
21151
21430
  *
21152
21431
  * @param params - The account owner context matching the original initiation.
21153
21432
  * @returns Promise resolving to the fund removal details.
21154
- * @throws {KitError} If the activation period has not elapsed or the
21433
+ * @throws {KitError} If the withdrawal delay has not elapsed or the
21155
21434
  * on-chain transaction fails.
21156
21435
  *
21157
21436
  * @example