When making a transaction on an EVM chain such as Ethereum, you need to specify three key parameters: gas limit, max fee per gas, and max priority fee per gas. These parameters are crucial for determining transaction costs and priority. In this post, we'll explore how to efficiently calculate these values using Viem and Wagmi libraries. You can experiment with the live demo here, and review the complete source code here.
Let's explore a practical implementation of gas fee calculation by building a simple web page. We'll create a user interface that displays different components of the gas fee and analyze the code step by step to understand the underlying mechanics.
import { PageMetaTags } from "@lib/next-ui/metadata/PageMetaTags"
import styled from "styled-components"
import { centeredContentColumn } from "@lib/ui/css/centeredContentColumn"
import { verticalPadding } from "@lib/ui/css/verticalPadding"
import { WebsiteNavigation } from "@lib/ui/website/navigation/WebsiteNavigation"
import { ProductLogo } from "../product/ProductLogo"
import { VStack } from "@lib/ui/css/stack"
import { MaxPriorityFeePerGas } from "./maxPriorityFeePerGas/MaxPriorityFeePerGas"
import { MaxFeePerGas } from "./maxFeePerGas/MaxFeePerGas"
import { BaseFee } from "./baseFee/BaseFee"
import { MaxFee } from "./maxFee/MaxFee"
export const PageContainer = styled.div`
${centeredContentColumn({
contentMaxWidth: 520,
})}
${verticalPadding(80)}
`
export const FeePage = () => (
<>
<PageMetaTags
title="EVM Gas Fee Calculator | Real-time Gas Price Monitoring"
description="Monitor real-time Ethereum gas fees, including max priority fees, base fees, and max fees per gas. Get accurate estimates for your EVM transactions."
/>
<WebsiteNavigation logo={<ProductLogo />}>
<PageContainer>
<VStack gap={40}>
<MaxFee />
<MaxPriorityFeePerGas />
<MaxFeePerGas />
<BaseFee />
</VStack>
</PageContainer>
</WebsiteNavigation>
</>
)
Since the MaxFee
component combines all fee components into a final value, we'll examine it last. Let's start by understanding the maxPriorityFeePerGas
parameter, which is a crucial part of the overall transaction fee structure.
import { VStack } from "@lib/ui/css/stack"
import { PriorityOptions } from "./options/PriorityOptions"
import { FeeSection } from "../FeeSection"
import { MaxPriorityFeePerGasTitle } from "./MaxPriorityFeePerGasTitle"
import { FeeChart } from "./chart/FeeChart"
export const MaxPriorityFeePerGas = () => {
return (
<FeeSection title={<MaxPriorityFeePerGasTitle />}>
<VStack gap={40}>
<PriorityOptions />
<FeeChart />
</VStack>
</FeeSection>
)
}
MaxPriorityFeePerGas, often referred to as the "priority fee," is the extra tip users include in their transactions to incentivize miners to process them faster. Unlike the base fee—which is determined by the network to reflect current demand—the maxPriorityFeePerGas is set by the user to ensure quicker transaction confirmations, especially during periods of high congestion.
In most Web3 applications, when initiating a transaction, users are presented with priority options for their transaction fees. This feature allows users to strategically choose between lower fees for non-urgent transactions and higher fees when faster confirmation is needed. The priority selection directly influences the maxPriorityFeePerGas parameter, giving users control over their transaction costs and confirmation speed.
export const feePriorities = ["low", "medium", "high"] as const
export type FeePriority = (typeof feePriorities)[number]
export const feePriorityPercentiles: Record<FeePriority, number> = {
low: 10,
medium: 50,
high: 90,
}
export const defaultFeePriority = "medium" as const
To determine the maxPriorityFeePerGas value, we analyze the historical priority fees paid by users in the last 10 blocks. This approach gives us a reliable estimate based on recent network activity. We fetch this data using Wagmi's useFeeHistory
hook, which retrieves fee information for different priority levels (low, medium, high). Each priority level corresponds to a specific percentile of fees paid - 10th percentile for low priority, 50th for medium, and 90th for high. This statistical approach ensures our estimates reflect the actual market conditions for transaction inclusion.
import { feePriorityPercentiles } from "../maxPriorityFeePerGas/core/FeePriority"
import { useFeeHistory } from "wagmi"
import { feePriorities } from "../maxPriorityFeePerGas/core/FeePriority"
import { useTransformQueryData } from "@lib/ui/query/hooks/useTransformQueryData"
import { GetFeeHistoryReturnType } from "viem"
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { arraysToRecord } from "@lib/utils/array/arraysToRecord"
export type PriorityFeeTimeseries = Record<
(typeof feePriorities)[number],
bigint[]
>
const transform = ({
reward,
}: GetFeeHistoryReturnType): PriorityFeeTimeseries =>
arraysToRecord(
feePriorities,
shouldBePresent(reward).reduce(
(acc, curr) => {
return acc.map((value, index) => [...value, curr[index]])
},
feePriorities.map(() => [] as bigint[]),
),
)
export const usePriorityFeeTimeseriesQuery = () => {
const query = useFeeHistory({
blockCount: 10,
rewardPercentiles: feePriorities.map((key) => feePriorityPercentiles[key]),
})
return useTransformQueryData(query, transform)
}
The Wagmi hook returns a nested array structure representing fee history data. Each inner array corresponds to a specific priority level (low, medium, high). To make this data more accessible and type-safe, we transform it into a structured record. The resulting format is a Record where each key is a priority level (e.g., "low", "medium", "high") and the corresponding value is an array of bigint values. This array contains the historical priority fees for that level, making it easier to analyze trends and calculate estimates for each priority tier.
To handle the transformation of query results efficiently and safely, we've created a custom hook called useTransformQueryData
. This hook is designed to transform raw query data into a more useful format while preserving the query's error handling capabilities. It's particularly useful when working with complex data structures like our fee history, where we need to reshape the data without losing the original query's metadata. The hook uses React's useMemo
to optimize performance by memoizing the transformed result:
import { useMemo } from "react"
import { Query } from "../Query"
type QueryBase<T> = Pick<Query<T>, "data" | "error">
export const useTransformQueryData = <
TInput,
TOutput,
TExtra extends object = {},
>(
queryResult: QueryBase<TInput> & TExtra,
transform: (data: TInput) => TOutput,
): QueryBase<TOutput> & Omit<TExtra, keyof QueryBase<TOutput>> => {
return useMemo(() => {
try {
return {
...queryResult,
data:
queryResult.data !== undefined
? transform(queryResult.data)
: undefined,
}
} catch (error) {
return {
...queryResult,
data: undefined,
error,
}
}
}, [queryResult, transform])
}
To display both historical and current priority fees, we need two data transformations. First, we use the hook to transform the raw fee history into a chart-friendly format. Second, to show the current fee for each priority level, we calculate the average of the historical values. This is accomplished by applying useTransformQueryData
again, this time using recordMap
to compute averages across each priority level's time series data. This dual transformation approach gives us both the temporal view needed for the chart and the current snapshot needed for immediate fee estimates.
import { recordMap } from "@lib/utils/record/recordMap"
import {
PriorityFeeTimeseries,
usePriorityFeeTimeseriesQuery,
} from "./usePriorityFeeTimeseriesQuery"
import { useTransformQueryData } from "@lib/ui/query/hooks/useTransformQueryData"
import { bigintAverage } from "@lib/utils/math/bigint/bigintAverage"
const transform = (timeseries: PriorityFeeTimeseries) =>
recordMap(timeseries, bigintAverage)
export const usePriorityFeesQuery = () => {
const query = usePriorityFeeTimeseriesQuery()
return useTransformQueryData(query, transform)
}
For cases where we only need the fee for a specific priority level (defaulting to medium
), the usePriorityFeeQuery
hook provides a streamlined way to access a single priority fee value. This hook builds on top of usePriorityFeesQuery
but extracts just the requested priority level's fee:
import { useTransformQueryData } from "@lib/ui/query/hooks/useTransformQueryData"
import {
defaultFeePriority,
FeePriority,
} from "../maxPriorityFeePerGas/core/FeePriority"
import { useCallback } from "react"
import { usePriorityFeesQuery } from "./usePriorityFeesQuery"
export const usePriorityFeeQuery = (
priority: FeePriority = defaultFeePriority,
) => {
const query = usePriorityFeesQuery()
return useTransformQueryData(
query,
useCallback((result) => result[priority], [priority]),
)
}
Having established our query infrastructure for priority fees, let's examine the first component in our implementation: the MaxPriorityFeePerGasTitle
. This component serves as a section header that dynamically displays the current average priority fee. It leverages the MatchQuery
component from RadzionKit for handling the asynchronous data state - rendering a Spinner
during loading and the formatted fee value once available.
import { gwei } from "@lib/chain/evm/utils/gwei"
import { fromChainAmount } from "@lib/chain/utils/fromChainAmount"
import { Spinner } from "@lib/ui/loaders/Spinner"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { formatAmount } from "@lib/utils/formatAmount"
import { usePriorityFeeQuery } from "../queries/usePriorityFeeQuery"
import { Text } from "@lib/ui/text"
export const MaxPriorityFeePerGasTitle = () => {
const query = usePriorityFeeQuery()
return (
<>
maxPriorityFeePerGas{" "}
<MatchQuery
value={query}
pending={() => <Spinner />}
success={(value) => (
<Text as="span" color="contrast">
{" = "}
{formatAmount(fromChainAmount(value, gwei.decimals))} {gwei.name}
</Text>
)}
/>
</>
)
}
The next crucial component in our implementation is the PriorityOptions
, which provides a visual representation of priority fee options. This component displays three priority levels (low, medium, high), each with its own color indicator and current fee value.
import styled from "styled-components"
import { feePriorities } from "../core/FeePriority"
import { HStack } from "@lib/ui/css/stack"
import { gwei } from "@lib/chain/evm/utils/gwei"
import { getFeePriorityColor } from "../utils/getFeePriorityColor"
import { Text } from "@lib/ui/text"
import { useTheme } from "styled-components"
import { formatAmount } from "@lib/utils/formatAmount"
import { fromChainAmount } from "@lib/chain/utils/fromChainAmount"
import { sameDimensions } from "@lib/ui/css/sameDimensions"
import { round } from "@lib/ui/css/round"
import { capitalizeFirstLetter } from "@lib/utils/capitalizeFirstLetter"
import { usePriorityFeesQuery } from "../../queries/usePriorityFeesQuery"
import { Spinner } from "@lib/ui/loaders/Spinner"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
const Identifier = styled.div`
${sameDimensions(8)}
${round}
`
export const PriorityOptions = () => {
const theme = useTheme()
const query = usePriorityFeesQuery()
return (
<HStack
fullWidth
justifyContent="space-between"
wrap="wrap"
alignItems="center"
gap={20}
>
{feePriorities.map((priority) => (
<HStack alignItems="center" gap={8} key={priority}>
<HStack alignItems="center" gap={6}>
<Identifier
style={{
background: getFeePriorityColor(theme, priority).toCssValue(),
}}
/>
<Text color="supporting">{capitalizeFirstLetter(priority)}</Text>
</HStack>
<MatchQuery
value={query}
pending={() => <Spinner />}
success={(value) => (
<Text>
{formatAmount(fromChainAmount(value[priority], gwei.decimals))}{" "}
{gwei.name}
</Text>
)}
/>
</HStack>
))}
</HStack>
)
}
To better understand fee trends, we can visualize how priority fees have fluctuated across the last 10 blocks using an interactive chart.
import { Spinner } from "@lib/ui/loaders/Spinner"
import { usePriorityFeeTimeseriesQuery } from "../../queries/usePriorityFeeTimeseriesQuery"
import { FeeChartContent } from "./FeeChartContent"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { recordMap } from "@lib/utils/record/recordMap"
export const FeeChart = () => {
const query = usePriorityFeeTimeseriesQuery()
return (
<MatchQuery
value={query}
pending={() => <Spinner />}
success={(value) => {
return (
<FeeChartContent
value={recordMap(value, (array) => array.map(Number))}
/>
)
}}
/>
)
}
The FeeChartContent
component handles the visualization of priority fee trends using our custom chart implementation. This component leverages various utility functions and styled components to create a responsive line chart. It normalizes the fee data arrays, generates appropriate Y-axis labels in Gwei units, and renders three overlapping line charts - one for each priority level (low, medium, high). Each line chart is color-coded to match its corresponding priority level and includes a gradient fill for better visual distinction. The chart is wrapped in an ElementSizeAware
component to ensure proper responsiveness, and it includes horizontal grid lines for easier value comparison. For a detailed explanation of the chart implementation itself, you can refer to a dedicated line chart blog post at /blog/linechart
.
import { ElementSizeAware } from "@lib/ui/base/ElementSizeAware"
import { VStack } from "@lib/ui/css/stack"
import { normalizeDataArrays } from "@lib/utils/math/normalizeDataArrays"
import { generateYLabels } from "@lib/ui/charts/utils/generateYLabels"
import { ChartYAxis } from "@lib/ui/charts/ChartYAxis"
import { useTheme } from "styled-components"
import { ChartHorizontalGridLines } from "@lib/ui/charts/ChartHorizontalGridLines"
import { LineChart } from "@lib/ui/charts/LineChart"
import { ValueProp } from "@lib/ui/props"
import { formatUnits } from "viem"
import { feePriorities, FeePriority } from "../core/FeePriority"
import { feeChartConfig } from "./config"
import { ChartSlice } from "@lib/ui/charts/ChartSlice"
import { ChartLabel } from "@lib/ui/charts/ChartLabel"
import { getFeePriorityColor } from "../utils/getFeePriorityColor"
import { TakeWholeSpaceAbsolutely } from "@lib/ui/css/takeWholeSpaceAbsolutely"
import { gwei } from "@lib/chain/evm/utils/gwei"
type FeeChartProps = ValueProp<Record<FeePriority, number[]>>
export const FeeChartContent = ({ value }: FeeChartProps) => {
const theme = useTheme()
const yLabels = generateYLabels({ data: Object.values(value).flat() })
const normalized = normalizeDataArrays({
...value,
yLabels,
})
return (
<ElementSizeAware
render={({ setElement, size }) => {
const contentWidth = size
? size.width - feeChartConfig.yLabelsWidth
: undefined
return (
<VStack flexGrow gap={20} ref={setElement}>
<ChartSlice yLabelsWidth={feeChartConfig.yLabelsWidth}>
<ChartYAxis
renderLabel={(index) => (
<ChartLabel key={index}>
{formatUnits(BigInt(yLabels[index]), gwei.decimals)}
</ChartLabel>
)}
data={normalized.yLabels}
/>
<VStack
style={{
position: "relative",
minHeight: feeChartConfig.chartHeight,
}}
fullWidth
>
{contentWidth &&
feePriorities.map((key) => {
const data = normalized[key]
return (
<TakeWholeSpaceAbsolutely key={key}>
<LineChart
key={key}
dataPointsConnectionKind="sharp"
fillKind={"gradient"}
data={data}
width={contentWidth}
height={feeChartConfig.chartHeight}
color={getFeePriorityColor(theme, key)}
/>
</TakeWholeSpaceAbsolutely>
)
})}
<ChartHorizontalGridLines data={normalized.yLabels} />
</VStack>
</ChartSlice>
</VStack>
)
}}
/>
)
}
To better understand fee trends, we can visualize how priority fees have fluctuated across the last 10 blocks using an interactive chart.
Now that we understand how to calculate and visualize the priority fee, let's examine another crucial parameter in EVM transaction fees: the maxFeePerGas
. This parameter represents the absolute maximum amount per unit of gas that a user is willing to pay for their transaction. It's calculated as the sum of the maxPriorityFeePerGas
(the tip to validators) and the baseFee
(the network's base cost). The MaxFeePerGas
component below displays this relationship in a clear formula and provides additional context about how this parameter helps manage transaction costs:
import { ShyInfoBlock } from "@lib/ui/info/ShyInfoBlock"
import { Text } from "@lib/ui/text"
import { FeeSection } from "../FeeSection"
import { toPercents } from "@lib/utils/toPercents"
import { baseFeeMultiplier } from "../baseFee/config"
export const MaxFeePerGas = () => (
<FeeSection
title={
<>
maxFeePerGas
<Text as="span" color="contrast">
{" "}
= maxPriorityFeePerGas + baseFee × {baseFeeMultiplier}
</Text>
</>
}
>
<ShyInfoBlock>
<Text color="supporting">
We multiply the base fee by {baseFeeMultiplier} to account for the
maximum allowed {toPercents(baseFeeMultiplier - 1)} increase in the base
fee between blocks. This buffer helps ensure that your transaction
remains competitive even if the network congestion causes the base fee
to rise unexpectedly.
</Text>
</ShyInfoBlock>
</FeeSection>
)
In EVM-based networks, the baseFee
is dynamically adjusted by the protocol based on network demand. When blocks are more than 50% full, the base fee increases by up to 12.5% per block, and when they're less than 50% full, it decreases by the same maximum percentage. This mechanism helps regulate network congestion by making transactions more expensive during high-demand periods and cheaper during low-demand periods. To account for this potential increase between blocks, web3 applications typically multiply the current baseFee
by a safety factor. In our implementation, we use a multiplier of 1.125 (12.5%), which matches the maximum possible increase in base fee between blocks. This approach provides a buffer against base fee fluctuations, reducing the likelihood of transaction failures due to insufficient gas fees while the transaction is pending in the mempool.
export const baseFeeMultiplier = 1.125
To monitor the current base fee, we've implemented the useBaseFeeQuery
hook that leverages Wagmi's useBlock
hook with the watch
option enabled. This setup allows us to continuously track the base fee as new blocks are produced. The hook extracts the baseFeePerGas
value from the latest block data, ensuring we always have access to the most up-to-date network fee information. Since the base fee is an optional parameter in some EVM chains, we use the shouldBePresent
utility to handle type safety, ensuring our application only works with chains that implement EIP-1559 fee mechanics.
import { useBlock } from "wagmi"
import { useTransformQueryData } from "@lib/ui/query/hooks/useTransformQueryData"
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { Block } from "viem"
const transform = (data: Block): bigint => {
return shouldBePresent(data.baseFeePerGas)
}
export const useBaseFeeQuery = () => {
const query = useBlock({
watch: true,
})
return useTransformQueryData(query, transform)
}
The BaseFee
component provides users with real-time visibility into the network's current base fee and explains its dynamic nature. Similar to our other fee components, it uses the FeeSection
layout and displays the current base fee value in Gwei using the MatchQuery
component for handling loading states. The component includes an informative description that helps users understand how the base fee automatically adjusts based on network demand, making it clear why transaction costs may vary over time.
import { FeeSection } from "../FeeSection"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { Text } from "@lib/ui/text"
import { ShyInfoBlock } from "@lib/ui/info/ShyInfoBlock"
import { gwei } from "@lib/chain/evm/utils/gwei"
import { formatAmount } from "@lib/utils/formatAmount"
import { fromChainAmount } from "@lib/chain/utils/fromChainAmount"
import { useBaseFeeQuery } from "../queries/useBaseFeeQuery"
import { Spinner } from "@lib/ui/loaders/Spinner"
export const BaseFee = () => {
const query = useBaseFeeQuery()
return (
<FeeSection
title={
<>
baseFee
<MatchQuery
value={query}
pending={() => <Spinner />}
success={(value) => (
<Text as="span" color="contrast">
{" = "}
{formatAmount(fromChainAmount(value, gwei.decimals))}{" "}
{gwei.name}
</Text>
)}
/>
</>
}
>
<ShyInfoBlock>
<Text color="supporting">
The base fee is automatically determined by the network based on the
demand for block space. When network activity increases, the base fee
goes up to discourage congestion. When activity decreases, the base
fee goes down to encourage network usage. This mechanism helps keep
transaction fees at a reasonable level while ensuring network
stability.
</Text>
</ShyInfoBlock>
</FeeSection>
)
}
Finally, let's bring all our fee calculations together in the MaxFee
component to provide users with a practical example. This component calculates the total estimated cost for transferring 1 ETH on the Ethereum mainnet. It combines three key pieces of data: the current base fee (adjusted with our safety multiplier), the priority fee (using the medium priority level), and the estimated gas required for the transfer. Using Wagmi's useEstimateGas
hook, we simulate the transaction to get an accurate gas estimate, while useAssetPriceQuery
fetches the current ETH price to display the fee in USD. The component uses useTransformQueriesData
to combine these data streams and calculate the total maximum fee, presenting it in both ETH and USD for better user understanding.
import { Text } from "@lib/ui/text"
import { formatAmount } from "@lib/utils/formatAmount"
import { useEstimateGas } from "wagmi"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { useAssetPriceQuery } from "@lib/chain-ui/queries/useAssetPriceQuery"
import { toChainAmount } from "@lib/chain/utils/toChainAmount"
import { mainnet } from "viem/chains"
import { useBaseFeeQuery } from "../queries/useBaseFeeQuery"
import { useTransformQueriesData } from "@lib/ui/query/hooks/useTransformQueriesData"
import { usePriorityFeeQuery } from "../queries/usePriorityFeeQuery"
import { baseFeeMultiplier } from "../baseFee/config"
import { fromChainAmount } from "@lib/chain/utils/fromChainAmount"
import { Spinner } from "@lib/ui/loaders/Spinner"
const ethAmount = 1
export const MaxFee = () => {
const baseFeeQuery = useBaseFeeQuery()
const priorityFeeQuery = usePriorityFeeQuery()
const gasQuery = useEstimateGas({
account: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
value: toChainAmount(ethAmount, mainnet.nativeCurrency.decimals),
})
const priceQuery = useAssetPriceQuery({ id: "ethereum" })
const maxFeeQuery = useTransformQueriesData(
{
baseFee: baseFeeQuery,
priorityFee: priorityFeeQuery,
gas: gasQuery,
},
({ baseFee, priorityFee, gas }) =>
fromChainAmount(
(BigInt(Math.round(Number(baseFee) * baseFeeMultiplier)) +
priorityFee) *
gas,
mainnet.nativeCurrency.decimals,
),
)
return (
<Text
color="contrast"
height="l"
as="h1"
size={28}
style={{ textTransform: "uppercase" }}
>
Estimated max fee to send {ethAmount} ETH:{" "}
<MatchQuery
value={maxFeeQuery}
pending={() => <Spinner />}
success={(maxFee) => {
return (
<>
<Text as="span" color="primary">
{formatAmount(maxFee)} ETH{" "}
<MatchQuery
value={priceQuery}
success={(price) => {
return <>≈ ${formatAmount(price * maxFee)}</>
}}
/>
</Text>
</>
)
}}
/>
</Text>
)
}
To provide users with familiar cost references, we fetch the current Ethereum price from CoinGecko using the useAssetPriceQuery
hook.
import { useQuery } from "@tanstack/react-query"
import {
getAssetPrice,
GetAssetPriceInput,
} from "../../chain/price/utils/getAssetPrice"
export const useAssetPriceQuery = (input: GetAssetPriceInput) => {
return useQuery({
queryKey: ["asset-price", input],
queryFn: () => getAssetPrice(input),
})
}
By combining Viem and Wagmi's powerful hooks with real-time data from the Ethereum network, we've created a comprehensive gas fee calculator that helps users make informed decisions about their transaction costs. The application not only provides current fee estimates but also visualizes historical trends and offers different priority levels to suit various transaction needs. This implementation demonstrates how modern web3 libraries can be leveraged to create intuitive interfaces for complex blockchain interactions.