ERC-20 helpers
Use @onmax/unerc20 when you already have an EIP-1193 provider and need portable ERC-20 calls without taking on wallet discovery, chain switching, or token-list policy.
Do not use it for native ETH/MATIC transfers, NFT transfers, allowance management, token registries, fiat pricing, or payout approvals. Those decisions belong in your app.
Read a balance
Use the read helpers when the provider already points at the chain that hosts the token.
import {
formatTokenAmountAtomic,
readErc20Balance,
readErc20Decimals,
readErc20Symbol,
} from '@onmax/unerc20'
const provider = window.ethereum
const token = '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'
const account = '0x1234567890abcdef1234567890abcdef12345678'
const [symbol, decimals, atomicBalance] = await Promise.all([
readErc20Symbol(provider, token),
readErc20Decimals(provider, token),
readErc20Balance(provider, { token, account }),
])
console.log(`${symbol}: ${formatTokenAmountAtomic(atomicBalance, decimals)}`)
Expected behavior:
USDT: 1.2345
The package calls eth_call for symbol(), decimals(), and balanceOf(address).
Skip metadata calls when you already know it
Use a token object when the app has trusted metadata. readErc20Symbol() and readErc20Decimals() return the provided metadata without calling the provider.
import { readErc20Decimals, readErc20Symbol } from '@onmax/unerc20'
const usdt = {
address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
symbol: 'USDT',
decimals: 6,
}
console.log(await readErc20Symbol(window.ethereum, usdt))
console.log(await readErc20Decimals(window.ethereum, usdt))
Expected behavior:
USDT
6
Use this for app-owned token allowlists. Do not trust user-provided token metadata for transfer screens.
Parse user input once
Use parseTokenAmountToAtomic() at the form boundary, then pass the returned bigint through the rest of the flow.
import { formatTokenAmountAtomic, parseTokenAmountToAtomic } from '@onmax/unerc20'
const decimals = 6
const amount = parseTokenAmountToAtomic('1,234.50', decimals)
if (amount === null)
throw new Error('Enter a valid token amount')
console.log(amount)
console.log(formatTokenAmountAtomic(amount, decimals))
Expected behavior:
1234500000n
1234.5
Inputs with more fraction digits than the token supports return null.
import { parseTokenAmountToAtomic } from '@onmax/unerc20'
console.log(parseTokenAmountToAtomic('1.2345678', 6))
console.log(parseTokenAmountToAtomic('1.23 USDT', 6))
Expected behavior:
null
null
Submit a transfer
Use transferErc20() after your app has selected the token, account, recipient, amount, and chain.
import { parseTokenAmountToAtomic, transferErc20 } from '@onmax/unerc20'
const provider = window.ethereum
const amount = parseTokenAmountToAtomic('10', 6)
if (amount === null)
throw new Error('Invalid amount')
const hash = await transferErc20(provider, {
token: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
from: '0x1234567890abcdef1234567890abcdef12345678',
to: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
amount,
})
console.log(hash)
Expected behavior:
0x...
The helper sends eth_sendTransaction with ERC-20 transfer(address,uint256) calldata, value: '0x0', and optional gas.
Reuse the ABI in simulations
Use ERC20_ABI when a test, simulator, or decoder needs the same standard ERC-20 shape as the helpers.
import { expect, it } from 'vitest'
import { ERC20_ABI } from '@onmax/unerc20'
it('keeps the helper ABI small', () => {
expect(ERC20_ABI.map(entry => entry.name).sort()).toEqual([
'balanceOf',
'decimals',
'symbol',
'transfer',
])
})