EVM Overview
The evm namespace — reads, writes, popup flow, nonce tracker, envelope pattern. Everything you need to know before writing EVM code with Oviato.
Every EVM-specific method in Oviato lives under the evm namespace — contract calls, block reads, fee estimates, transaction sending, typed-data signing, receipt polling. Chain-agnostic actions (sign message, send native transfer) stay at the top level.
import { evm } from '@oviato/connect';
// Reads (no popup)
const block = await evm.getBlockNumber();
const bal = await evm.getTokenBalance({ tokenAddress });
// Writes (popup, user approves with passkey)
const r = await evm.sendTransaction({ to, value });
const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash });New to the SDK? Start with the Quickstart to get a working app, then come back here for the EVM surface.
Why a namespace?
An early version of the SDK exported readContract at the top level. A Bitcoin-session dev would import it, call it, and get a runtime error — "readContract is EVM-only".
The namespace fixes that: evm.readContract is unambiguous at the type level. You can't call it on a Bitcoin session without knowing you're doing something wrong. The utxo.* namespace mirrors this for Bitcoin-specific methods.
Chain-agnostic actions (signMessage, sendTransfer, switchNetwork, getBalance) stay at the top level — they're the multi-chain DX.
What's in the namespace
Reads (full reference) — no popup, no user interaction.
- Contract reads:
readContract,multicall - Token:
getTokenBalance,getTokenAllowance,getTokens - NFT:
getNftOwner,getNftBalance,getNftMetadata - Chain:
getBlockNumber,getBlock,getTransaction,getTransactionReceipt,getTransactionCount,getCode,getStorageAt - Fees:
getGasPrice,estimateGas,estimateFeesPerGas,estimateMaxPriorityFeePerGas,getFeeHistory,getFeeData - Verify:
verifyMessage,verifyTypedData - Wait:
waitForTransactionReceipt,waitForTransaction
Writes (full reference) — opens popup/iframe, requires passkey.
sendTransaction— generic EVM transactionwriteContract— ABI-driven contract call (specialized confirmation sheets)signTypedData— EIP-712 structured data signingswitchChain— ethers-style alias forswitchNetwork
The write flow
Here's what happens when you call evm.sendTransaction or evm.writeContract.
Prepare. SDK calls connect_prepareEvm on our API. Server reserves a nonce from the tracker, estimates gas, computes three fee tiers (fast/medium/slow), and — for ABI calls — decodes the function into a category (erc20_transfer, erc721_transfer, erc20_approve, contract_call).
Confirm. SDK opens a popup or iframe to connect.oviato.com with the prepared transaction. The user sees a specialized sheet based on the decoded category — "Transfer Token", "Transfer NFT", "Approve", or the generic call UI with decoded args.
Sign. User taps "Approve", the bridge prompts for the passkey (WebAuthn), and signs the transaction server-side. The raw signed hex never reaches your dapp.
Broadcast. Bridge calls connect_broadcastEvm, which submits via Chainstack and commits the nonce tracker.
Resolve. SDK returns { status: 'success', type: '...', data: { hash, network, explorerUrl } }.
You don't handle any of this. The SDK orchestrates the full flow — you await one function and get back a tx hash.
The response envelope
Every write resolves to a discriminated envelope, never a raw value.
type SendTransactionResult =
| { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } }
| { status: 'error'; message: string };Narrow on status before reading .data:
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log(r.data.hash); // ← safely typed
} else {
console.error(r.message);
}This differs from ethers/viem which return a raw hash and throw on error. The envelope makes cancellation ("user closed the popup") non-exceptional — you get a normal error result, not a thrown exception. See Migrating from viem for a thin adapter if you prefer the raw-hash style.
The nonce tracker
Rapid-fire EVM sends are a pain on raw JSON-RPC — eth_getTransactionCount('pending') often lags what's actually in the mempool, so firing three transactions in 100 ms can hand out the same nonce three times.
Oviato's API maintains a server-side nonce counter per (address, chain). You call evm.sendTransaction three times in a row; each call reserves a unique sequential nonce atomically in D1.
// All three get unique, sequential nonces automatically.
await Promise.all([
evm.sendTransaction({ to, value: 1n }),
evm.sendTransaction({ to, value: 2n }),
evm.sendTransaction({ to, value: 3n }),
]);When a broadcast fails pre-mempool (replacement underpriced, nonce too low, already known, underpriced), the tracker automatically releases
the nonce. Your next send picks up cleanly with the right number. No
manual recovery required.
If you need to override (advanced — e.g. migrating a legacy system with its own nonce state), pass nonce: explicitly:
await evm.sendTransaction({ to, value, nonce: 42 });Bigint everywhere (for overflow-safe math)
EVM units can exceed Number.MAX_SAFE_INTEGER (2⁵³ − 1). We use bigint for anything that can overflow: balances, gas, fees, values.
// bigint literals — note the `n` suffix
const value = 10n ** 18n; // 1 ETH in wei
const amount = 1_000_000n; // 1 USDC (6 decimals)
// For display — never parse `formatted` back for math
const { raw, formatted } = await evm.getTokenBalance({ tokenAddress });
console.log(formatted); // '1234.56'
// For parsing user input into bigint
import { parseUnits, formatUnits } from '@oviato/connect';
const wei = parseUnits('0.5', 18); // 500000000000000000n
const display = formatUnits(1234500n, 6); // '1.2345'Supported EVM networks
ethmainnet(Ethereum)ethsepolia(Ethereum Sepolia testnet)base(Base mainnet)basesepolia(Base Sepolia testnet)
More chains come online via the dashboard. If your app's supported networks don't include an EVM chain, SDK calls will reject early — no silent misroutes.
Session scope
A session is scoped to one network. When a user is on Bitcoin, evm.* methods throw — EVM is not the active network. Use switchNetwork to re-scope the session.
import { switchNetwork, Network } from '@oviato/connect';
await switchNetwork(Network.ETHMAINNET);
// Session now Ethereum-scoped. evm.* works.Switching networks requires the user to tap their passkey once — the server re-issues the JWT with the new chain's claims. This is a security measure (a stolen JWT can't be silently repointed).