diff --git a/packages/arbitrum-adapter/CHANGELOG.md b/packages/arbitrum-adapter/CHANGELOG.md index d3fccd9d..027116b2 100644 --- a/packages/arbitrum-adapter/CHANGELOG.md +++ b/packages/arbitrum-adapter/CHANGELOG.md @@ -1,5 +1,11 @@ # @txkit/arbitrum-adapter +## [Unreleased] + +- `previewSequencerFee` is now a live estimation rather than a stub. It reads `NodeInterface.gasEstimateComponents` (precompile 0xC8) through a caller-supplied viem `PublicClient`, splitting the cost into the L2 compute portion and the L1 calldata-posting portion (both priced at the L2 base fee, per the Arbitrum Nitro fee model) and filling every `SequencerFeePreview` field. +- Signature change (alpha, surface was flagged unstable): `previewSequencerFee(client, { chain, to, calldata, from?, l1BaseFeeWei? })` is now async and returns `Promise`. A `to` is required (the precompile simulates the call) and the viem client is injected so the function stays RPC-agnostic and testable. Any failure (RPC down, simulated call reverts, malformed calldata) returns `null` - the preview is advisory and never blocks signing. +- `viem` added as a peer dependency (`>=2`). + ## [0.1.0-alpha.0] - 2026-05-29 Initial skeleton release for the Arbitrum Open House London Buildathon Week 1 deliverable. diff --git a/packages/arbitrum-adapter/README.md b/packages/arbitrum-adapter/README.md index 9880ea0a..12e1a514 100644 --- a/packages/arbitrum-adapter/README.md +++ b/packages/arbitrum-adapter/README.md @@ -5,7 +5,7 @@ Bridge Arbitrum specifics and `@txkit/tx-protocol` `PreparedEnvelope`. Attach L1->L2 bridge intents, retryable-ticket UX hints, and sequencer-fee previews to an envelope; decode Arbitrum-flavoured calldata. -> **v0.1.0-alpha** - skeleton scaffolded for the Arbitrum Open House London Buildathon (June 14 deadline). Surface stable; helper bodies will harden in alpha.1 with viem integration. +> **v0.1.0-alpha** - scaffolded for the Arbitrum Open House London Buildathon (June 14 deadline). `previewSequencerFee` is live (viem-driven precompile read); the decoder registry and the remaining helper coverage harden in alpha.1. ## Why this exists @@ -21,9 +21,11 @@ The PreparedTransaction Envelope (ERC-8265, [PR #1753](https://github.com/ethere ## Install ```bash -npm install @txkit/arbitrum-adapter@alpha @txkit/tx-protocol@alpha +npm install @txkit/arbitrum-adapter@alpha @txkit/tx-protocol@alpha viem ``` +`viem` is a peer dependency - `previewSequencerFee` reads Arbitrum precompiles through a viem `PublicClient` you supply. + ## Usage ### Attach an L1->L2 bridge intent @@ -67,6 +69,30 @@ const withRetryable = attachRetryableHints(envelope, { ### Preview the sequencer fee +Compute a live preview from an Arbitrum RPC, then attach it to the envelope: + +```ts +import { createPublicClient, http } from 'viem' +import { arbitrum } from 'viem/chains' +import { previewSequencerFee, attachSequencerFeePreview } from '@txkit/arbitrum-adapter' + +const client = createPublicClient({ chain: arbitrum, transport: http() }) + +const preview = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // inner call target + calldata: '0xa9059cbb...', // inner calldata +}) +// -> { l2GasEstimate, l1CalldataBytes, l1FeeWei, l2FeeWei, totalFeeWei, isCompressed, previewBlock } +// or null if the RPC is unreachable or the simulated call reverts + +const withFee = preview ? attachSequencerFeePreview(envelope, preview) : envelope +``` + +`previewSequencerFee` reads `NodeInterface.gasEstimateComponents` (precompile 0xC8), which simulates the `to` + `calldata` call and splits the cost into the L2 compute portion and the L1 calldata-posting portion. Both are priced at the L2 base fee (the L1 component is denominated in L2 gas units). Pass `from` to set the simulated `msg.sender`, or `l1BaseFeeWei` to pin the reported L1 base fee. + +You can also attach a producer-supplied preview directly, without an RPC: + ```ts import { attachSequencerFeePreview } from '@txkit/arbitrum-adapter' @@ -112,7 +138,7 @@ const decoded = decodeArbitrumCall({ - `attachSequencerFeePreview(envelope, preview)` - attach `meta.arbitrum.sequencerFee` - `extractSequencerFeePreview(envelope)` - read `meta.arbitrum.sequencerFee` - `isSequencerFeePreview(value)` - type guard -- `previewSequencerFee({ chain, calldata, l1BaseFeeWei? })` - skeleton stub, returns `null` until alpha.1 +- `previewSequencerFee(client, { chain, to, calldata, from?, l1BaseFeeWei? })` - async; live preview via `NodeInterface.gasEstimateComponents` read through the supplied viem `PublicClient`. Returns `null` on RPC failure or if the simulated call reverts - `NOVA_USES_COMPRESSED_CALLDATA` - constant flag (`true`) ### Decoder @@ -122,7 +148,7 @@ const decoded = decodeArbitrumCall({ ## Status -Skeleton. Helper bodies for `previewSequencerFee` and the decoder registry will harden in alpha.1 with viem-driven precompile reads (`ArbGasInfo`, `NodeInterface`) plus expanded coverage of Hop bonders per token, Across SpokePool per chain, Stargate routers, Camelot, GMX, Pendle, and Aave V3 on Arbitrum One. +`previewSequencerFee` is live via a viem-driven `NodeInterface` read. The decoder registry is still a seed and will harden in alpha.1 with expanded coverage of Hop bonders per token, Across SpokePool per chain, Stargate routers, Camelot, GMX, Pendle, and Aave V3 on Arbitrum One. Two known alpha.1 refinements for the fee preview: Nova `l1CalldataBytes` currently reports the raw byte count (not the post-Brotli/DAC size, though the fee itself is accurate), and a revert-resilient `ArbGasInfo` fallback would let the L1 portion survive when the simulated call reverts. Tracking issue: open one in [github.com/txkit/mono/issues](https://github.com/txkit/mono/issues) if you have a buildathon use case that needs prioritisation. diff --git a/packages/arbitrum-adapter/package.json b/packages/arbitrum-adapter/package.json index 03e0c270..5f0b7e9d 100644 --- a/packages/arbitrum-adapter/package.json +++ b/packages/arbitrum-adapter/package.json @@ -68,9 +68,13 @@ "dependencies": { "@txkit/tx-protocol": "workspace:^" }, + "peerDependencies": { + "viem": ">=2" + }, "devDependencies": { "tsup": "^8.4.0", "typescript": "^5.7.3", + "viem": "^2.48.4", "vitest": "^4.1.1" }, "sideEffects": false diff --git a/packages/arbitrum-adapter/src/__tests__/arbitrum.spec.ts b/packages/arbitrum-adapter/src/__tests__/arbitrum.spec.ts index 1cfaad4c..73386366 100644 --- a/packages/arbitrum-adapter/src/__tests__/arbitrum.spec.ts +++ b/packages/arbitrum-adapter/src/__tests__/arbitrum.spec.ts @@ -1,3 +1,4 @@ +import { toHex, type PublicClient } from 'viem' import { describe, expect, it } from 'vitest' import { attachBridgeIntent, extractBridgeIntent, isBridgeIntent } from '../bridge' @@ -50,6 +51,44 @@ const SAMPLE_PREVIEW: SequencerFeePreview = { isCompressed: false, } +const DEAD_ADDRESS = '0x000000000000000000000000000000000000dEaD' as const + +type MockReadParams = { + address: string, + functionName: string, + args: readonly unknown[], + account?: unknown, +} + +/** + * Minimal fake viem PublicClient. previewSequencerFee only touches + * readContract + getBlockNumber, so we stub exactly those two and cast. + * The gasEstimateComponents tuple defaults to the happy-path values used + * across the assertions below. + */ +const createMockClient = (options: { + components?: readonly [ bigint, bigint, bigint, bigint ], + blockNumber?: bigint, + throwOnRead?: boolean, + onRead?: (params: MockReadParams) => void, +}): PublicClient => { + const { components, blockNumber, throwOnRead, onRead } = options + const componentsValue = components ?? [ 1000000n, 200000n, 100000000n, 30000000000n ] + const blockNumberValue = blockNumber ?? 12345n + + return { + readContract: async (params: MockReadParams) => { + onRead?.(params) + if (throwOnRead) { + throw new Error('rpc down') + } + + return componentsValue + }, + getBlockNumber: async () => blockNumberValue, + } as unknown as PublicClient +} + describe('arbitrum-adapter / bridge', () => { it('attachBridgeIntent populates meta.arbitrum.bridge', () => { const envelope = attachBridgeIntent(MINIMAL_ENVELOPE as unknown as Parameters[0], SAMPLE_BRIDGE) @@ -119,11 +158,94 @@ describe('arbitrum-adapter / sequencer', () => { expect(isSequencerFeePreview({ l2GasEstimate: '0x1' })).toBe(false) }) - it('previewSequencerFee is a skeleton stub - returns null', () => { - const preview = previewSequencerFee({ chain: 'eip155:42161', calldata: '0x' }) + it('previewSequencerFee computes every field from gasEstimateComponents', async () => { + const client = createMockClient({ + components: [ 1000000n, 200000n, 100000000n, 30000000000n ], + blockNumber: 12345n, + }) + const preview = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0xabcdef', + }) + + expect(preview).toEqual({ + l2GasEstimate: toHex(800000n), + l1CalldataBytes: 3, + l1BaseFeeWei: toHex(30000000000n), + l1FeeWei: toHex(200000n * 100000000n), + l2FeeWei: toHex(800000n * 100000000n), + totalFeeWei: toHex(1000000n * 100000000n), + isCompressed: false, + previewBlock: 12345, + }) + }) + + it('previewSequencerFee reads gasEstimateComponents on NodeInterface 0xC8', async () => { + let captured: MockReadParams | undefined + const client = createMockClient({ onRead: (params) => { captured = params } }) + await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0x1234', + }) + + expect(captured?.address).toBe('0x00000000000000000000000000000000000000C8') + expect(captured?.functionName).toBe('gasEstimateComponents') + expect(captured?.args).toEqual([ DEAD_ADDRESS, false, '0x1234' ]) + }) + + it('previewSequencerFee flags Nova calldata compression', async () => { + const client = createMockClient({}) + const preview = await previewSequencerFee(client, { + chain: 'eip155:42170', + to: DEAD_ADDRESS, + calldata: '0x', + }) + + expect(preview?.isCompressed).toBe(true) + }) + + it('previewSequencerFee honours an l1BaseFeeWei override', async () => { + const client = createMockClient({ components: [ 1000000n, 200000n, 100000000n, 30000000000n ] }) + const preview = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0x', + l1BaseFeeWei: '0x1', + }) + + expect(preview?.l1BaseFeeWei).toBe('0x1') + }) + + it('previewSequencerFee returns null when the precompile read fails', async () => { + const client = createMockClient({ throwOnRead: true }) + const preview = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0x', + }) + expect(preview).toBeNull() }) + it('previewSequencerFee counts calldata bytes', async () => { + const client = createMockClient({}) + const empty = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0x', + }) + const filled = await previewSequencerFee(client, { + chain: 'eip155:42161', + to: DEAD_ADDRESS, + calldata: '0xdeadbeef', + }) + + expect(empty?.l1CalldataBytes).toBe(0) + expect(filled?.l1CalldataBytes).toBe(4) + }) + it('exposes the Nova compression flag', () => { expect(NOVA_USES_COMPRESSED_CALLDATA).toBe(true) }) diff --git a/packages/arbitrum-adapter/src/sequencer.ts b/packages/arbitrum-adapter/src/sequencer.ts index 66793c01..00dfb13f 100644 --- a/packages/arbitrum-adapter/src/sequencer.ts +++ b/packages/arbitrum-adapter/src/sequencer.ts @@ -1,3 +1,5 @@ +import { toHex, type PublicClient } from 'viem' + import type { PreparedEnvelope } from '@txkit/tx-protocol' import type { ArbitrumChainId, EnvelopeWithArbitrum, SequencerFeePreview } from './types' @@ -12,7 +14,7 @@ import type { ArbitrumChainId, EnvelopeWithArbitrum, SequencerFeePreview } from */ export const NOVA_USES_COMPRESSED_CALLDATA = true -const isNova = (chainId: ArbitrumChainId): boolean => chainId === 'eip155:42170' +const checkIsNova = (chainId: ArbitrumChainId): boolean => chainId === 'eip155:42170' /** * Attach a sequencer-fee preview to a PreparedEnvelope's @@ -53,10 +55,48 @@ export const extractSequencerFeePreview = (envelope: EnvelopeWithArbitrum): Sequ } /** - * Compute a sequencer-fee preview for a calldata payload on the given - * Arbitrum chain. Skeleton stub - returns `null` until alpha.1, when the - * estimation will use viem's `arbGasInfo.getPricesInWei` precompile read - * plus `NodeInterface.gasEstimateL1Component`. + * NodeInterface is a virtual precompile (0xC8) the Arbitrum node resolves + * via eth_call - it is not actually deployed. `gasEstimateComponents` + * simulates the (to, data) call and splits the estimate into the L2 + * compute portion plus the L1 calldata-posting component. + * + * The canonical method is `payable`, but we own this ABI and mark it + * `view` so viem's `readContract` accepts it; eth_call ignores the + * declared mutability. Verified live against Arbitrum One mainnet. + */ +const NODE_INTERFACE_ADDRESS = '0x00000000000000000000000000000000000000C8' as const + +const NODE_INTERFACE_GAS_ESTIMATE_COMPONENTS_ABI = [ + { + type: 'function', + name: 'gasEstimateComponents', + stateMutability: 'view', + inputs: [ + { name: 'to', type: 'address' }, + { name: 'contractCreation', type: 'bool' }, + { name: 'data', type: 'bytes' }, + ], + outputs: [ + { name: 'gasEstimate', type: 'uint64' }, + { name: 'gasEstimateForL1', type: 'uint64' }, + { name: 'baseFee', type: 'uint256' }, + { name: 'l1BaseFeeEstimate', type: 'uint256' }, + ], + }, +] as const + +/** + * Compute a live sequencer-fee preview for a calldata payload on the given + * Arbitrum chain. Reads NodeInterface.gasEstimateComponents (precompile + * 0xC8) through the supplied viem client, which simulates the `to` + `data` + * call and splits the cost into the L2 compute portion and the L1 + * calldata-posting portion. Both portions are priced at the L2 base fee + * returned by the same call - the L1 component is expressed in L2 gas units, + * so `l1FeeWei = gasEstimateForL1 * baseFee`. + * + * Returns `null` on any failure - RPC unreachable, the simulated call + * reverts, or malformed calldata. The preview is advisory wallet UX and + * must never block signing, so failures degrade to `null` rather than throw. * * Reference contract addresses (precompiles, identical across Arbitrum * One / Sepolia / Nova): @@ -64,15 +104,44 @@ export const extractSequencerFeePreview = (envelope: EnvelopeWithArbitrum): Sequ * - ArbGasInfo 0x000000000000000000000000000000000000006C * - NodeInterface 0x00000000000000000000000000000000000000C8 */ -export const previewSequencerFee = (_args: { - chain: ArbitrumChainId, - calldata: `0x${string}`, - l1BaseFeeWei?: `0x${string}`, -}): SequencerFeePreview | null => { - // Skeleton: surface the chain compression flag so callers can still - // render the Nova vs One distinction even before live estimation lands. - const { chain } = _args - void chain - void isNova - return null +export const previewSequencerFee = async ( + client: PublicClient, + args: { + chain: ArbitrumChainId, + to: `0x${string}`, + calldata: `0x${string}`, + from?: `0x${string}`, + l1BaseFeeWei?: `0x${string}`, + }, +): Promise => { + const { chain, to, calldata, from, l1BaseFeeWei } = args + + try { + const [ gasEstimate, gasEstimateForL1, baseFee, l1BaseFeeEstimate ] = await client.readContract({ + address: NODE_INTERFACE_ADDRESS, + abi: NODE_INTERFACE_GAS_ESTIMATE_COMPONENTS_ABI, + functionName: 'gasEstimateComponents', + args: [ to, false, calldata ], + account: from, + }) + + const blockNumber = await client.getBlockNumber() + const l2GasUnits = gasEstimate - gasEstimateForL1 + const l1BaseFeeWeiValue = l1BaseFeeWei ? BigInt(l1BaseFeeWei) : l1BaseFeeEstimate + const calldataByteCount = (calldata.length - 2) / 2 + + return { + l2GasEstimate: toHex(l2GasUnits), + l1CalldataBytes: calldataByteCount, + l1BaseFeeWei: toHex(l1BaseFeeWeiValue), + l1FeeWei: toHex(gasEstimateForL1 * baseFee), + l2FeeWei: toHex(l2GasUnits * baseFee), + totalFeeWei: toHex(gasEstimate * baseFee), + isCompressed: checkIsNova(chain), + previewBlock: Number(blockNumber), + } + } catch { + + return null + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8242923d..cf70b0df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,9 +273,12 @@ importers: typescript: specifier: ^5.7.3 version: 5.9.3 + viem: + specifier: ^2.48.4 + version: 2.48.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6) vitest: specifier: ^4.1.1 - version: 4.1.1(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.1(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/core: devDependencies: @@ -393,7 +396,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.1 - version: 4.1.1(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.1(@types/node@25.5.0)(vite@8.0.8(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) zod-to-json-schema: specifier: ^3.25.2 version: 3.25.2(zod@3.25.76) @@ -15866,8 +15869,8 @@ snapshots: '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1)) @@ -15886,7 +15889,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -15897,22 +15900,22 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -15923,7 +15926,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3