Utilities

ERC-20 helpers

Read ERC-20 balances, metadata, and transfers through any EIP-1193 provider.

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.

app/read-usdt.ts
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:

output
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.

app/known-token.ts
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:

output
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.

app/amount.ts
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:

output
1234500000n
1234.5

Inputs with more fraction digits than the token supports return null.

app/invalid-amount.ts
import { parseTokenAmountToAtomic } from '@onmax/unerc20'

console.log(parseTokenAmountToAtomic('1.2345678', 6))
console.log(parseTokenAmountToAtomic('1.23 USDT', 6))

Expected behavior:

output
null
null

Submit a transfer

Use transferErc20() after your app has selected the token, account, recipient, amount, and chain.

app/send-usdt.ts
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:

output
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.

test/erc20-abi.test.ts
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',
  ])
})
Copyright © 2026