Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion examples/arbitrum-london/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "next build --turbopack",
"start": "next start",
"lint": "eslint",
"test": "vitest run",
"build:contracts": "cd contracts && forge build",
"test:contracts": "cd contracts && forge test"
},
Expand All @@ -31,12 +32,17 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react-swc": "^4.3.1",
"eslint": "^9",
"eslint-config-next": "15.5.18",
"jsdom": "^29.1.1",
"tailwindcss": "^4",
"typescript": "^6"
"typescript": "^6",
"vitest": "^4.1.1"
}
}
86 changes: 86 additions & 0 deletions examples/arbitrum-london/src/ui/SequencerFeeRow.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, render, screen, waitFor } from '@testing-library/react'
import { previewSequencerFee, type SequencerFeePreview } from '@txkit/arbitrum-adapter'
import type { ReactNode } from 'react'
import { formatGwei, toHex } from 'viem'
import { afterEach, describe, expect, it, vi } from 'vitest'

import { SequencerFeeRow } from './SequencerFeeRow'


vi.mock('@txkit/arbitrum-adapter', () => ({
previewSequencerFee: vi.fn(),
}))

const mockedPreviewSequencerFee = vi.mocked(previewSequencerFee)

const SAMPLE_TO = '0x000000000000000000000000000000000000dEaD' as const
const SAMPLE_CALLDATA = '0xabcdef' as const

/**
* Wei amounts picked to format as clean, distinct gwei values (1 gwei = 1e9
* wei): 1.5 / 4.2 / 5.7 gwei. Distinct strings let each row be asserted on
* its own without ambiguity.
*/
const SAMPLE_FEE_PREVIEW: SequencerFeePreview = {
l2GasEstimate: '0x4c4b40',
l1CalldataBytes: 132,
l1BaseFeeWei: '0x3b9aca00',
l1FeeWei: toHex(1_500_000_000n),
l2FeeWei: toHex(4_200_000_000n),
totalFeeWei: toHex(5_700_000_000n),
isCompressed: false,
}

const renderWithQueryClient = (node: ReactNode) => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
})

return render(<QueryClientProvider client={queryClient}>{node}</QueryClientProvider>)
}

afterEach(() => {
cleanup()
vi.clearAllMocks()
})

describe('SequencerFeeRow', () => {
it('shows the estimating hint while the preview request is in flight', () => {
const pendingForever = new Promise<SequencerFeePreview | null>(() => {})
mockedPreviewSequencerFee.mockReturnValue(pendingForever)

renderWithQueryClient(<SequencerFeeRow chain="eip155:42161" to={SAMPLE_TO} calldata={SAMPLE_CALLDATA} />)

expect(screen.getByText(/estimating sequencer fee/i)).toBeTruthy()
})

it('renders nothing once the preview resolves to null', async () => {
mockedPreviewSequencerFee.mockResolvedValue(null)

const { container } = renderWithQueryClient(
<SequencerFeeRow chain="eip155:42161" to={SAMPLE_TO} calldata={SAMPLE_CALLDATA} />,
)

await waitFor(() => {
expect(screen.queryByText(/estimating sequencer fee/i)).toBeNull()
})
expect(screen.queryByText(/estimated sequencer fee/i)).toBeNull()
expect(container.textContent).toBe('')
})

it('renders the L1, L2, and total fee rows when the preview resolves with data', async () => {
mockedPreviewSequencerFee.mockResolvedValue(SAMPLE_FEE_PREVIEW)

renderWithQueryClient(<SequencerFeeRow chain="eip155:42161" to={SAMPLE_TO} calldata={SAMPLE_CALLDATA} />)

expect(await screen.findByText('Estimated sequencer fee')).toBeTruthy()
expect(screen.getByText('L1 calldata')).toBeTruthy()
expect(screen.getByText('L2 compute')).toBeTruthy()
expect(screen.getByText('Total')).toBeTruthy()

expect(screen.getByText(`${formatGwei(BigInt(SAMPLE_FEE_PREVIEW.l1FeeWei))} gwei`)).toBeTruthy()
expect(screen.getByText(`${formatGwei(BigInt(SAMPLE_FEE_PREVIEW.l2FeeWei))} gwei`)).toBeTruthy()
expect(screen.getByText(`${formatGwei(BigInt(SAMPLE_FEE_PREVIEW.totalFeeWei))} gwei`)).toBeTruthy()
})
})
23 changes: 23 additions & 0 deletions examples/arbitrum-london/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from 'vitest/config'
import { resolve } from 'path'


