EVM Reads
Read contract state, token balances, NFT metadata, chain state, and fees on EVM networks. No popup, no passkey.
Every read below is a plain JSON-RPC call routed through our backend. No popup, no WebAuthn, no user interaction. The active network is picked up from the session.
import { evm } from '@oviato/connect';
const block = await evm.getBlockNumber();
const { formatted, symbol } = await evm.getTokenBalance({ tokenAddress: USDC });
const [name, sym, dec] = await evm.multicall({ contracts: [...] });All overflow-safe values come back as bigint. Use formatUnits /
parseUnits for display and user input. See
Overview → Bigint everywhere.
Contract reads
Read a single value
import { evm } from '@oviato/connect';
const ERC20_ABI = [
{
type: 'function',
name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ type: 'uint256' }],
stateMutability: 'view',
},
] as const;
const raw = await evm.readContract<bigint>({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: ERC20_ABI,
functionName: 'balanceOf',
args: ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6'],
});The generic <T> declares your expected return type. The SDK can't infer from the ABI without dependencies, so cast manually.
Batch reads with multicall
Any time you'd make more than one contract read per render, reach for multicall instead. One request, one on-chain call, N decoded results.
const [name, symbol, decimals, totalSupply] = await evm.multicall({
contracts: [
{ address: USDC, abi: ERC20_ABI, functionName: 'name' },
{ address: USDC, abi: ERC20_ABI, functionName: 'symbol' },
{ address: USDC, abi: ERC20_ABI, functionName: 'decimals' },
{ address: USDC, abi: ERC20_ABI, functionName: 'totalSupply' },
],
});
// Each entry: { status: 'success', result: unknown } | { status: 'failure', error: string }allowFailure defaults to true — a single revert in the batch doesn't
poison the rest. Set allowFailure: false if you want the whole call to
throw on any revert.
Multicall uses the Multicall3 contract at 0xcA11bde05977b3631167028862bE2a173976CA11 — the same deterministic address on every EVM chain.
Token helpers
Thin conveniences that bundle the common reads for ERC-20 interactions.
Token balance
const {
raw, // bigint — smallest unit
formatted, // '1234.56'
symbol, // 'USDC'
decimals, // 6
tokenAddress,
} = await evm.getTokenBalance({
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
address: session.address, // optional — defaults to session
});formatted is for display only. Never parse it back for arithmetic — the
decimal precision is tuned for human readability, not computation. Use
raw (bigint) and formatUnits/parseUnits for math.
Allowance
How much a spender is allowed to pull from an owner's balance. Useful before calling approve.
const { raw, formatted } = await evm.getTokenAllowance({
tokenAddress: USDC,
owner: session.address,
spender: UNISWAP_ROUTER,
});
if (raw < BigInt(needed)) {
// Approve first — see /evm/recipes
}Curated token list
Oviato maintains a registry of well-known ERC-20 tokens per chain (USDC, USDT, WETH, wrapped BTC variants). Use it to populate dropdowns or wallet UIs without hardcoding addresses.
const { network, tokens } = await evm.getTokens();
// tokens: [{ address, symbol, name, decimals, logoUrl? }, ...]New tokens are added server-side — no SDK release needed to pick them up.
NFT helpers
Owner, balance, metadata
// Who owns tokenId 1?
const { owner } = await evm.getNftOwner({ contractAddress, tokenId: 1n });
// How many NFTs in this collection does the user hold?
const { raw, formatted } = await evm.getNftBalance({
contractAddress,
address: session.address,
});
// Resolved metadata JSON — image normalized to a gateway URL
const meta = await evm.getNftMetadata({ contractAddress, tokenId: 1n });The metadata call resolves the on-chain tokenURI transparently:
| Scheme | Behaviour |
|---|---|
data:application/json;base64,… | Decoded inline |
ipfs://<cid> | Raced across three public IPFS gateways |
ar://<txid> | Resolved to arweave.net |
https://… | Direct fetch with 5 s timeout |
http://… | Rejected (mixed-content security) |
// Returned shape
{
tokenURI: 'ipfs://...', // original on-chain URI
name: 'Collection Name', // from `name()`
symbol: 'COLL', // from `symbol()`
metadata: { // resolved JSON (or absent on failure)
name: 'Token #42',
description: '...',
image: 'https://ipfs.io/ipfs/...', // normalized to gateway URL
attributes: [...],
},
resolverError: undefined, // populated when resolution failed
}Resolution failures are non-fatal — metadata is absent and
resolverError carries a one-line reason. The tokenURI + collection name
still come back so you have something to show.
Chain reads
Block
const n = await evm.getBlockNumber(); // bigint
const latest = await evm.getBlock({}); // latest block
const past = await evm.getBlock({ blockNumber: 12345678n });
const byHash = await evm.getBlock({ blockHash: '0x...' });
const full = await evm.getBlock({ includeTransactions: true });Transaction + receipt
const tx = await evm.getTransaction({ hash: '0x...' }); // Transaction | null
const rx = await evm.getTransactionReceipt({ hash: '0x...' }); // TransactionReceipt | nullBoth return null when the tx doesn't exist on-chain yet. A transaction you just broadcast may not have a receipt until the next block — see waitForTransactionReceipt for polling.
Nonce
const nonce = await evm.getTransactionCount({
address: session.address,
blockTag: 'pending', // 'pending' reflects our nonce tracker
});The pending count is tracker-adjusted so rapid-fire sends don't collide.
Pass blockTag: 'latest' for raw on-chain state.
Contract code + storage
const code = await evm.getCode({ address });
const isContract = code !== '0x';
const slot0 = await evm.getStorageAt({ address, slot: '0x0' });Fees
Single values
const gp = await evm.getGasPrice(); // bigint wei
const pri = await evm.estimateMaxPriorityFeePerGas(); // bigint wei
const units = await evm.estimateGas({ to, data, value }); // bigint gas unitsCombined snapshot
For gas-picker UIs, grab everything in one call:
const {
gasPrice, // bigint — legacy
maxFeePerGas, // bigint — EIP-1559
maxPriorityFeePerGas,
baseFeePerGas, // null on legacy chains
} = await evm.estimateFeesPerGas();
// Ethers-style alias
const fd = await evm.getFeeData();
// same + maxFeePerBlobGas stubbed to nullHistorical
const h = await evm.getFeeHistory({
blockCount: 20,
newestBlock: 'latest',
rewardPercentiles: [10, 50, 90],
});Signature verification (offchain)
Pure ecrecover — no chain call, no popup. Useful for backend verification of signatures your frontend produced.
EIP-191 (plain messages)
const valid = await evm.verifyMessage({
address: session.address,
message: 'Hello, Oviato!',
signature: '0x...',
});EIP-712 (typed data)
const valid = await evm.verifyTypedData({
address: session.address,
domain: { name: 'MyApp', version: '1', chainId: 8453 },
types: { Greeting: [{ name: 'content', type: 'string' }] },
primaryType: 'Greeting',
message: { content: 'Hi' },
signature: '0x...',
});types must NOT include an EIP712Domain entry — the domain separator is
computed from the domain object directly. Adding EIP712Domain to
types produces a different hash and the signature won't match.
Waiting for receipts
evm.waitForTransactionReceipt
Polls eth_getTransactionReceipt until the tx is mined + N confirmations deep. Returns null on timeout (default 180 s) — callers distinguish "not yet" from "RPC error" without try/catch.
const rx = await evm.waitForTransactionReceipt({ hash });
if (rx?.status === 'success') {
console.log('Mined in block', rx.blockNumber);
} else if (rx === null) {
console.log('Timed out — try again later');
} else {
console.log('Reverted:', rx);
}Defaults adapt to the chain:
| Chain | Confirmations | Polling interval |
|---|---|---|
| Base, Base Sepolia, other L2s | 1 | 2 s |
| Ethereum mainnet | 3 | 4 s |
| Ethereum Sepolia | 1 | 2 s |
Override via { confirmations, pollingInterval, timeout }.
Cancel via AbortSignal
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000);
try {
const rx = await evm.waitForTransactionReceipt({ hash, signal: ctrl.signal });
} catch (err: any) {
if (err.name === 'AbortError') { /* handled */ }
}Factory (client object style)
If you prefer a single client object to pass around (viem-style), createEvmClient() bundles all the methods:
import { createEvmClient } from '@oviato/connect';
const client = createEvmClient();
const block = await client.getBlockNumber();
const bal = await client.getTokenBalance({ tokenAddress });The evm namespace and createEvmClient() share identical internals — pick whichever reads nicer in your codebase.
Common questions
Related
- Writes — send transactions, call contracts, sign typed data
- Recipes — common flows (approve + transfer, batch balance, NFT mint)
- Troubleshooting — error-keyed lookup