/**
* Component tests run in jsdom and reuse the app's `@/*` path alias so a
* spec can import `@/src/...` exactly like the components do. The React SWC
* plugin owns the JSX transform here: the app tsconfig keeps jsx: "preserve"
* for Next, which the test transform must not inherit.
*/
export default defineConfig({
plugins: [ react() ],
test: {
environment: 'jsdom',
include: [ 'src/**/*.spec.{ts,tsx}' ],
},
resolve: {
alias: {
'@': resolve(__dirname, './'),
},
},
})
29 changes: 27 additions & 2 deletions packages/arbitrum-adapter/src/__tests__/arbitrum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ const createMockClient = (options: {
components?: readonly [ bigint, bigint, bigint, bigint ],
blockNumber?: bigint,
throwOnRead?: boolean,
throwOnBlockNumber?: boolean,
onRead?: (params: MockReadParams) => void,
}): PublicClient => {
const { components, blockNumber, throwOnRead, onRead } = options
const { components, blockNumber, throwOnRead, throwOnBlockNumber, onRead } = options
const componentsValue = components ?? [ 1000000n, 200000n, 100000000n, 30000000000n ]
const blockNumberValue = blockNumber ?? 12345n

Expand All @@ -85,7 +86,13 @@ const createMockClient = (options: {

return componentsValue
},
getBlockNumber: async () => blockNumberValue,
getBlockNumber: async () => {
if (throwOnBlockNumber) {
throw new Error('block number unavailable')
}

return blockNumberValue
},
} as unknown as PublicClient
}

Expand Down Expand Up @@ -229,6 +236,24 @@ describe('arbitrum-adapter / sequencer', () => {
expect(preview).toBeNull()
})

it('previewSequencerFee returns the fee even when getBlockNumber rejects', async () => {
const client = createMockClient({
components: [ 1000000n, 200000n, 100000000n, 30000000000n ],
throwOnBlockNumber: true,
})
const preview = await previewSequencerFee(client, {
chain: 'eip155:42161',
to: DEAD_ADDRESS,
calldata: '0xabcdef',
})

expect(preview).not.toBeNull()
expect(preview?.l1FeeWei).toBe(toHex(200000n * 100000000n))
expect(preview?.l2FeeWei).toBe(toHex(800000n * 100000000n))
expect(preview?.totalFeeWei).toBe(toHex(1000000n * 100000000n))
expect(preview?.previewBlock).toBeUndefined()
})

it('previewSequencerFee counts calldata bytes', async () => {
const client = createMockClient({})
const empty = await previewSequencerFee(client, {
Expand Down
17 changes: 15 additions & 2 deletions packages/arbitrum-adapter/src/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ const NODE_INTERFACE_GAS_ESTIMATE_COMPONENTS_ABI = [
},
] as const

/**
* Best-effort current block height for the preview metadata. The block
* number is informational only, so a failed lookup must not discard an
* otherwise valid fee estimate - it degrades to `undefined` instead.
*/
const resolvePreviewBlock = async (client: PublicClient): Promise<number | undefined> => {
try {
return Number(await client.getBlockNumber())
} catch {
return undefined
}
}

/**
* Compute a live sequencer-fee preview for a calldata payload on the given
* Arbitrum chain. Reads NodeInterface.gasEstimateComponents (precompile
Expand Down Expand Up @@ -125,10 +138,10 @@ export const previewSequencerFee = async (
account: from,
})

const blockNumber = await client.getBlockNumber()
const l2GasUnits = gasEstimate - gasEstimateForL1
const l1BaseFeeWeiValue = l1BaseFeeWei ? BigInt(l1BaseFeeWei) : l1BaseFeeEstimate
const calldataByteCount = (calldata.length - 2) / 2
const previewBlock = await resolvePreviewBlock(client)

return {
l2GasEstimate: toHex(l2GasUnits),
Expand All @@ -138,7 +151,7 @@ export const previewSequencerFee = async (
l2FeeWei: toHex(l2GasUnits * baseFee),
totalFeeWei: toHex(gasEstimate * baseFee),
isCompressed: checkIsNova(chain),
previewBlock: Number(blockNumber),
previewBlock,
}
} catch {
return null
Expand Down
Loading
Loading