# Oviato — Complete Documentation for LLMs Oviato is a multi-chain wallet SDK with passkey authentication. This file concatenates every docs page in sidebar order. If you only need a pointer map, fetch `https://docs.oviato.com/llms.txt` instead — it's the llmstxt.org-spec index. ## Mental model (read this first) Oviato exposes three API surfaces: 1. **Chain-agnostic (top level)** — `signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`, plus auth (`login`, `register`, `authenticate`). These dispatch by session network and work on Bitcoin + EVM with identical signatures. 2. **`evm.*` namespace** — EVM-specific: `readContract`, `multicall`, `sendTransaction`, `writeContract`, `signTypedData`, `waitForTransactionReceipt`, token + NFT helpers, fee estimates. Throws on non-EVM sessions. 3. **`utxo.*` namespace** — Bitcoin-specific: `signPsbt`. Throws on non-UTXO sessions. Two packages, same API: - **`@oviato/sdk`** — framework-agnostic. Vue, Svelte, Angular, vanilla JS, Node, Deno, Bun. - **`@oviato/connect`** — React. Re-exports 100% of `@oviato/sdk` plus adds `OviConnectProvider`, `useOviConnect`, `ConnectButton`, `Connect` at `@oviato/connect/client`. Never install both. `@oviato/connect` already depends on `@oviato/sdk`. ## Response envelope pattern Writes return a discriminated envelope, never a raw value: ```ts const r = await evm.sendTransaction({ to, value }); if (r.status === 'success' && r.type === 'sendTransactionSuccess') { console.log(r.data.hash); } else if (r.status === 'error') { console.error(r.message); } ``` Narrow on `status` + `type` before accessing `data`. "User cancelled popup" arrives as `status: 'error'` — not a thrown exception. ## Nonce tracker (EVM writes) Rapid-fire EVM sends never collide — the server serializes nonces per `(address, chain)`. You never manage nonces. On pre-mempool errors (`replacement underpriced`, `nonce too low`, `already known`, `underpriced`) the tracker auto-releases and the next send picks the right nonce. ## Supported networks EVM: `eth`, `ethsepolia`, `base`, `basesepolia` UTXO: `bitcoin`, `bitcointestnet` --- # Oviato Documentation > Passkey-based authentication + multi-chain wallet SDK for Bitcoin and EVM chains. Canonical URL: https://docs.oviato.com File: index.mdx # Welcome to Oviato Oviato is a **passkey-based wallet SDK** for multi-chain applications. Users sign in with Face ID / Touch ID / security key — no passwords, no seed phrases. One API works across Bitcoin and EVM. Ship a working sign-in + transfer in 10 minutes. Mental model — passkeys, sessions, popup flows, nonce tracker. `@oviato/sdk` vs `@oviato/connect` — same API, pick by framework. Create your app, configure domains, themes, networks. ## Explore the SDK Works on Bitcoin + EVM with one API — `signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`. `evm.*` namespace — `readContract`, `multicall`, `sendTransaction`, `writeContract`, token + NFT helpers, fees, receipts. `utxo.*` namespace — PSBT signing for Bitcoin, including server-built transactions and counterparty signing. `OviConnectProvider`, `useOviConnect`, ``, ``. ## Why Oviato - **Passkey authentication** — Face ID, Touch ID, or FIDO2 security keys. No passwords, no seed phrases. - **Multi-chain with one API** — identical surface for Bitcoin + EVM. Switching chains is a one-liner. - **Server-side nonce tracker** — rapid-fire EVM sends don't collide. You don't manage nonces. - **Specialized confirmation sheets** — `writeContract` with an ERC-20 ABI shows a branded "Transfer Token" sheet, not raw hex. - **Tiny bundle** — ~11 KB gzipped core, ~14 KB with React bindings. - **TypeScript-native** — full type safety with `bigint` for EVM, discriminated unions for chain-agnostic returns. ## Three-step start 1. **Create your app** at [dashboard.oviato.com](https://dashboard.oviato.com) → copy your `appId`. 2. **Install** `@oviato/connect` (React) or `@oviato/sdk` (any framework). 3. **Drop in the provider + button**: ```tsx import { OviConnectProvider, ConnectButton } from '@oviato/connect/client'; import { Network } from '@oviato/connect'; ``` See the [Quickstart](/getting-started/quickstart) for the full walk-through including sends and contract calls. ## Community + Resources - [GitHub](https://github.com/oviato-hq) — report issues, follow releases - [Developer Dashboard](https://dashboard.oviato.com) — create apps, view activity logs, manage branding - [Changelog](/changelog/sdk) — version history --- # Section: Getting Started # Quick Start > Get started with Oviato in minutes Canonical URL: https://docs.oviato.com/getting-started/overview File: getting-started/overview.mdx Welcome to Oviato! This guide will walk you through integrating Oviato authentication and wallet functionality into your application. ## What is Oviato? Oviato provides WebAuthn-based passkey authentication for multi-chain blockchain applications. Users can create accounts and sign transactions using biometrics (Face ID, Touch ID) or security keys—no passwords or seed phrases required. ## Getting Started in 3 Steps ### 1. Create Your App First, create an application in the [Oviato Developer Dashboard](https://dashboard.oviato.com): 1. Sign up or log in at [dashboard.oviato.com](https://dashboard.oviato.com) 2. Create a new application 3. Configure your app settings (name, logo, domains) 4. Copy your **App ID** - you'll need this for the SDK See the [Developer Dashboard](/developer-dashboard/setting-up-app) section for detailed instructions. ### 2. Choose Your SDK Oviato offers two SDK options depending on your framework: #### For React/Next.js Apps: **@oviato/connect** Best for React applications. Includes hooks, providers, and pre-built UI components. ```bash npm install @oviato/connect ``` [→ @oviato/connect Documentation](/react-components/overview) #### For Other Frameworks: **@oviato/sdk** Framework-agnostic core SDK. Works with Vue, Svelte, Angular, or vanilla JavaScript. ```bash npm install @oviato/sdk ``` [→ @oviato/sdk Documentation](/packages/choosing) ### 3. Integrate Authentication #### Next.js Example ```tsx // app/layout.tsx import { OviConnectProvider } from "@oviato/connect/client"; export default function RootLayout({ children }) { return ( {children} ); } ``` ```tsx // app/page.tsx "use client"; import { useOviConnect, ConnectButton } from "@oviato/connect/client"; export default function Page() { const { session, disconnect } = useOviConnect(); if (!session) { return ; } return (

Connected: {session.address}

); } ``` #### Vanilla JavaScript Example ```javascript import { initialize, authenticate, getSession } from "@oviato/sdk"; // Initialize await initialize({ appId: "your-app-id", network: "bitcoinmainnet", }); // Authenticate const result = await authenticate(); if (result.status === "success") { const session = getSession(); console.log("Connected:", session.address); } ``` ## What's Next? - **[Developer Dashboard Guide](/developer-dashboard/setting-up-app)** - Set up your first app - **[@oviato/connect Docs](/react-components/overview)** - React SDK with hooks and components - **[@oviato/sdk Docs](/packages/choosing)** - Framework-agnostic core SDK ## Key Features - **Passkey Authentication** - Secure WebAuthn-based login with biometrics - **Multi-Chain Support** - Bitcoin, Ethereum, Base (Solana, XRP, and more coming soon) - **SSR Compatible** - Works with Next.js App Router and all SSR frameworks - **TypeScript First** - Full type safety and IntelliSense - **Pre-built UI** - Drop-in components for rapid development - **Tiny Bundle** - Optimized for performance ## Need Help? - Drop us an email to: [support[at]oviato.com](mailto:support@oviato.com) --- # How Oviato Works? > An overview of the Oviato architecture and data flow. Canonical URL: https://docs.oviato.com/getting-started/how-oviato-works File: getting-started/how-oviato-works.mdx Understand the high-level architecture and request flow within Oviato. --- # Quickstart > Ship a working sign-in + multi-chain wallet in 10 minutes with @oviato/connect. Canonical URL: https://docs.oviato.com/getting-started/quickstart File: getting-started/quickstart.mdx Build a working React app with passkey sign-in, native sends (BTC + ETH), and a token transfer — all in one file. This guide uses `@oviato/connect`. Not on React? See [Chain-Agnostic → Overview](/chain-agnostic/overview) for the `@oviato/sdk` version. ## Prerequisites - An `appId` from the [Developer Dashboard](/developer-dashboard/setting-up-app) (free to create) - Node 18+ and React 18+ - A device with a passkey — iPhone/iPad with Face ID, Mac with Touch ID, Android with fingerprint, or a FIDO2 security key ## 1. Install ```bash pnpm add @oviato/connect ``` ```bash npm install @oviato/connect ``` ```bash yarn add @oviato/connect ``` ## 2. Wrap your app in the provider ```tsx title="app/providers.tsx" 'use client'; import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx title="app/layout.tsx" import { Providers } from './providers'; export default function RootLayout({ children }) { return ( {children} ); } ``` ```tsx title="src/main.tsx" import React from 'react'; import ReactDOM from 'react-dom/client'; import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; import App from './App'; ReactDOM.createRoot(document.getElementById('root')!).render( , ); ``` Replace `your-app-id-here` with the `appId` from your dashboard. The provider's `appId` is non-optional — the SDK refuses to initialize without it. ## 3. Add a Connect button The fastest way — drop in our pre-built button, pick a network per button. ```tsx title="app/page.tsx" 'use client'; import { ConnectButton, useOviConnect } from '@oviato/connect/client'; import { Network } from '@oviato/connect'; export default function Page() { const { session, disconnect } = useOviConnect(); if (!session) { return (
); } return (

Connected as {session.username}

Address: {session.address}

Network: {session.network}

); } ``` Click one, approve the passkey, done. The `session` object is now populated with the user's address, network, public keys, and auth token. ## 4. Send your first transaction Every chain supports `sendTransfer` — pass the recipient + amount in the chain's smallest unit. ```tsx import { sendTransfer } from '@oviato/connect'; async function sendBitcoin() { const r = await sendTransfer({ recipient: { address: 'bc1q...', amount: 10000, // satoshis (0.0001 BTC) }, fee: 'auto', }); if (r.status === 'success' && r.type === 'sendTransferSuccess') { console.log('TxID:', r.data.txid); console.log('Explorer:', r.data.explorerUrl); } else if (r.status === 'error') { console.error(r.message); } } ``` ```tsx import { sendTransfer } from '@oviato/connect'; async function sendEth() { const r = await sendTransfer({ recipient: { address: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', amount: 10n ** 15n, // wei (0.001 ETH) — bigint for overflow safety }, fee: 'auto', }); if (r.status === 'success' && r.type === 'sendTransferSuccess') { console.log('Hash:', r.data.txid); console.log('Explorer:', r.data.explorerUrl); } } ``` A popup opens with the full transaction details. The user approves with their passkey, the bridge signs server-side, and the tx broadcasts. You get back a hash + explorer URL. ## 5. Transfer a token (EVM only) Use `evm.writeContract` to call any contract method. Here's USDC transfer: ```tsx import { evm } from '@oviato/connect'; const ERC20_ABI = [ { type: 'function', name: 'transfer', inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', }, ] as const; async function transferUSDC() { const r = await evm.writeContract({ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum abi: ERC20_ABI, functionName: 'transfer', args: ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', 1_000_000n], // 1 USDC (6 decimals) }); if (r.status === 'success' && r.type === 'sendTransactionSuccess') { console.log('Hash:', r.data.hash); } } ``` The bridge auto-detects this is an ERC-20 transfer (via the ABI + known-contracts registry) and renders a dedicated "Transfer Token" confirmation sheet — no raw hex shown to the user. ## 6. Wait for confirmation ```tsx import { evm } from '@oviato/connect'; const receipt = await evm.waitForTransactionReceipt({ hash }); if (receipt?.status === 'success') { console.log('Mined in block', receipt.blockNumber); } ``` Defaults adapt to the chain: 1 confirmation + 2s polling on L2s, 3 confirmations + 4s polling on Ethereum mainnet. ## That's it You now have: - Passkey auth - Multi-chain connect - Native + ERC-20 sends - Receipt polling Not bad for 20 lines of code. ## What to learn next - [Core Concepts](/getting-started/core-concepts) — the mental model behind the SDK - [EVM → Overview](/evm/overview) — the full EVM surface (`readContract`, `multicall`, NFTs, fees, EIP-712) - [UTXO → Overview](/utxo/overview) — PSBT signing, multi-recipient sends - [React Components → Provider](/react-components/provider) — deeper config (themes, DNS override, custom domains) - [Developer Dashboard](/developer-dashboard/setting-up-app) — configure your app (domains, branding, supported networks) --- # Core Concepts > The mental model behind Oviato — passkeys, sessions, popup flows, chain scoping, the nonce tracker. Canonical URL: https://docs.oviato.com/getting-started/core-concepts File: getting-started/core-concepts.mdx You can use Oviato without reading this page — the SDK handles everything. But when something surprises you later, come back here: this is where the moving parts are named. ## The data model Oviato has four things a developer interacts with. **App.** Your product. Created in the [Developer Dashboard](/developer-dashboard/setting-up-app) — gives you an `appId`, domains, theme, supported networks. The SDK reads your app's config at `initialize()` time. **User.** A human with a [passkey](https://fidoalliance.org/passkeys/). Identified by a username (which maps to an [OVI.ID](/getting-started/how-oviato-works)). A user can connect to many apps; their wallet addresses are deterministic per (user, app, network). **Session.** A signed JWT + the user's public state (address, network, hdkeys). Created on login and kept in-memory + localStorage by the SDK. Scoped to one network at a time — switching networks re-issues the JWT. **Bridge.** The popup/iframe UI at `connect.oviato.com` where the user approves sign requests. Your dapp never touches private keys; signatures happen inside the bridge via WebAuthn. ## The passkey flow WebAuthn passkeys are how users sign in and sign transactions — no passwords, no seed phrases. **First-time registration.** User types a username, the SDK calls `register()`. The browser prompts for a passkey (Face ID / Touch ID / security key). A cryptographic keypair is generated + stored in the user's device keychain. The public key is uploaded to Oviato; the private half never leaves the device. **Login on the same device.** `login()` opens a WebAuthn prompt. The device proves possession of the private key by signing a server-issued challenge. SDK receives a JWT + hd-keys, session starts. **Login on a new device.** Passkeys sync across a user's Apple/Google/Microsoft ecosystem automatically. A user who registered on iPhone can log in on their Mac without extra setup. **Signing a transaction.** The dapp calls `sendTransaction` / `writeContract` / etc. A popup opens showing the full transaction details. User approves with their passkey — WebAuthn signs on the device, the bridge completes the on-chain signature server-side, and the tx is broadcast. The dapp never holds a private key or sees the passkey. All signing flows through the bridge UI, gated by a biometric/security-key tap. ## Sessions are chain-scoped A session's JWT is issued for **one network** — `eth`, `base`, `bitcoin`, etc. When a user switches networks you must re-issue the JWT. ```ts import { switchNetwork, Network } from '@oviato/connect'; const r = await switchNetwork(Network.BASEMAINNET); if (r.status === 'success') { // SDK's session is now Base-scoped. Every subsequent call // (reads, writes, balance) targets Base until you switch again. } ``` Switching networks requires a WebAuthn assertion — the user will see one passkey prompt to re-sign the session. This is deliberate: it prevents a stolen JWT from being silently repointed to a different chain. Under the hood, `switchNetwork` calls `refreshSession` + re-opens the bridge for a one-tap signature. ## Popup vs iframe Write operations (sign a message, send a tx, switch a network) always show an approval UI. The SDK picks between two modes: - **Iframe** (default) — overlays a modal in your page. Better UX, works on mobile, same-domain-feel. Used on production HTTPS. - **Popup** — opens a new browser window. Only used when iframe can't work: - **Localhost / non-HTTPS** — iframes have security restrictions in insecure contexts, so the SDK auto-falls-back to popup. - **Safari / iOS with WebAuthn** (passkey create/login) — Safari has long-standing bugs around WebAuthn in iframes; forced to popup for those ops only. ```ts await evm.sendTransaction({ to, value, method: 'iframe' }); // force iframe await evm.sendTransaction({ to, value, method: 'popup' }); // force popup ``` In production, expect iframe everywhere. Popup is a localhost-dev concession and a Safari-WebAuthn concession — you typically don't need to think about it. ## The response envelope Every write method resolves to a discriminated envelope, never a raw value. ```ts type Result = | { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } } | { status: 'error'; message: string }; ``` Narrow on `status` + `type` before reading `.data`: ```ts const r = await evm.sendTransaction({ to, value }); if (r.status === 'success' && r.type === 'sendTransactionSuccess') { console.log('Hash:', r.data.hash); } else if (r.status === 'error') { console.error(r.message); } ``` This is different from ethers/viem, which return a raw hash and throw on error. See [Migrating from viem](/evm/migrating-from-viem) for a one-line adapter if you prefer that style. ## The nonce tracker EVM transactions need a monotonically-increasing nonce. A dapp that fires three `sendTransaction` calls in 100 ms would collide if they all read `eth_getTransactionCount('pending')` at the same time — the chain hasn't caught up yet. Oviato runs a server-side nonce tracker per (address, chain). Rapid-fire calls serialize correctly without you having to manage anything. ```ts // These three calls all get unique, sequential nonces automatically. const [r1, r2, r3] = await Promise.all([ evm.sendTransaction({ to, value: 10n ** 15n }), evm.sendTransaction({ to, value: 2n * 10n ** 15n }), evm.sendTransaction({ to, value: 3n * 10n ** 15n }), ]); ``` If a broadcast fails pre-mempool (`replacement underpriced`, `nonce too low`), the tracker automatically releases the nonce for reuse. You get a structured error with a `retryAfter` hint — no manual recovery needed. ## Protected vs read-only methods Some methods require a valid Bearer JWT; others are public reads. **Protected (JWT required):** - `connect_prepare` + `connect_preparePsbt` (UTXO writes) - `connect_prepareEvm` + `connect_broadcastEvm` (EVM writes) - `refreshSession` **Read-only (no JWT needed):** - All `evm.*` reads (contract, chain, fees, tokens, NFTs) - `rpc.getBalance()`, balance history, username resolution - `x402` payment flows Rate limits apply to protected methods per `(app, address)`. Reads have looser app-level limits. ## Server-side components you don't see The SDK looks simple because most of the complexity lives behind it: - **Bridge worker** (`connect.oviato.com`) — hosts the popup/iframe UI, holds the WebAuthn flow, signs transactions server-side. - **RPC API** (`rpc.oviato.com`) — JSON-RPC router. Handles auth, rate limits, nonce tracking, activity logging. Routes on-chain reads to Chainstack. - **Chainstack** — our RPC provider for EVM chains. Behind the API; you never talk to it directly. - **D1 + KV** — Cloudflare storage for users, app config, nonce state, token cache, activity logs. You don't need to understand these to ship. They matter when you hit an edge case (rate-limit error, wedged nonce, cache miss) and want to know what's going on. ## Next steps - [Quickstart](/getting-started/quickstart) — build a working sign-in + transfer in 10 minutes - [Chain-Agnostic → Overview](/chain-agnostic/overview) — methods that work across BTC + EVM - [EVM → Overview](/evm/overview) — the `evm.*` namespace, popup flow, envelope pattern --- # Section: Developer Dashboard # Signing Up / Logging In > Create an account and sign in to the Developer Dashboard. Canonical URL: https://docs.oviato.com/developer-dashboard/signing-up-logging-in File: developer-dashboard/signing-up-logging-in.mdx ## Overview Use this guide to create an account and access the Developer Dashboard. It covers sign up, email verification, logging in, and password reset. ## Sign up 1. Key in your work email Address 2. Press Continue 3. Check your inbox for one time password to activate your account. ![Sign up screen](/developer-dashboard/signup1.png) You would recieve the one time password in few minutes. - Check spam/junk - Key in the code in the Dashboard Signup ![Verify OTP](/developer-dashboard/signup2.png) ## Setting up an Org Key in your Org name, Org URL and Size. ![Setup Org](/developer-dashboard/signup-org.png) ## Log in 1. Key in the email you signed up with. 2. Check your inbox for one time password 3. Click “Continue” to the dashboard. ## Troubleshooting - Make sure your email address is typed correctly and has been verified. - If you run into repeated failures, try another browser or disable extensions temporarily. - Still stuck? Contact support@oviato.com. --- # Setting Up Your App > Create and configure your first application in the Oviato Developer Dashboard Canonical URL: https://docs.oviato.com/developer-dashboard/setting-up-app File: developer-dashboard/setting-up-app.mdx This guide will walk you through creating and configuring your first application in the Oviato Developer Dashboard. ## Prerequisites Before you begin, you'll need: - An Oviato account (sign up at [dashboard.oviato.com](https://dashboard.oviato.com)) - Your application's domain(s) where you'll integrate Oviato ## Step 1: Access the Dashboard 1. Go to [dashboard.oviato.com](https://dashboard.oviato.com) 2. Sign in with your Oviato account 3. You'll land on the dashboard home page ## Step 2: Create a New Application 1. Click **"Create New App"** or **"New Application"** button 2. Fill in the required information: - **App Name**: A friendly name for your application (e.g., "My DeFi App"). This is an internal name and won't be visible to your users. - **Select Network**: We support Bitcoin, Ethereum, and Base networks. ![Create an App](/developer-dashboard/create-app.png) ## Step 3: Configure App Settings After creating your app, you'll need to configure the following settings: ### Basic Information - **App Name**: Update anytime if needed - **App ID**: Auto Generated ID for your App. You will be using this ID in the SDK ```tsx // You'll use this App ID in your code ``` ### Allowed Domains Add the domains where your app will be hosted: 1. Click **"Add Domain"** 2. Enter your domain (e.g., `https://myapp.com` or `http://localhost:3000` for development) 3. Click **"Add"** **Important**: - Include both production and development domains - Localhost addresses are supported for testing - Wildcards are not supported for security reasons ### Branding (Optional) Customize how Oviato authentication appears to your users: - **Logo**: Upload your app logo (recommended: 200x200px PNG or SVG) - **Brand Color**: Choose your primary brand color - **Theme**: Select light or dark theme These settings affect the authentication modal and wallet UI shown to your users. ## Step 4: Copy Your App ID 1. In your app settings, locate the **App ID** field 2. Click the copy icon or select and copy the ID 3. Save this somewhere secure - you'll need it for SDK integration ## Step 5: Environment Selection Oviato supports multiple environments: - **Production**: For live applications - **Staging**: For testing before deployment You can switch between environments in the dashboard sidebar. ## Step 6: SDK Integration Now that your app is configured, integrate the Oviato SDK: ### For React/Next.js: ```bash npm install @oviato/connect ``` ```tsx import { OviConnectProvider } from "@oviato/connect/client"; {children} ; ``` [→ Full @oviato/connect Documentation](/react-components/overview) ### For Other Frameworks: ```bash npm install @oviato/sdk ``` ```javascript import { initialize } from "@oviato/sdk"; await initialize({ appId: "your-app-id", network: "bitcoinmainnet", }); ``` [→ Full @oviato/sdk Documentation](/packages/choosing) ## Managing Your App ### View App Analytics The dashboard provides insights into: - Total users - Authentication events - Transaction volume - Network usage ### Update App Settings You can update your app settings anytime: 1. Select your app from the sidebar 2. Navigate to the settings you want to change 3. Make your updates 4. Changes take effect immediately ### Multiple Apps You can create and manage multiple applications: - Each app has its own App ID - Apps can have different configurations - Switch between apps using the sidebar dropdown ## Security Best Practices - **Never expose your App ID in public repositories** (if it includes sensitive configuration) - **Use environment variables** to store your App ID ```bash # .env.local NEXT_PUBLIC_OVIATO_APP_ID=your-app-id ``` - **Regularly review** allowed domains to ensure only authorized domains can use your app - **Monitor** your app's usage in the analytics dashboard ## Troubleshooting ### "Domain not allowed" error - Verify the domain is added to your allowed domains list - Check for typos in the domain URL - Ensure you're using the correct protocol (http/https) ### App ID not working - Confirm you copied the entire App ID - Check that you're using the correct environment (staging vs production) - Verify the SDK is properly initialized ## Next Steps - **[Integrate @oviato/connect](/getting-started/quickstart)** - Install and setup the React SDK - **[Integrate @oviato/sdk](/getting-started/quickstart)** - Install the framework-agnostic SDK --- # Branding and Colors > Customize brand assets, theme, and colors. Canonical URL: https://docs.oviato.com/developer-dashboard/branding-and-colors File: developer-dashboard/branding-and-colors.mdx You can customize the Branding, logo and colors of the secure iframe. When users are prompted to login, sign a message, approve transfer, Your branding and colors will be visible. ![Logo and Branding](/developer-dashboard/setup-colors.png) --- # Wallets using your dApp > View the Wallets/Users using your dApp Canonical URL: https://docs.oviato.com/developer-dashboard/wallets File: developer-dashboard/wallets.mdx This feature lets you see the users and their wallet address using your dApp. ## Prerequisites For the users information to show up here your app must have users. ![Wallet Addresses](/developer-dashboard/walletaddr.png) --- # Section: AI Integration # AI Integration Overview > Use AI tools to accelerate your Oviato development workflow Canonical URL: https://docs.oviato.com/ai-integration/overview File: ai-integration/overview.mdx Modern AI coding assistants can dramatically speed up your Oviato integration. This section covers how to configure the most popular tools for optimal Oviato development. ## Quick Comparison | Tool | Best For | Setup Complexity | |------|----------|------------------| | Claude Code | Deep reasoning, complex integrations, security reviews | Low | | Cursor | Fast iteration, multi-file editing | Low | | Antigravity | Agentic workflows, multi-file refactors | Low | | ChatGPT | Quick questions, code review, learning | Low | | Codex | Automated tasks, background code generation | Low | ## The LLM Documentation File All these tools work best when given context about Oviato. We provide an optimized documentation file: ``` https://docs.oviato.com/llms-full.txt ``` This file contains: - Complete SDK documentation (@oviato/connect and @oviato/sdk) - Authentication and wallet operation guides - Code examples for all frameworks - Changelog and version history It's automatically regenerated on each deployment to stay current. Learn more about setting it up in the [LLM Documentation File](/ai-integration/using-llms) guide. ## Choosing the Right Tool ### Use Claude Code when: - Building complex authentication flows - Reviewing security of wallet integrations - Debugging tricky PSBT signing issues - You need deep reasoning about your code - You want to use [Oviato Skills](/ai-integration/skills) for guided development ### Use Cursor when: - Rapidly iterating on UI components - Making changes across multiple files - You want inline code suggestions - Working in a VS Code-style environment ### Use Antigravity when: - You prefer agentic AI that takes action autonomously - Working on large refactors across many files - You want a Cursor-like experience with a different workflow ### Use ChatGPT when: - Learning Oviato concepts - Getting quick answers about the SDK - Reviewing code snippets - Creating Custom GPTs for your team ### Use Codex when: - You want AI to run tasks in the background - Automating repetitive integration work - Running code generation from GitHub issues --- # LLM Documentation Files > Point Claude Code, Cursor, Codex, and other AI coding tools at Oviato's LLM-optimized docs. Canonical URL: https://docs.oviato.com/ai-integration/using-llms File: ai-integration/using-llms.mdx Oviato ships two LLM-facing documentation artifacts. They're regenerated on every docs deploy, so anything you point your AI tooling at stays in sync with the codebase. ## The files ### `llms.txt` — index (llmstxt.org spec) ``` https://docs.oviato.com/llms.txt ``` Follows the [llmstxt.org](https://llmstxt.org) standard — a concise markdown file with one line per docs page (title + description + URL), organized by sidebar section. Agents that support the spec (Cursor, Claude Code, newer Copilot modes) fetch this first to see what's available, then pull individual pages on demand. This saves context window: instead of ingesting ~200 KB of full documentation on every query, the agent scans the 6 KB index and fetches only the pages that match the task. ### `llms-full.txt` — full concatenation ``` https://docs.oviato.com/llms-full.txt ``` Every docs page concatenated into one file, with a preamble that front-loads the mental model (three API surfaces, two-package story, response envelope, nonce tracker) so any LLM reading has the scope before the method-level reference. Use this when your tool doesn't understand `llms.txt` or you want one-shot context. ## What's included Both files cover: - **Getting started** — install, quickstart, core concepts - **Packages** — how to pick between `@oviato/sdk` and `@oviato/connect` - **Chain-agnostic** — multi-chain actions (`sendTransfer`, `signMessage`, `switchNetwork`, `getBalance`, auth, session) - **EVM** — the `evm.*` namespace (reads, writes, recipes, troubleshooting, migration from viem/ethers) - **UTXO** — the `utxo.*` namespace (PSBT signing) - **React components** — provider, hooks, ``, `` - **x402 payments**, **RPC reference**, **developer dashboard** - **Full changelog** Both are regenerated from the sidebar's `meta.json` on every deploy — drift between sidebar and LLM export is structurally impossible. ## Setup in Claude Code Add to your project's `CLAUDE.md`: ```markdown ## Documentation - Oviato LLM index: https://docs.oviato.com/llms.txt - Oviato full docs: https://docs.oviato.com/llms-full.txt - Oviato skill (deeper patterns): https://docs.oviato.com/skills/oviato.md ``` Claude Code will consult these when you ask Oviato-related questions. Install the Oviato skill for richer guidance (decision trees, recipes, anti-patterns): ```bash curl -o .claude/skills/oviato.md https://docs.oviato.com/skills/oviato.md ``` See [Skills](/ai-integration/skills) for details. ## Setup in Cursor 1. Open **Cursor Settings** → **Features** → **Docs** 2. Click **Add new doc** 3. Paste: `https://docs.oviato.com/llms-full.txt` 4. Name it `Oviato` Use `@docs Oviato` in chat to reference. Cursor also supports `.cursorrules` at your project root. Grab our template: ```bash curl -o .cursorrules https://docs.oviato.com/templates/cursorrules.txt ``` ## Setup in Antigravity / Gemini CLI Add to your project's `GEMINI.md` or `AGENTS.md`: ```markdown ## Documentation - Oviato LLM index: https://docs.oviato.com/llms.txt - Oviato full docs: https://docs.oviato.com/llms-full.txt ``` The skill also works under `.agents/skills/` — copy `oviato.md` in. ## Setup in Codex Paste the `llms-full.txt` URL into your repo's setup instructions or system prompt. For CLI agents, you can pre-fetch: ```bash curl https://docs.oviato.com/llms-full.txt > .oviato-docs.txt ``` Then reference `.oviato-docs.txt` in your agent's context. ## Setup in ChatGPT / other web UIs Download and attach: ```bash curl -O https://docs.oviato.com/llms-full.txt ``` Upload to a conversation or add to a Custom GPT's knowledge base. ## How fresh are these files? Regenerated on every docs deploy. The generator walks the `meta.json` sidebar — if a page ships in the docs, it's in the LLM files within minutes of deploy. If your agent ever gives you stale API details (e.g. an old import path), ask it to re-fetch `https://docs.oviato.com/llms-full.txt`. The skill file explicitly instructs agents to do this. ## Related - [Skills](/ai-integration/skills) — install the Oviato skill for decision trees + recipes - [Overview](/ai-integration/overview) — AI workflow overview --- # Skills > Install the Oviato skill for AI-assisted development in Claude Code, Cursor, Antigravity, and Codex Canonical URL: https://docs.oviato.com/ai-integration/skills File: ai-integration/skills.mdx Skills are markdown files that give AI coding tools deep knowledge about the Oviato SDK. Instead of configuring each tool separately, you install one skill and your AI assistant understands authentication, wallets, transactions, RPC, and all Oviato patterns. ## Oviato Skill The Oviato skill teaches your AI assistant the SDK's mental model, decision trees, recipes, and anti-patterns: - **Three API surfaces** — chain-agnostic (`sendTransfer`, `signMessage`, `switchNetwork`, `getBalance`, auth), `evm.*` (reads, writes, tokens, NFTs, EIP-712, fees, receipts), `utxo.*` (PSBT signing) - **Two packages, one API** — when to pick `@oviato/sdk` vs `@oviato/connect`, the exact import rules, why installing both is a foot-gun - **Decision trees** — "which package?", "which surface for this task?", "popup or iframe?" - **Response envelope pattern** — discriminated `{ status, type, data }` returns, how to narrow, why cancellation isn't a thrown exception - **Nonce tracker** — what it is, why you never manage nonces, how auto-release works on pre-mempool errors - **Recipes** — connect + session, native send, ERC-20 transfer, approve + use, typed-data signing, PSBT signing, multicall - **Error-keyed troubleshooting** — replacement underpriced, nonce too low, missing revert data, popup blocked, etc. - **Anti-patterns** — explicit "don't do this" list (install both packages, `EIP712Domain` in types, parsing `formatted` for math, etc.) - **Scope limits** — explicitly tells the agent what's NOT supported (ordinals/inscriptions, Solana, `watchAsset`, `simulateContract`) so it won't invent APIs The skill also instructs the agent to fetch `https://docs.oviato.com/llms-full.txt` for anything not covered — so it stays accurate even after the SDK evolves. ### View the Skill You can view and download the full skill file here: ``` https://docs.oviato.com/skills/oviato.md ``` ## How to Use Once installed, your AI assistant will automatically apply Oviato knowledge when you: - Ask it to implement authentication, wallet features, or transactions - Reference `@oviato/connect` / `@oviato/sdk` / `evm.*` / `utxo.*` in your code - Ask about PSBT signing, contract calls, EIP-712, gas estimation, or RPC calls - Migrate code from viem or ethers - Need help debugging Oviato-related errors You don't need to prompt it with any special commands — the skill is always active in your project. ### Example Prompts After installing the skill, try: ``` Set up Oviato authentication in my Next.js app with OviConnectProvider and a connect button. ``` ``` Add a "Send Bitcoin" button that calls sendTransfer when the user clicks it. Show the txid on success. ``` ``` I need to sign a PSBT hex string and broadcast it using the Oviato SDK. Show me how to call signPsbt with broadcast enabled. ``` ``` Fetch and display the user's wallet balance using the Oviato RPC client. ``` ## How to Install ### Claude Code Create the skill file in your project's `.claude/skills/` directory: ```bash mkdir -p .claude/skills/oviato curl -o .claude/skills/oviato/SKILL.md https://docs.oviato.com/skills/oviato.md ``` Claude Code automatically reads skills from `.claude/skills/` in your project. No additional configuration needed. ### Cursor Create a `.cursor/rules/` directory and add the skill as a rule file: ```bash mkdir -p .cursor/rules curl -o .cursor/rules/oviato.mdc https://docs.oviato.com/skills/oviato.md ``` Cursor will automatically pick up rules from `.cursor/rules/` and apply them as context. ### Antigravity Add the skill to your project's `.agents/skills/` directory: ```bash mkdir -p .agents/skills curl -o .agents/skills/oviato.md https://docs.oviato.com/skills/oviato.md ``` Antigravity reads skills from `.agents/skills/` at the workspace level. It also supports `GEMINI.md` and `AGENTS.md` at the project root for rules. ### Codex Add the skill to your repository's `AGENTS.md` or Codex setup instructions. Download the skill file and include its contents: ```bash curl -o AGENTS.md https://docs.oviato.com/skills/oviato.md ``` Codex will read `AGENTS.md` from your repository root as project context. ### Other Tools For any AI coding tool that supports project-level context files: 1. Download the skill: [oviato.md](https://docs.oviato.com/skills/oviato.md) 2. Place it in your tool's context/rules directory 3. The AI will use it as persistent knowledge for your project ## Keeping the Skill Updated The skill file at `https://docs.oviato.com/skills/oviato.md` is kept in sync with the latest SDK releases. To update: ```bash # Re-download to overwrite your local copy curl -o .claude/skills/oviato/SKILL.md https://docs.oviato.com/skills/oviato.md ``` Replace the path with wherever you installed the skill for your tool. --- # Section: Packages # Choosing a Package > Which Oviato package to install — @oviato/sdk or @oviato/connect — and which imports to use. Canonical URL: https://docs.oviato.com/packages/choosing File: packages/choosing.mdx Oviato ships **one SDK, two packages.** Pick based on your framework; the API surface is identical under the hood. ## TL;DR Install **`@oviato/connect`**. ```bash pnpm add @oviato/connect ``` Import everything from `@oviato/connect` (for actions) or `@oviato/connect/client` (for React hooks + components). ```tsx // Actions — re-exports all of @oviato/sdk import { evm, utxo, sendTransfer, signMessage } from '@oviato/connect'; // React hooks + components import { OviConnectProvider, useOviConnect, ConnectButton } from '@oviato/connect/client'; ``` Install **`@oviato/sdk`**. ```bash pnpm add @oviato/sdk ``` Import directly. ```ts import { evm, utxo, sendTransfer, signMessage, initialize, login } from '@oviato/sdk'; ``` Works with Vue, Svelte, Angular, Solid, vanilla JS, Node, Deno, Bun. ## What's in each package **`@oviato/sdk`** — the framework-agnostic core. Contains everything: - Authentication (`login`, `register`, `authenticate`) - Chain-agnostic actions (`signMessage`, `sendTransfer`, `switchNetwork`, `getBalance`) - EVM-specific surface (`evm.*` — reads, writes, typed data, receipts) - UTXO-specific surface (`utxo.*` — PSBT signing) - RPC client (`rpc`, `rpcRequest`) - x402 payments (`x402`) - Types + bigint helpers Runtime dep: `zustand` only. ~11 KB gzipped. **`@oviato/connect`** — `@oviato/sdk` + React bindings on top. Two entry points: - `@oviato/connect` — **re-exports 100% of `@oviato/sdk`.** Nothing new. - `@oviato/connect/client` — adds `OviConnectProvider`, `useOviConnect`, `useOviStore`, `ConnectButton`, `Connect` (React 18+). Because `@oviato/connect` re-exports everything from `@oviato/sdk`, a React dev never needs to install both. One package, everything on hand. ## The one confusion we want to squash You will see both `@oviato/sdk` and `@oviato/connect` in code samples on other sites and in GitHub issues. **They're the same API.** The function names, parameters, and return shapes are identical — `@oviato/connect` is a pass-through. In this documentation we use `@oviato/connect` in examples by default (React is our primary audience). Swap to `@oviato/sdk` if you're using a different framework. Nothing else changes. **Do not install both packages.** `@oviato/connect` already depends on `@oviato/sdk` and re-exports everything. Installing both leads to duplicate module instances and subtle state-sharing bugs — the Zustand store lives in `@oviato/sdk`'s module scope, and two copies mean two stores. ## Import cheat sheet Use this as a reminder when copy-pasting from mixed sources: | You need… | `@oviato/connect` (React) | `@oviato/sdk` (any framework) | |---|---|---| | Auth (`login`, `register`, `authenticate`) | `@oviato/connect` | `@oviato/sdk` | | Session (`getSession`, `clearSession`) | `@oviato/connect` | `@oviato/sdk` | | Chain-agnostic actions (`sendTransfer`, `signMessage`) | `@oviato/connect` | `@oviato/sdk` | | EVM namespace (`evm.*`) | `@oviato/connect` | `@oviato/sdk` | | UTXO namespace (`utxo.*`) | `@oviato/connect` | `@oviato/sdk` | | RPC client (`rpc`, `rpcRequest`) | `@oviato/connect` | `@oviato/sdk` | | x402 payments (`x402`) | `@oviato/connect` | `@oviato/sdk` | | Types, bigint helpers | `@oviato/connect` | `@oviato/sdk` | | **React provider, hooks, components** | **`@oviato/connect/client`** | not available | ## Bundle sizes - `@oviato/sdk` — ~11 KB gzipped (ESM) - `@oviato/connect` (re-export entry) — thin layer over `@oviato/sdk`, ~12 KB gzipped - `@oviato/connect/client` — adds React components, ~14 KB gzipped All entry points are tree-shakeable — only imported functions ship. ## CDN usage (SDK only) For quick prototypes without a build step, load `@oviato/sdk` from a CDN: ```html ``` The CDN bundle is UMD — everything is under the global `OviatoSDK`. Not recommended for production. ## Next steps - [Quickstart](/getting-started/quickstart) — ship a working sign-in in 10 minutes - [Core Concepts](/getting-started/core-concepts) — passkey flow, session scope, popup-vs-iframe, nonce tracker - [Chain-Agnostic → Overview](/chain-agnostic/overview) — methods that work across BTC + EVM --- # Section: Chain-Agnostic # Chain-Agnostic Overview > Methods that work identically across Bitcoin + EVM with one API. Oviato's unique value proposition. Canonical URL: https://docs.oviato.com/chain-agnostic/overview File: chain-agnostic/overview.mdx This is what Oviato does that no other wallet SDK does cleanly: **one API that works across chain families.** Same function names, same return shapes — the SDK routes by the session's active network. ```ts import { signMessage, sendTransfer, switchNetwork, getBalance } from '@oviato/connect'; // Same code, different chain each run const bal = await getBalance({ address: session.address }); await signMessage('Hello from Oviato'); await sendTransfer({ recipient: { address, amount } }); ``` No branches on `network === 'eth'` vs `network === 'bitcoin'`. The SDK dispatches internally. ## What's chain-agnostic | Method | Works on | Dispatches by | |---|---|---| | [`signMessage`](/chain-agnostic/sign-messages) | BTC + EVM | session network | | [`sendTransfer`](/chain-agnostic/send-transfers) | BTC + EVM (native only) | session network | | [`switchNetwork`](/chain-agnostic/session-and-network) | any → any | target network | | [`getBalance`](/chain-agnostic/get-balance) | BTC + EVM (via `rpc`) | session network | | Auth: [`login`](/chain-agnostic/authentication), `register`, `authenticate` | any | n/a — WebAuthn is universal | | Session: `getSession`, `clearSession`, `isAuthenticated` | any | n/a | ## What's chain-specific (lives elsewhere) When you're doing something that genuinely only works on one chain family, use the dedicated namespace: - **EVM:** contract calls, token reads, NFTs, typed data, gas fees → [`evm.*`](/evm/overview) - **UTXO:** PSBT signing → [`utxo.*`](/utxo/overview) The namespaces exist precisely so you don't accidentally call an EVM method on a Bitcoin session. Runtime errors become impossible compile-time mistakes. ## The dispatch model Every chain-agnostic method inspects the session's network and chooses the right backend path: ``` sendTransfer({ recipient }) ↓ session.network ├─ 'bitcoin' / 'bitcointestnet' → UTXO prepare path (PSBT, fee_rate, multi-recipient supported) └─ 'eth' / 'base' / 'basesepolia' / 'ethsepolia' → EVM prepare path (nonce, gas, value as wei) ``` Return shapes are aligned where possible: - `sendTransfer` → `{ txid, network, explorerUrl }` — same across both chain families - `getBalance` → discriminated union: `{ chainType: 'utxo', raw: number, confirmed, unconfirmed, ... }` OR `{ chainType: 'evm', raw: bigint, ... }` so your code narrows explicitly ## Amount unit conventions Different chains have different native units. The SDK uses whatever's overflow-safe for the chain: | Chain family | Unit | Type | |---|---|---| | Bitcoin + other UTXO | satoshis (1 BTC = 10⁸ sats) | `number` | | Ethereum + EVM | wei (1 ETH = 10¹⁸ wei) | `bigint` | `number` overflows past 2⁵³−1 (~9×10¹⁵). UTXO amounts never get that big (max BTC supply × 10⁸ ≈ 2.1×10¹⁵). EVM amounts easily exceed it, so we use `bigint` there. `sendTransfer` accepts both — pass `number` or `bigint` based on the active chain. ```ts // Bitcoin — satoshis as number await sendTransfer({ recipient: { address: 'bc1q...', amount: 10000 }, // 0.0001 BTC }); // Ethereum — wei as bigint await sendTransfer({ recipient: { address: '0x...', amount: 10n ** 15n }, // 0.001 ETH }); ``` ## When "chain-agnostic" isn't quite true A few caveats worth calling out: - **`sendTransfer` on EVM** sends *native* coin (ETH/Base ETH/etc.) only. For ERC-20 / NFT / contract calls, use [`evm.writeContract`](/evm/writes). - **`sendTransfer` with multiple recipients** works only on UTXO (Bitcoin supports multi-output natively; EVM doesn't in one tx). - **`signMessage`** produces different signature formats: EIP-191 on EVM, Bitcoin message signing on UTXO. Verification needs to know which. ## When to use chain-agnostic vs chain-specific **Chain-agnostic** when you're writing code that should work across chain families — a generic "connect + send" UI, a faucet, a multi-chain dashboard. **Chain-specific** when you're doing something that genuinely only makes sense for one chain — a DeFi app (EVM), a PSBT-heavy Bitcoin flow, a DEX swap. Mixing is fine: ```ts import { getBalance, signMessage, evm } from '@oviato/connect'; async function sendUsdcWithAuthProof(amount: bigint, recipient: `0x${string}`) { // Chain-agnostic: fetch balance for a pre-check const { balance } = await getBalance({ address: session.address }); // Chain-agnostic: sign an auth message for off-chain logging await signMessage(`Authorize transfer of ${amount} at ${Date.now()}`); // Chain-specific: the actual USDC transfer (EVM-only) return evm.writeContract({ address: USDC, abi: ERC20_ABI, functionName: 'transfer', args: [recipient, amount], }); } ``` ## Explore --- # Authentication > Learn how to authenticate users with passkeys using @oviato/connect Canonical URL: https://docs.oviato.com/chain-agnostic/authentication File: chain-agnostic/authentication.mdx @oviato/connect provides three authentication methods: smart authentication, explicit login, and explicit registration. This guide covers all authentication flows. ## Authentication Methods ### Smart Authentication (Recommended) The `authenticate()` function automatically detects whether the user is new or returning: ```javascript import { authenticate } from "@oviato/connect"; const result = await authenticate({ username: "alice", // optional: pre-fill username }); if (result.status === "success") { console.log("Connected:", result.data.address); console.log("Username:", result.data.username); console.log("Network:", result.data.network); } ``` **When to use**: Most of the time. This provides the best user experience. ### Explicit Login For returning users who already have a passkey: ```javascript import { login } from "@oviato/connect"; const result = await login({ username: "alice", // optional: pre-fill username }); if (result.status === "success") { console.log("Logged in:", result.data.address); } ``` **When to use**: When you know the user already has an account (e.g., they clicked "Login" instead of "Sign Up"). ### Explicit Registration For new users creating an account: ```javascript import { register } from "@oviato/connect"; const result = await register({ username: "bob", // required for registration }); if (result.status === "success") { console.log("Registered:", result.data.address); } ``` **When to use**: When you have separate sign-up and login flows. ## Authentication Options ### Pre-filling Username You can pre-fill the username field: ```javascript await authenticate({ username: "alice@ovi.id", }); ``` ### Without Pre-filled Username Let users enter their own username: ```javascript await authenticate(); // Opens modal with empty username field ``` ## Response Handling All authentication methods return a `BridgeResponse` object: ```typescript type BridgeResponse = { status: "success" | "error"; data?: T; message?: string; }; ``` ### Success Response ```javascript const result = await authenticate(); if (result.status === "success") { console.log("Session data:", result.data); // result.data contains UserSession } ``` ### Error Handling ```javascript const result = await authenticate(); if (result.status === "error") { console.error("Authentication failed:", result.message); // Handle specific errors if (result.message.includes("cancelled")) { console.log("User cancelled authentication"); } else if (result.message.includes("timeout")) { console.log("Authentication timed out"); } } ``` ## Session Data After successful authentication, access the session: ```javascript import { getSession } from "@oviato/connect"; const session = getSession(); if (session) { console.log("Username:", session.username); console.log("Address:", session.address); console.log("Public Key:", session.publicKey); console.log("Network:", session.network); console.log("HD Keys:", session.hdKeys); } ``` ### Session Type ```typescript type UserSession = { username: string; // OVI.ID username address: string; // Current network address publicKey: string; // Current network public key network: string; // Active network networkType: string; // Network type (e.g., 'utxo', 'evm') networkEnvironment: string; // Environment ('mainnet', 'testnet') hdKeys: { mainnet: string; // Mainnet HD public key testnet: string; // Testnet HD public key }; auth?: { token: string; // RPC auth token expires: string; // Token expiration (ISO 8601) }; }; ``` ## Checking Authentication Status ```javascript import { isAuthenticated, getSession } from "@oviato/connect"; // Quick check if (isAuthenticated()) { console.log("User is authenticated"); } // With session data const session = getSession(); if (session) { console.log("User is authenticated as:", session.username); } ``` ## Logout Clear the user session: ```javascript import { clearSession } from "@oviato/connect"; // Logout user clearSession(); // Verify logout console.log("Authenticated:", isAuthenticated()); // false ``` ## Complete Authentication Example Here's a complete example with all authentication flows: ```javascript import { initialize, authenticate, login, register, getSession, isAuthenticated, clearSession, } from "@oviato/connect"; // Initialize SDK await initialize({ appId: "your-app-id", network: "bitcoinmainnet", }); // Check if already authenticated if (isAuthenticated()) { const session = getSession(); console.log("Already logged in as:", session.username); } else { // Authenticate user const result = await authenticate(); if (result.status === "success") { const session = getSession(); console.log("Successfully authenticated!"); console.log("Username:", session.username); console.log("Address:", session.address); } else { console.error("Authentication failed:", result.message); } } // Later: logout function handleLogout() { clearSession(); console.log("Logged out"); } ``` ## Framework-Specific Examples ### Vue 3 ```vue ``` ### Svelte ```svelte {#if error}
{error}
{/if} {#if session}

Welcome, {session.username}!

Address: {session.address}

{:else} {/if} ``` ## WebAuthn / Passkeys Oviato uses WebAuthn for secure, passwordless authentication: ### How It Works 1. **Registration**: Creates a new passkey tied to the user's device 2. **Authentication**: Uses the passkey to verify the user's identity 3. **Signing**: Uses the passkey to sign messages and transactions ### Browser Support WebAuthn is supported in: - Chrome 67+ - Safari 14+ - Firefox 60+ - Edge 18+ ### Requirements - **HTTPS**: Required in production (localhost works for development) - **Biometrics**: Touch ID, Face ID, or security key - **User Gesture**: Must be triggered by user interaction (button click) ## Troubleshooting ### "WebAuthn not supported" Ensure: - Using HTTPS (required in production) - Browser supports WebAuthn - Device has biometric hardware or security key ### "User cancelled" User closed the authentication dialog or cancelled biometric prompt. ### "Username already exists" When using `register()`, the username is already taken. Try `login()` instead or use `authenticate()` for auto-detection. ### "No passkey found" When using `login()`, no passkey exists for this username. User should `register()` first or use `authenticate()`. ### Session not persisting Check: - Cookies are enabled - LocalStorage is accessible - Domain is in allowed domains list (dashboard) - Using HTTPS in production ## Security Best Practices ### 1. Verify Session on Server Never trust client-side session data alone. Use the `auth.token` for server-side verification: ```javascript const session = getSession(); // Send token to your backend fetch("/api/verify", { headers: { Authorization: `Bearer ${session.auth.token}`, }, }); ``` ### 2. Check Token Expiration ```javascript const session = getSession(); if (session?.auth) { const expiresAt = new Date(session.auth.expires); const now = new Date(); if (now > expiresAt) { console.warn("Auth token expired, re-authenticate"); clearSession(); } } ``` ### 3. Handle Network Changes When users switch networks, re-authenticate if needed: ```javascript import { switchNetwork } from "@oviato/connect"; const result = await switchNetwork("ethmainnet"); if (result.status === "success") { // Session automatically updated const session = getSession(); console.log("New network:", session.network); console.log("New address:", session.address); } ``` ## Next Steps - **[Wallet Operations](/chain-agnostic/overview)** - Learn about signing and transactions --- # Session & Network > Read the active session, switch networks, and refresh the JWT. Works identically across Bitcoin + EVM. Canonical URL: https://docs.oviato.com/chain-agnostic/session-and-network File: chain-agnostic/session-and-network.mdx Every SDK method that touches a wallet reads from the session — a small in-memory object that holds the user's address, network, hdkeys, and auth token. This page covers reading, updating, and clearing it. ## Read the session ```ts import { getSession } from '@oviato/connect'; const session = getSession(); if (!session) { // User isn't logged in } else { console.log(session.username); // 'alice' console.log(session.address); // 'bc1q...' or '0x...' console.log(session.network); // 'bitcoin' | 'eth' | 'base' | ... console.log(session.networkType); // 'utxo' | 'evm' } ``` ### React hook equivalent ```tsx import { useOviConnect } from '@oviato/connect/client'; function Profile() { const { session, isLoading, disconnect } = useOviConnect(); if (isLoading) return

Loading…

; if (!session) return

Please connect

; return (

{session.username}

); } ``` ## Check whether authenticated ```ts import { isAuthenticated } from '@oviato/connect'; if (isAuthenticated()) { // Session exists AND JWT hasn't expired } ``` This is stricter than `!!getSession()` — it also checks that the bearer token is still valid (not expired). ## Clear the session ```ts import { clearSession } from '@oviato/connect'; clearSession(); ``` Wipes the in-memory store + the localStorage cache. The user's passkey isn't affected — they can `login()` again and pick up where they left off. Server-side sessions aren't revoked — only the client-side copy is dropped. If you need to revoke the JWT server-side (account takeover, compromised device), call `auth.revokeToken()` via the dashboard. ## Switch network Changes the active chain. Requires a passkey tap because the SDK re-signs the session JWT for the new chain — this prevents a stolen JWT from being silently repointed. ```ts import { switchNetwork, Network } from '@oviato/connect'; const r = await switchNetwork(Network.BASEMAINNET); if (r.status === 'success' && r.type === 'switchNetworkSuccess') { console.log('Now on', r.data.network); console.log('New address:', r.data.session.address); } ``` ### What changes Switching networks re-derives the user's address (addresses are deterministic per (user, app, network)) and re-issues the JWT with a chain-scoped claim. After a successful switch: - `session.network` → the new network - `session.networkType` → `'utxo'` or `'evm'` based on the new chain - `session.address` → the deterministic address on the new chain - The JWT's `network` claim matches — required for write methods ### Supported networks - `Network.BTCMAINNET` (`'bitcoin'`) - `Network.BTCTESTNET` (`'bitcointestnet'`) - `Network.ETHMAINNET` (`'eth'`) - `Network.ETHSEPOLIA` (`'ethsepolia'`) - `Network.BASEMAINNET` (`'base'`) - `Network.BASESEPOLIA` (`'basesepolia'`) More come online via the dashboard. If you try to switch to an unsupported network for your app, the call fails with a clear error. ### Ethers-style alias ```ts import { evm } from '@oviato/connect'; await evm.switchChain(Network.BASEMAINNET); ``` Identical to `switchNetwork` — kept for ethers-ecosystem familiarity. ## The session shape ```ts type Session = { username: string; address: string; publicKey: string; network: Network; networkType: 'utxo' | 'evm'; hdKeys?: { mainnet: string; testnet: string; solana: string }; pk?: string; auth?: { token: string; // Bearer JWT expires: string; // ISO 8601 }; }; ``` ## Subscribe to session changes (React) ```tsx import { useOviConnect } from '@oviato/connect/client'; function App() { const { session } = useOviConnect(); useEffect(() => { if (session) { console.log('User logged in:', session.address); } else { console.log('User logged out'); } }, [session]); } ``` For vanilla JS, the underlying store is Zustand — subscribe directly: ```ts import { oviStore } from '@oviato/connect'; const unsub = oviStore.subscribe((state) => { console.log('Session changed:', state.session); }); ``` ## Related - [Authentication](/chain-agnostic/authentication) — login/register/authenticate flows - [Core Concepts → Sessions are chain-scoped](/getting-started/core-concepts#sessions-are-chain-scoped) --- # Sign Messages > Sign arbitrary messages with the user's passkey. Same API across Bitcoin + EVM, different wire format per chain. Canonical URL: https://docs.oviato.com/chain-agnostic/sign-messages File: chain-agnostic/sign-messages.mdx `signMessage` produces a cryptographic signature over an arbitrary string, authenticated by the user's passkey. Works on every chain family — the signature format is dispatched by the session's network type. ```ts import { signMessage } from '@oviato/connect'; const r = await signMessage('Hello, Oviato!'); if (r.status === 'success' && r.type === 'signMessageSuccess') { console.log(r.data.signature); } ``` ## What you get back ```ts type Result = | { status: 'success'; type: 'signMessageSuccess'; data: { signature: string; message: string; publicKey: string } } | { status: 'error'; message: string }; ``` Narrow on `status`: ```ts const r = await signMessage('Verify my ownership'); if (r.status === 'success' && r.type === 'signMessageSuccess') { await sendToBackend({ message: r.data.message, signature: r.data.signature, publicKey: r.data.publicKey, address: session.address, }); } ``` ## How the signature is formatted Different chains, different wire formats — same API. On EVM networks, messages are signed per [EIP-191](https://eips.ethereum.org/EIPS/eip-191) — the message is prefixed with `\x19Ethereum Signed Message:\n` before signing, so you can't trick a user into signing a raw transaction hash. ```ts // Signed message: '\x19Ethereum Signed Message:\n13Hello, world!' const r = await signMessage('Hello, world!'); // r.data.signature → '0x…' (65 bytes: r + s + v) ``` Verify with [`evm.verifyMessage`](/evm/reads#eip-191-plain-messages): ```ts import { evm } from '@oviato/connect'; const valid = await evm.verifyMessage({ address: session.address, message: r.data.message, signature: r.data.signature, }); ``` On UTXO networks, messages are signed using the [Bitcoin Signed Message](https://en.bitcoin.it/wiki/Message_signing) standard — a prefix of `\x18Bitcoin Signed Message:\n` wrapped in a double-sha256, signed with the address's private key. ```ts const r = await signMessage('Hello, world!'); // r.data.signature → base64 string (65 bytes raw → base64) ``` Verification uses standard Bitcoin libraries (bitcoinjs, bitcore, etc.). The SDK doesn't ship a built-in verifier today — pass the signature to your backend for verification. ## Common use cases ### Prove wallet ownership Before a backend grants a user access to a premium feature, have them sign a challenge that includes a nonce + timestamp. ```ts // 1. Fetch a challenge from your backend const { challenge } = await fetch('/api/auth/challenge').then((r) => r.json()); // challenge example: 'Sign to prove ownership: nonce=abc123 ts=1700000000' // 2. Have the user sign it const r = await signMessage(challenge); if (r.status !== 'success' || r.type !== 'signMessageSuccess') return; // 3. Send back for verification const { granted } = await fetch('/api/auth/verify', { method: 'POST', body: JSON.stringify({ message: r.data.message, signature: r.data.signature, address: session.address, }), }).then((r) => r.json()); ``` ### Off-chain consent Get a signed consent to avoid an on-chain gas cost: ```ts const intent = `I authorize a transfer of 100 USDC to 0x... at ${Date.now()}`; const r = await signMessage(intent); // Store r.data.signature for later — execute on-chain if/when needed ``` ### Sign-in with [SIWE](https://eips.ethereum.org/EIPS/eip-4361) (EVM) SIWE is the canonical "sign in with Ethereum" flow. Build the message per spec, sign it: ```ts const domain = 'example.com'; const statement = 'Sign in to Example'; const nonce = crypto.randomUUID(); const issuedAt = new Date().toISOString(); const message = `${domain} wants you to sign in with your Ethereum account: ${session.address} ${statement} URI: https://${domain} Version: 1 Chain ID: 1 Nonce: ${nonce} Issued At: ${issuedAt}`; const r = await signMessage(message); // Send r.data.signature + message to your backend for verification ``` ## Security notes **Never have users sign an untrusted message.** Messages can be structured to look like plain text but actually encode a transaction. Always show users the full message content before signing, and treat any signing request as deliberate on your part. - Signatures produced by `signMessage` authenticate the user's passkey, not their on-chain wallet directly. Verification recovers the *public key*, which you then match against the expected session address. - Nonces + timestamps in signed messages prevent replay attacks. - Oviato's bridge UI displays the full message to the user before they approve. For ASCII messages this is straightforward; for binary payloads users see a hex dump — avoid binary content in user-facing flows. ## Options ```ts await signMessage('...', { method: 'popup', // or 'iframe' — how the approval UI opens }); ``` Defaults to **iframe** — the SDK only falls back to popup on localhost / non-HTTPS or on Safari/iOS for passkey operations. In production you get iframe unless you override. ## Related - [EVM Writes → signTypedData](/evm/writes#sign-eip-712-typed-data) — structured EIP-712 data - [EVM Reads → Verify](/evm/reads#signature-verification-offchain) — recover + check a signature --- # Send Transfers > Send native coin to an address. One API for Bitcoin (satoshis) and EVM chains (wei). Single recipient or batch on UTXO. Canonical URL: https://docs.oviato.com/chain-agnostic/send-transfers File: chain-agnostic/send-transfers.mdx `sendTransfer` moves native coin (BTC, ETH, Base ETH, etc.) from the session address to one or more recipients. Dispatched by session network. ```ts import { sendTransfer } from '@oviato/connect'; const r = await sendTransfer({ recipient: { address: '0x742d35Cc...', amount: 10n ** 15n }, fee: 'auto', }); if (r.status === 'success' && r.type === 'sendTransferSuccess') { console.log('TxID:', r.data.txid); console.log('Explorer:', r.data.explorerUrl); } ``` For ERC-20 / NFT / contract calls on EVM, use [`evm.writeContract`](/evm/writes) instead — `sendTransfer` is *native coin only*. ## Amount units per chain | Chain | Unit | Type | |---|---|---| | Bitcoin | satoshis (1 BTC = 100 000 000 sats) | `number` | | Ethereum, Base | wei (1 ETH = 10¹⁸ wei) | `bigint` (preferred), `string`, or `number` for small values | ```ts // Bitcoin await sendTransfer({ recipient: { address: 'bc1q...', amount: 10_000 }, // 0.0001 BTC }); // EVM — use bigint for overflow safety await sendTransfer({ recipient: { address: '0x...', amount: 10n ** 15n }, // 0.001 ETH }); ``` ## Single vs batch recipients ```ts await sendTransfer({ recipient: { address, amount }, fee: 'auto', memo: 'Payment for services', // optional }); ``` Works on Bitcoin + EVM. Bitcoin natively supports multiple outputs in one transaction. Save on fees vs sending N separate txs. ```ts await sendTransfer({ recipients: [ { address: 'bc1q...', amount: 10000 }, { address: 'bc1p...', amount: 5000 }, { address: 'bc1q...', amount: 7500 }, ], fee: 'auto', memo: 'Splitting a payment', }); ``` Attempting `recipients: []` on an EVM chain returns an error — multi-recipient doesn't exist in EVM. ## Fee handling Fees are chain-specific, but the API is unified via semantic tiers: ```ts await sendTransfer({ recipient: { address, amount }, fee: 'fast', // or 'medium' | 'slow' | 'auto' | }); ``` - `auto` (default) — sensible default, ≈ `medium` - `fast`, `medium`, `slow` — explicit tiers. Map to fee multipliers per chain. - `` — raw fee rate. On Bitcoin it's sat/vB; on EVM it's wei gas-price (not recommended — prefer tiers). The bridge UI shows three tiers with current cost estimates before the user confirms, and lets them pick inline. ## The full response shape ```ts type Result = | { status: 'success'; type: 'sendTransferSuccess'; data: { txid, network, explorerUrl } } | { status: 'error'; message: string }; ``` Always narrow on `status`: ```ts const r = await sendTransfer({ recipient: { address, amount } }); if (r.status === 'success' && r.type === 'sendTransferSuccess') { console.log(r.data.txid); } else if (r.status === 'error') { console.error(r.message); } ``` EVM writes return `data.hash` via `evm.sendTransaction` / `evm.writeContract`. `sendTransfer` uses `data.txid` universally. Both are the same 0x-prefixed hex — the field name is different for historical reasons. ## Memo / OP_RETURN Bitcoin supports a small arbitrary payload per transaction via `OP_RETURN`: ```ts await sendTransfer({ recipient: { address, amount }, memo: 'Invoice #1234', }); ``` Max size: ~80 bytes. Larger memos are rejected. On EVM, `memo` is ignored — `sendTransaction`'s `data` field serves the same purpose but has different semantics (it's calldata, not a comment). ## Full parameter list | Field | Type | Notes | |---|---|---| | `recipient` | `{ address, amount }` | For single-recipient sends. XOR with `recipients`. | | `recipients` | `[{ address, amount }, ...]` | UTXO only. XOR with `recipient`. | | `fee` | `'auto' \| 'fast' \| 'medium' \| 'slow' \| number` | Default `'auto'`. | | `memo` | `string` | UTXO only. Max ~80 bytes. | | `method` | `'popup' \| 'iframe'` | How the approval UI opens. | ## Gotchas **Amount is smallest unit, always.** Bitcoin gives you satoshis, EVM gives you wei. `amount: 1` on Bitcoin is 1 satoshi (0.00000001 BTC), not 1 BTC. Use `parseUnits('1', 18)` for ETH or `10000` for 0.0001 BTC. - **Dust outputs.** On Bitcoin, amounts below the dust threshold (≈546 sats) are rejected. - **Insufficient funds.** Not just `amount > balance`, but `amount + fee > balance`. The SDK fetches a fee estimate before confirming. - **Self-sends.** Sending to your own address works — useful for testing. The fee still applies. ## Preview before sending To show a transaction summary before the user confirms (cost, USD value, decoded recipient), call `rpc.prepareTx` with the same params: ```ts import { rpc } from '@oviato/connect'; const preview = await rpc.prepareTx({ recipient: { address, amount }, fee: 'auto', publicKey: session.publicKey, address: session.address, }); console.log(preview.result.feeTotalUsd); ``` ## Related - [EVM Writes](/evm/writes) — contract calls, token transfers, EIP-712 - [UTXO → Sign PSBT](/utxo/sign-psbt) — advanced Bitcoin signing via PSBTs --- # Get Balance > Fetch native coin balance for any address on any chain. Discriminated return — EVM gives bigint wei, UTXO gives confirmed + unconfirmed satoshis. Canonical URL: https://docs.oviato.com/chain-agnostic/get-balance File: chain-agnostic/get-balance.mdx Get balance uses the RPC client to dispatch to the right backend based on the session's active network. The return shape differs per chain family so you can access chain-specific fields (like UTXO's confirmed/unconfirmed split) when needed. ```ts import { rpc, getSession } from '@oviato/connect'; const session = getSession(); const r = await rpc.getBalance({ address: session.address }); if (r.result) { console.log(r.result.balance.formatted); // '1.2345' console.log(r.result.symbol); // 'ETH' | 'BTC' | 'USDC' etc. } ``` `getBalance` lives under the `rpc` client today. A flat top-level `getBalance({ address })` with the spec-flattened return shape is on the roadmap — the current form works and is stable for use. ## Return shape The response is a discriminated union keyed on `chainType`: ### EVM ```ts { address: string; network: string; // 'eth' | 'base' | ... networkName: string; // 'Ethereum' | 'Base' | ... chainType: 'evm'; balance: { raw: string; // wei as decimal string (avoids bigint-over-JSON) formatted: string; // '1.2345' (native units) symbol: string; // 'ETH' | 'BASE' | ... decimals: number; // 18 }; symbol: string; decimals: number; } ``` ### UTXO (Bitcoin) ```ts { address: string; network: string; // 'bitcoin' | 'bitcointestnet' | ... networkName: string; chainType: 'utxo'; balance: { raw: number; // satoshis formatted: string; // '0.0125' symbol: string; // 'BTC' | 'tBTC' decimals: number; // 8 confirmed: { // mined-only portion raw: number; formatted: string; symbol: string; decimals: number; }; unconfirmed: { // pending mempool portion raw: number; formatted: string; symbol: string; decimals: number; }; }; symbol: string; decimals: number; } ``` EVM `raw` is a *decimal string* (e.g. `"1234000000000000"`), not a bigint — because bigint can't JSON-serialize. Parse with `BigInt(raw)` or use `fromWire()` to get back a bigint. UTXO `raw` is a number (safe since satoshi amounts don't overflow 2⁵³). ## Typical usage ### Display a user's balance ```tsx import { rpc, useOviConnect } from '@oviato/connect/client'; import { useEffect, useState } from 'react'; function BalanceView() { const { session } = useOviConnect(); const [balance, setBalance] = useState(null); useEffect(() => { if (!session) return; rpc.getBalance({ address: session.address }).then((r) => { if (r.result) setBalance(r.result.balance.formatted); }); }, [session]); if (!session) return null; return

{balance ?? '…'} {/* symbol */}

; } ``` ### Pre-check before sending ```ts import { rpc, getSession, fromWire } from '@oviato/connect'; async function canSend(amountWei: bigint) { const session = getSession(); const r = await rpc.getBalance({ address: session!.address }); if (!r.result || r.result.chainType !== 'evm') return false; const available = fromWire(r.result.balance.raw); // string → bigint return available >= amountWei; } ``` ### Narrow by chain type ```ts const r = await rpc.getBalance({ address }); if (!r.result) return; if (r.result.chainType === 'evm') { // r.result.balance is { raw: string, formatted, symbol, decimals } console.log('ETH balance:', r.result.balance.formatted); } else { // r.result.balance has confirmed + unconfirmed console.log('Confirmed BTC:', r.result.balance.confirmed.formatted); console.log('Pending BTC:', r.result.balance.unconfirmed.formatted); } ``` ## How it's fetched - **EVM** — Chainstack RPC `eth_getBalance(address, 'latest')`. Returns wei, SDK formats with chain decimals. - **UTXO** — Our Bitcoin indexer (built on mempool.space). Returns confirmed + unconfirmed separately since the UTXO model distinguishes them on-chain. Both paths go through Oviato's RPC API — no direct Chainstack / indexer calls from your dapp. ## What this isn't `getBalance` is for the *native coin* only. For ERC-20 balances, use [`evm.getTokenBalance`](/evm/reads#token-balance). For multiple tokens in one call, use [`evm.multicall`](/evm/reads#batch-reads-with-multicall). ## Related - [EVM → Token Balance](/evm/reads#token-balance) — ERC-20 balances - [Send Transfers](/chain-agnostic/send-transfers) — once you know the balance, send some --- # Section: EVM # EVM Overview > The evm namespace — reads, writes, popup flow, nonce tracker, envelope pattern. Everything you need to know before writing EVM code with Oviato. Canonical URL: https://docs.oviato.com/evm/overview File: evm/overview.mdx 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. ```ts 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](/getting-started/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](/evm/reads)) — 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](/evm/writes)) — opens popup/iframe, requires passkey. - `sendTransaction` — generic EVM transaction - `writeContract` — ABI-driven contract call (specialized confirmation sheets) - `signTypedData` — EIP-712 structured data signing - `switchChain` — ethers-style alias for `switchNetwork` ## 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. ```ts type SendTransactionResult = | { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } } | { status: 'error'; message: string }; ``` Narrow on `status` before reading `.data`: ```ts 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](/evm/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. ```ts // 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: ```ts 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. ```ts // 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. ```ts 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). ## Where to go next --- # EVM Reads > Read contract state, token balances, NFT metadata, chain state, and fees on EVM networks. No popup, no passkey. Canonical URL: https://docs.oviato.com/evm/reads File: evm/reads.mdx 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. ```ts 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](/evm/overview#bigint-everywhere-for-overflow-safe-math). ## Contract reads ### Read a single value ```ts 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({ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', abi: ERC20_ABI, functionName: 'balanceOf', args: ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6'], }); ``` The generic `` 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. ```ts 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](https://github.com/mds1/multicall) 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 ```ts 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`. ```ts 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. ```ts 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 ```ts // 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://` | Raced across three public IPFS gateways | | `ar://` | Resolved to `arweave.net` | | `https://…` | Direct fetch with 5 s timeout | | `http://…` | **Rejected** (mixed-content security) | ```ts // 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 ```ts 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 ```ts const tx = await evm.getTransaction({ hash: '0x...' }); // Transaction | null const rx = await evm.getTransactionReceipt({ hash: '0x...' }); // TransactionReceipt | null ``` Both 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 ```ts 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 ```ts const code = await evm.getCode({ address }); const isContract = code !== '0x'; const slot0 = await evm.getStorageAt({ address, slot: '0x0' }); ``` ## Fees ### Single values ```ts const gp = await evm.getGasPrice(); // bigint wei const pri = await evm.estimateMaxPriorityFeePerGas(); // bigint wei const units = await evm.estimateGas({ to, data, value }); // bigint gas units ``` ### Combined snapshot For gas-picker UIs, grab everything in one call: ```ts 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 null ``` ### Historical ```ts 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) ```ts const valid = await evm.verifyMessage({ address: session.address, message: 'Hello, Oviato!', signature: '0x...', }); ``` ### EIP-712 (typed data) ```ts 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. ```ts 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 ```ts 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: ```ts 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 `getBalance` is chain-agnostic (works on Bitcoin + EVM) so it lives at the top level. See [Chain-Agnostic → getBalance](/chain-agnostic/get-balance). Yes. The SDK's ABI type (`AbiItem[]`) is structurally compatible with viem's `Abi`. Pass viem ABIs verbatim — `as const` still gives you the strongest TS narrowing. Pass `blockTag` to any read that supports it — `'latest'` | `'pending'` | `'earliest'` | `'safe'` | `'finalized'` | a hex block number. `readContract`, `getCode`, `getStorageAt`, `getTransactionCount` all accept it. Reads throw with a descriptive error. You should retry with exponential backoff client-side or show a "try again" UI — the SDK doesn't retry automatically to avoid amplifying outages. ## Related - [Writes](/evm/writes) — send transactions, call contracts, sign typed data - [Recipes](/evm/recipes) — common flows (approve + transfer, batch balance, NFT mint) - [Troubleshooting](/evm/troubleshooting) — error-keyed lookup --- # EVM Writes > Send transactions, call contracts, sign EIP-712 data, and switch chains on EVM networks. Canonical URL: https://docs.oviato.com/evm/writes File: evm/writes.mdx All EVM writes open a popup or iframe where the user approves with their passkey. Signing happens server-side inside the bridge — your dapp never touches private keys. ```ts import { evm } from '@oviato/connect'; const r = await evm.sendTransaction({ to, value: 10n ** 15n }); // → popup → passkey → broadcast → { hash, network, explorerUrl } ``` New to Oviato writes? [Core Concepts → The write flow](/getting-started/core-concepts#the-write-flow) walks through the full popup → sign → broadcast chain. ## Response envelope Every write returns a discriminated envelope: ```ts type Result = | { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } } | { status: 'error'; message: string }; ``` Always narrow on `status` before reading `.data`: ```ts const r = await evm.sendTransaction({ to, value }); if (r.status === 'success' && r.type === 'sendTransactionSuccess') { console.log(r.data.hash, r.data.explorerUrl); } else if (r.status === 'error') { console.error(r.message); } ``` Prefer the raw-hash style from ethers/viem? See [Migrating from viem](/evm/migrating-from-viem) for a one-line unwrap helper. ## Send ETH / native The simplest write — just move native value from the session address to a recipient. ```ts import { evm } from '@oviato/connect'; const r = await evm.sendTransaction({ to: '0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', value: 10n ** 15n, // 0.001 ETH in wei (bigint) fee: 'auto', }); ``` ### Parameters | Field | Type | Notes | |---|---|---| | `to` | `Address` | Required. | | `value` | `bigint \| string \| number` | Wei amount. Default `0n`. | | `data` | `Hex` | Optional calldata (`0x…`). Default `0x`. | | `gas` | `bigint \| string \| number` | Override gas limit. Usually not needed. | | `maxFeePerGas` | `bigint \| string \| number` | EIP-1559 override. | | `maxPriorityFeePerGas` | `bigint \| string \| number` | EIP-1559 override. | | `gasPrice` | `bigint \| string \| number` | Legacy override (mutually exclusive with 1559). | | `nonce` | `number` | Power-user override — bypasses the tracker. | | `fee` | `'auto' \| 'fast' \| 'medium' \| 'slow'` | Semantic tier. Default `auto` (≈ medium). | | `method` | `'popup' \| 'iframe'` | How the approval UI opens. | `fee: 'auto'` is the right default. Tiers (`fast`/`medium`/`slow`) map to priority-fee multipliers per chain; explicit values (`maxFeePerGas` etc.) override them. Don't set both. ## Transfer a token (ERC-20) `evm.writeContract` takes an ABI + function name + args. The backend encodes the calldata and the bridge renders a specialized "Transfer Token" sheet — no raw hex shown to the user. ```ts const ERC20_ABI = [ { type: 'function', name: 'transfer', inputs: [ { name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }, ], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', }, ] as const; const r = await evm.writeContract({ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum abi: ERC20_ABI, functionName: 'transfer', args: [recipient, 1_000_000n], // 1 USDC (6 decimals) }); ``` `1_000_000n` is 1 USDC because USDC has 6 decimals. Always remember — token amounts are in smallest units (wei-equivalent). Use `parseUnits('1', 6)` if you want to be explicit about the conversion. ### Approve a spender ```ts const ERC20_APPROVE_ABI = [ { type: 'function', name: 'approve', inputs: [ { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, ], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', }, ] as const; // Approve exactly 100 USDC await evm.writeContract({ address: USDC, abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [spender, 100_000_000n], }); // Max uint256 — "infinite" approval (saves gas on repeat use) await evm.writeContract({ address: USDC, abi: ERC20_APPROVE_ABI, functionName: 'approve', args: [spender, 2n ** 256n - 1n], }); ``` The bridge detects this is an ERC-20 approval (via the selector + known-contracts registry) and renders a dedicated "Approve" sheet with clear messaging about unlimited approvals when the value is max-uint256. ## Transfer an NFT (ERC-721) ```ts const ERC721_ABI = [ { type: 'function', name: 'safeTransferFrom', inputs: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'tokenId', type: 'uint256' }, ], outputs: [], stateMutability: 'nonpayable', }, ] as const; await evm.writeContract({ address: nftContract, abi: ERC721_ABI, functionName: 'safeTransferFrom', args: [session.address, recipient, tokenId], }); ``` The bridge fetches the NFT's image via [`evm.getNftMetadata`](/evm/reads#owner-balance-metadata) and renders it in a "Transfer NFT" sheet. If the image can't be resolved, the sheet shows a placeholder — the user still sees the collection name + token ID. ## Call any contract (generic) For contract calls that aren't erc20/erc721 standards — governance, DeFi, custom contracts — `writeContract` takes any ABI + function. ```ts const ROUTER_ABI = [ { type: 'function', name: 'swapExactTokensForTokens', inputs: [ { name: 'amountIn', type: 'uint256' }, { name: 'amountOutMin', type: 'uint256' }, { name: 'path', type: 'address[]' }, { name: 'to', type: 'address' }, { name: 'deadline', type: 'uint256' }, ], outputs: [{ type: 'uint256[]' }], stateMutability: 'nonpayable', }, ] as const; await evm.writeContract({ address: UNISWAP_V2_ROUTER, abi: ROUTER_ABI, functionName: 'swapExactTokensForTokens', args: [amountIn, 0n, [TOKEN_A, TOKEN_B], recipient, BigInt(deadline)], }); ``` The bridge shows a generic "Contract Call" sheet with decoded args. Consider adding your contract to our curated registry (via the dashboard, coming in a later release) so users see a branded sheet instead. ## Sign EIP-712 typed data ```ts const r = await evm.signTypedData({ domain: { name: 'MyApp', version: '1', chainId: 8453, // Base }, types: { Greeting: [ { name: 'content', type: 'string' }, { name: 'from', type: 'address' }, ], }, primaryType: 'Greeting', message: { content: 'Hi from Oviato', from: session.address, }, }); if (r.status === 'success' && r.type === 'signTypedDataSuccess') { console.log('Signature:', r.data.signature); } ``` `types` must NOT include an `EIP712Domain` entry. The SDK computes the domain separator from `domain` directly — adding it manually produces a different hash and signatures won't recover to the expected address. Verify the signature offchain with [`evm.verifyTypedData`](/evm/reads#eip-712-typed-data). ## Switch chains Ethers-style alias for [`switchNetwork`](/chain-agnostic/session-and-network#switch-network). ```ts import { evm } from '@oviato/connect'; import { Network } from '@oviato/connect'; const r = await evm.switchChain(Network.BASEMAINNET); if (r.status === 'success') { console.log('Now on', r.data.network); } ``` Chain switching requires a passkey tap — the server re-issues the session JWT scoped to the new network. ## Wait for confirmations After any write, pass the hash to [`evm.waitForTransactionReceipt`](/evm/reads#waiting-for-receipts) to block until it mines. ```ts const r = await evm.sendTransaction({ to, value }); if (r.status !== 'success') return; const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash }); if (rx?.status === 'success') { console.log('Mined in block', rx.blockNumber); } ``` ## Fee tiers Instead of setting `maxFeePerGas` manually, pass a semantic tier: - `fee: 'fast'` — high priority, 1–2 block inclusion - `fee: 'medium'` — sensible default, 2–5 block inclusion - `fee: 'slow'` — cheapest, may take 5–20 blocks - `fee: 'auto'` — same as `'medium'` The bridge UI's gas picker lets the user override inline — they can still bump to fast if they're in a hurry. ## Rate limits Per `(app, address)` pair: - `sendTransaction` / `writeContract` — 60 prepares/min, 30 broadcasts/min - `signTypedData` — no explicit limit (purely client-side, no chain state) - `switchChain` / `switchNetwork` — 10/min Exceeding any of these returns an error with a `retryAfter` hint. Your normal UI flow should be nowhere near these — they exist to bound runaway scripts. ## Common questions Both produce a signed EVM tx. Use `sendTransaction` when you have pre-encoded calldata (or none — native sends). Use `writeContract` when you have an ABI + function name + args — the bridge can then show a specialized confirmation sheet (Transfer Token, Transfer NFT, Approve). Yes — call `rpc.prepareEvmTx({ to, value, ... })` to get the fully prepared tx (hex, fee tiers, decoded category) without opening a popup. Useful for showing a summary UI before the user commits. You get `{ status: 'error', message: 'User closed popup' }` (or similar). It's non-exceptional — no try/catch needed, just check `status`. Yes, pass `gas:`. The default comes from `eth_estimateGas` on our prep step, which is accurate within ~10% for most calls. Override only if you have specific reason. A previous tx at the same nonce is already in the mempool with higher gas. Either wait for it to clear, or retry with `fee: 'fast'`. The nonce tracker auto-releases your reservation — no manual cleanup needed. See [Troubleshooting](/evm/troubleshooting#replacement-transaction-underpriced). ## Related - [Reads](/evm/reads) — every method that doesn't need a popup - [Recipes](/evm/recipes) — end-to-end flows (approve + transfer, mint, sign permit) - [Troubleshooting](/evm/troubleshooting) — error-keyed lookup - [Migrating from viem](/evm/migrating-from-viem) — side-by-side --- # EVM Recipes > End-to-end copy-paste flows for the common EVM tasks — approve-and-transfer, batch balances, NFT mint, EIP-2612 permit, and more. Canonical URL: https://docs.oviato.com/evm/recipes File: evm/recipes.mdx Each recipe below is complete — imports, session setup, error handling, explorer URL. Paste, swap the addresses, and ship. All recipes use `@oviato/connect` (React). Swap for `@oviato/sdk` if you're using a different framework — the function signatures are identical. ## Approve-and-transfer (ERC-20) Check allowance, approve if needed, then transfer through a contract (e.g. a DEX router). ```ts import { evm } from '@oviato/connect'; import { parseUnits } from '@oviato/connect'; const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; const ROUTER = '0x...'; const amount = parseUnits('100', 6); // 100 USDC const ERC20_ABI = [ { type: 'function', name: 'approve', inputs: [{ name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }], outputs: [{ type: 'bool' }], stateMutability: 'nonpayable', }, ] as const; async function approveAndTransfer(session: { address: `0x${string}` }) { // 1. Check current allowance const { raw: current } = await evm.getTokenAllowance({ tokenAddress: USDC, owner: session.address, spender: ROUTER, }); // 2. Approve if short if (current < amount) { const approve = await evm.writeContract({ address: USDC, abi: ERC20_ABI, functionName: 'approve', args: [ROUTER, amount], }); if (approve.status !== 'success') { throw new Error(approve.status === 'error' ? approve.message : 'Approve failed'); } // Wait for approval to land before using it await evm.waitForTransactionReceipt({ hash: approve.data.hash }); } // 3. Now call the router // ... evm.writeContract({ address: ROUTER, ... }) } ``` Always `waitForTransactionReceipt` on the approve before using the allowance. A pending approve tx doesn't count — the router will still see the old allowance until it mines. ## Dashboard-style balance fetch (multicall) Fetch balances for multiple tokens in one round-trip. ```ts import { evm, formatUnits } from '@oviato/connect'; const ERC20_BALANCE_OF_ABI = [ { type: 'function', name: 'balanceOf', inputs: [{ name: 'account', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view', }, ] as const; async function getMultiBalance(owner: `0x${string}`) { // Get the curated token list const { tokens } = await evm.getTokens(); // Batch their balances in one call const results = await evm.multicall({ contracts: tokens.map((t) => ({ address: t.address, abi: ERC20_BALANCE_OF_ABI, functionName: 'balanceOf', args: [owner], })), }); return tokens.map((t, i) => { const r = results[i]; if (r.status !== 'success') { return { ...t, balance: '0', raw: 0n, error: r.error }; } const raw = r.result as bigint; return { ...t, raw, balance: formatUnits(raw, t.decimals), }; }); } // Output: [{ symbol: 'USDC', address, balance: '1234.56', raw: 1234560000n, ... }, ...] ``` ## Mint an NFT (no-args mint) Many test/community NFTs expose a parameterless `mintNft()` function. ```ts const MINTABLE_NFT_ABI = [ { type: 'function', name: 'mintNft', inputs: [], outputs: [], stateMutability: 'nonpayable' }, ] as const; async function mintNft(contract: `0x${string}`) { const r = await evm.writeContract({ address: contract, abi: MINTABLE_NFT_ABI, functionName: 'mintNft', args: [], }); if (r.status !== 'success') { throw new Error(r.status === 'error' ? r.message : 'Mint failed'); } const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash }); // Your newly-minted token id often comes from a Transfer event in the receipt. // For simple contracts, the token id is 1 + totalSupply before mint. return { hash: r.data.hash, receipt: rx }; } ``` ## Sign an EIP-2612 Permit (gasless approval) Permit lets a user approve a spender without an on-chain transaction — the signature is presented later by the spender alongside `permit()` + the intended action. USDC on Ethereum supports this natively. ```ts import { evm } from '@oviato/connect'; async function signPermit(session: { address: `0x${string}` }) { const chainId = 1; // mainnet const deadline = Math.floor(Date.now() / 1000) + 3600; // +1h // Read the current nonce from the token (USDC permit nonces are token-side, not eth nonce) const USDC_PERMIT_ABI = [ { type: 'function', name: 'nonces', inputs: [{ name: 'owner', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view', }, ] as const; const nonce = await evm.readContract({ address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', abi: USDC_PERMIT_ABI, functionName: 'nonces', args: [session.address], }); const r = await evm.signTypedData({ domain: { name: 'USD Coin', version: '2', chainId, verifyingContract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', }, types: { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }, primaryType: 'Permit', message: { owner: session.address, spender: '0x...', value: 100_000_000n, // 100 USDC nonce, deadline: BigInt(deadline), }, }); if (r.status !== 'success') return null; // Split the signature for the permit() call — r, s, v const sig = r.data.signature as `0x${string}`; return { deadline, v: parseInt(sig.slice(130, 132), 16), r: `0x${sig.slice(2, 66)}`, s: `0x${sig.slice(66, 130)}`, }; } ``` ## Check owner + metadata + transfer an NFT ```ts async function inspectAndTransfer( contract: `0x${string}`, tokenId: bigint, newOwner: `0x${string}`, session: { address: `0x${string}` }, ) { // 1. Confirm the session owns this NFT const { owner } = await evm.getNftOwner({ contractAddress: contract, tokenId }); if (owner.toLowerCase() !== session.address.toLowerCase()) { throw new Error(`You don't own token ${tokenId}`); } // 2. Pull metadata for the confirmation UI (bridge does this automatically too) const meta = await evm.getNftMetadata({ contractAddress: contract, tokenId }); console.log(`Transferring ${meta.metadata?.name ?? `#${tokenId}`}`); // 3. Transfer const ERC721_ABI = [ { type: 'function', name: 'safeTransferFrom', inputs: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'tokenId', type: 'uint256' }, ], outputs: [], stateMutability: 'nonpayable', }, ] as const; const r = await evm.writeContract({ address: contract, abi: ERC721_ABI, functionName: 'safeTransferFrom', args: [session.address, newOwner, tokenId], }); if (r.status !== 'success') throw new Error('Transfer failed'); return r.data; } ``` ## Pay with USDC via x402 (gasless) Oviato supports the [x402 payment protocol](/x402/overview) — an HTTP-level payment flow for gated resources. The SDK signs an ERC-3009 authorization, attaches it as an HTTP header, and your server validates. ```ts import { x402 } from '@oviato/connect'; const r = await x402.pay('https://api.example.com/premium-data'); if (r.paid) { const data = await r.response.json(); console.log(data); } else if (r.error) { console.error('Payment failed:', r.error); } ``` See [x402 Payments](/x402/overview) for the full flow, including facilitator setup. ## Fee-aware transaction Let the user pick a speed tier before signing. ```ts import { evm, formatUnits } from '@oviato/connect'; async function previewAndSend(to: `0x${string}`, value: bigint) { // 1. Get current fees const fees = await evm.estimateFeesPerGas(); const gasUnits = await evm.estimateGas({ to, value }); const costWei = gasUnits * fees.maxFeePerGas; const costEth = formatUnits(costWei, 18); // 2. Show the user the cost before proceeding if (!confirm(`Send ${formatUnits(value, 18)} ETH? (gas ≈ ${costEth} ETH)`)) { return null; } // 3. Send — the bridge will ultimately recompute fees, this preview is UI-only const r = await evm.sendTransaction({ to, value, fee: 'medium' }); return r; } ``` ## More - [Writes reference](/evm/writes) — full parameter details - [Troubleshooting](/evm/troubleshooting) — error-keyed lookup - [Migrating from viem](/evm/migrating-from-viem) — side-by-side --- # EVM Troubleshooting > Common errors you'll see when sending EVM transactions, and what to do about each. Canonical URL: https://docs.oviato.com/evm/troubleshooting File: evm/troubleshooting.mdx This page is keyed by the error message you see. Search for the phrase or scan the headings. ## Replacement transaction underpriced **What happened.** A previous transaction at the same nonce is pending in the mempool with a higher gas price. Your new tx can't replace it without ≥10% higher gas. **What to do.** Either wait for the pending tx to clear (usually seconds-to-minutes), or retry with `fee: 'fast'` to bump the gas. ```ts const r = await evm.sendTransaction({ to, value, fee: 'fast' }); ``` The nonce tracker automatically releases your reservation on this error, so your next send picks a fresh nonce. No manual cleanup needed. ## Nonce too low **What happened.** The nonce you submitted has already been used on-chain. This usually means: - The server-side tracker drifted from the actual chain state. - You passed a manual `nonce:` override that's stale. **What to do.** Just retry. The tracker auto-resyncs on the next `prepareEvm` call (reads `eth_getTransactionCount('pending')` from Chainstack). No code change needed on your side. ## Insufficient funds for this transaction **What happened.** The session address doesn't have enough native token to cover `value + (gas × maxFeePerGas)`. **What to do.** 1. Check the balance: `await rpc.getBalance({ address: session.address })`. 2. Check the gas cost: `await evm.estimateFeesPerGas()` + `evm.estimateGas()`. 3. Top up the wallet, or lower `value`, or switch to `fee: 'slow'`. On testnets, grab faucet ETH from [Sepolia faucet](https://faucet.sepolia.dev) or [Base Sepolia faucet](https://portal.cdp.coinbase.com/products/faucet). ## Missing revert data **What happened.** An `eth_call` reverted without a readable reason — almost always because a precondition in the contract failed silently. **Common causes:** - **ERC-20 transfer with 0 balance** — you're trying to move tokens you don't have. The `transfer` reverts because `balanceOf(sender) < amount`. - **Uninitialized contract** — the address has no code. - **Wrong function signature** — your ABI doesn't match what's on-chain. **What to do.** Read the state first. ```ts // Before transferring: const { raw } = await evm.getTokenBalance({ tokenAddress }); if (raw < amount) throw new Error('Insufficient token balance'); // Verify the contract exists: const code = await evm.getCode({ address }); if (code === '0x') throw new Error('Not a contract'); ``` ## Rate limit exceeded **What happened.** Your app hit the per-(app, address) rate limit: 60 prepares/min, 30 broadcasts/min on EVM writes. **What to do.** - For normal UI flows, you should never hit this. Investigate why you're calling so often. - If you genuinely need higher limits (automated system, batch job), contact us via the [Developer Dashboard](https://dashboard.oviato.com). - The error includes a `retryAfter` hint (seconds). Exponential-backoff on the retry. ## Popup blocked by the browser **What happened.** Your browser blocked the popup because `sendTransaction` was called outside a user gesture. **What to do.** Always initiate writes from a direct click handler — not in a `useEffect`, not in a setTimeout, not after an async gap. ```ts // ✅ Good: initiated from a click // ❌ Bad: initiated from an async completion useEffect(() => { fetchSomething().then(() => evm.sendTransaction({ to, value })); }, []); ``` If you genuinely need to trigger a write outside a click context (uncommon), use `method: 'iframe'` — iframes aren't subject to popup blockers. ## User closed popup **What happened.** The user dismissed the popup without approving. **What to do.** Not an exception — it's a normal error result. Just check `r.status`: ```ts const r = await evm.sendTransaction({ to, value }); if (r.status === 'error') { // User cancelled, or another error. Handle gracefully. console.log('Cancelled or failed:', r.message); } ``` Don't wrap this in try/catch — nothing is thrown. ## Invalid nonce. Please try again. **What happened.** The bridge received a response with a nonce that doesn't match the one it issued. Usually means the user opened multiple popups, or a race between popup and iframe modes. **What to do.** Retry once. If it persists, check that you're not calling `sendTransaction` from multiple places simultaneously. ## Chain mismatch (JWT network claim vs request) **What happened.** You called an EVM method while the session is on a UTXO chain (or vice versa). The JWT's `network` claim doesn't match what the method needs. **What to do.** Switch the session first. ```ts import { switchNetwork, Network, getSession, getNetworkType, NetworkType } from '@oviato/connect'; const session = getSession(); if (session && getNetworkType(session.network) !== NetworkType.EVM) { await switchNetwork(Network.BASEMAINNET); // or whatever EVM chain } await evm.sendTransaction({ to, value }); ``` ## Not connected: Please login first. **What happened.** The SDK checked for a session before opening a write popup, and there isn't one. **What to do.** Require the user to log in first. ```tsx import { useOviConnect, ConnectButton } from '@oviato/connect/client'; function SendButton() { const { session } = useOviConnect(); if (!session) return ; return ; } ``` ## SDK not initialized **What happened.** You called an SDK method before `initialize()` or before the `` mounted. **What to do.** Make sure `initialize()` runs before any other SDK call. In React, `` handles this — just make sure you wrapped your app. ## Still stuck? - Check the [Developer Dashboard](https://dashboard.oviato.com) activity logs — every prepare/broadcast event with error context. - Report an issue at [github.com/oviato-hq](https://github.com). - Ping the team in the dashboard's support panel. --- # Migrating from viem > Method-by-method mapping from viem clients to Oviato's evm.* namespace, plus adapters for the response-envelope pattern. Canonical URL: https://docs.oviato.com/evm/migrating-from-viem File: evm/migrating-from-viem.mdx If you're comfortable with [viem](https://viem.sh), you'll find Oviato's EVM surface shaped almost identically — method names, parameter keys, and return shapes all match the viem convention where possible. ## The one-minute map | viem | Oviato | Notes | |---|---|---| | `publicClient.readContract` | `evm.readContract` | identical params | | `publicClient.multicall` | `evm.multicall` | identical — Multicall3 default | | `publicClient.getBalance` | `getBalance` (top level) | chain-agnostic return | | `publicClient.getBlockNumber` | `evm.getBlockNumber` | | | `publicClient.getBlock` | `evm.getBlock` | | | `publicClient.getTransaction` | `evm.getTransaction` | | | `publicClient.getTransactionReceipt` | `evm.getTransactionReceipt` | | | `publicClient.getTransactionCount` | `evm.getTransactionCount` | `pending` is tracker-adjusted | | `publicClient.getCode` | `evm.getCode` | | | `publicClient.getStorageAt` | `evm.getStorageAt` | | | `publicClient.getGasPrice` | `evm.getGasPrice` | | | `publicClient.estimateGas` | `evm.estimateGas` | | | `publicClient.estimateFeesPerGas` | `evm.estimateFeesPerGas` | | | `publicClient.estimateMaxPriorityFeePerGas` | `evm.estimateMaxPriorityFeePerGas` | | | `publicClient.getFeeHistory` | `evm.getFeeHistory` | | | `publicClient.waitForTransactionReceipt` | `evm.waitForTransactionReceipt` | | | `publicClient.verifyMessage` | `evm.verifyMessage` | | | `publicClient.verifyTypedData` | `evm.verifyTypedData` | | | `walletClient.sendTransaction` | `evm.sendTransaction` | **returns envelope, not hash** | | `walletClient.writeContract` | `evm.writeContract` | **returns envelope, not hash** | | `walletClient.signTypedData` | `evm.signTypedData` | **returns envelope, not signature** | | `walletClient.signMessage` | `signMessage` (top level) | chain-agnostic | | `walletClient.switchChain` | `evm.switchChain` | also exported as `switchNetwork` | | `walletClient.watchAsset` | — | not implemented; use `evm.getTokens` | | `publicClient.simulateContract` | — | v2 roadmap | | `publicClient.watchBlocks` | — | v2 roadmap | | `publicClient.watchEvent` | — | v2 roadmap | ## Clients vs namespace viem: ```ts const publicClient = createPublicClient({ chain: mainnet, transport: http() }); const walletClient = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) }); const balance = await publicClient.getBalance({ address }); const hash = await walletClient.sendTransaction({ to, value }); ``` Oviato: ```ts import { evm } from '@oviato/connect'; // No client to create — the namespace IS the client. Chain comes from the user's session. const balance = await getBalance({ address }); // chain-agnostic const r = await evm.sendTransaction({ to, value }); ``` Oviato's chain is controlled by the user (via `switchNetwork`), not by the developer. You don't pass `chain:` on every call — the session already knows. This is deliberate: multi-chain UX without the dev having to juggle configs. ## The envelope adapter The biggest DX difference: Oviato returns a discriminated envelope, viem returns a raw value and throws on error. ```ts try { const hash = await walletClient.sendTransaction({ to, value }); console.log('Hash:', hash); } catch (err) { console.error('Failed:', err); } ``` ```ts const r = await evm.sendTransaction({ to, value }); if (r.status === 'success' && r.type === 'sendTransactionSuccess') { console.log('Hash:', r.data.hash); } else if (r.status === 'error') { console.error('Failed:', r.message); } ``` ```ts // One-line adapter — drop-in replacement for viem-style code async function unwrap(p: Promise) { const r = await p; if (r.status === 'success') return r.data; throw new Error(r.message ?? 'Unknown error'); } const { hash } = await unwrap(evm.sendTransaction({ to, value })); ``` We keep the envelope by default because it makes "user cancelled popup" a first-class state instead of an exception — far easier to handle in UI code. Use the adapter if your codebase assumes the viem style. ## ABI compatibility viem's `Abi` type and ours are structurally compatible. Pass viem ABIs verbatim: ```ts import { erc20Abi } from 'viem'; import { evm } from '@oviato/connect'; const balance = await evm.readContract({ address: USDC, abi: erc20Abi, functionName: 'balanceOf', args: [address], }); ``` ## Address + Hex types Identical: ```ts // viem import type { Address, Hex } from 'viem'; // Oviato import type { Address, Hex } from '@oviato/connect'; ``` Both resolve to the TypeScript template literal type for 0x-prefixed hex strings. ## bigint conventions Identical — wei is always `bigint`, overflow-safe. ```ts const value = 10n ** 18n; // 1 ETH // parseUnits / formatUnits match viem's semantics exactly import { parseUnits, formatUnits } from '@oviato/connect'; parseUnits('1.5', 18); // 1500000000000000000n formatUnits(1_234_500n, 6); // '1.2345' ``` ## Chains viem has explicit chain objects (`mainnet`, `base`, `sepolia`). Oviato uses string slugs on the session. | viem chain | Oviato Network | |---|---| | `mainnet` | `Network.ETHMAINNET` (`'eth'`) | | `sepolia` | `Network.ETHSEPOLIA` (`'ethsepolia'`) | | `base` | `Network.BASEMAINNET` (`'base'`) | | `baseSepolia` | `Network.BASESEPOLIA` (`'basesepolia'`) | You don't pass the chain on each call — it's implicit from the session. Switch via `switchNetwork(Network.X)`. ## What Oviato adds over viem - **Account custody.** Oviato manages keys via WebAuthn passkeys — no private-key juggling, no seed phrase UX. - **Server-side nonce tracker.** Rapid-fire sends just work; no "pending count lag" race. - **Specialized confirmation sheets.** `writeContract` with an ERC-20 ABI shows a "Transfer Token" sheet, not raw calldata. - **Bridge UI.** The popup/iframe handles approval — you don't ship wallet UI. - **Multi-chain session.** Same API on Bitcoin + EVM via `sendTransfer` / `signMessage` / `getBalance`. - **Curated token registry.** `evm.getTokens()` returns canonical ERC-20s per chain, server-maintained. ## What Oviato doesn't do (yet) - **`simulateContract`** — v2. For now, call `readContract` with the same ABI to preview. - **`watchBlocks` / `watchEvent` / subscriptions** — v2. Poll via `getBlockNumber` + `getLogs` if you need it now. - **Local ABI encoding.** The SDK ships ABIs to the server for encoding. If you need pure-client encoding, wrap viem's `encodeFunctionData` and pass the result as `data:` to `sendTransaction`. - **Custom chains not in our supported list.** See [EVM Overview → Supported networks](/evm/overview#supported-evm-networks). ## Side-by-side: a typical swap **viem** ```ts import { createWalletClient, http, parseEther } from 'viem'; import { mainnet } from 'viem/chains'; const walletClient = createWalletClient({ chain: mainnet, transport: custom(window.ethereum!), }); const hash = await walletClient.sendTransaction({ to: '0x742d35Cc...', value: parseEther('0.01'), }); const rx = await publicClient.waitForTransactionReceipt({ hash }); ``` **Oviato** ```ts import { evm, parseUnits } from '@oviato/connect'; const r = await evm.sendTransaction({ to: '0x742d35Cc...', value: parseUnits('0.01', 18), }); if (r.status !== 'success') return; const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash }); ``` No client to set up, no wallet to connect — the session handles it. ## Related - [EVM Overview](/evm/overview) — the mental model - [Migrating from ethers](/evm/migrating-from-ethers) — if you're coming from ethers v5/v6 instead --- # Migrating from ethers > Method-by-method mapping from ethers v5/v6 to Oviato's evm.* namespace, with adapters for the response-envelope pattern. Canonical URL: https://docs.oviato.com/evm/migrating-from-ethers File: evm/migrating-from-ethers.mdx Oviato borrows viem's naming for most methods. If you're on ethers, the mapping is still straightforward — every ethers method has a counterpart, and we ship aliases where the names differ. ## Quick map | ethers v5 / v6 | Oviato | Notes | |---|---|---| | `provider.getBlockNumber()` | `evm.getBlockNumber()` | bigint return | | `provider.getBlock(n)` | `evm.getBlock({ blockNumber: n })` | key-arg signature | | `provider.getTransaction(hash)` | `evm.getTransaction({ hash })` | | | `provider.getTransactionReceipt(hash)` | `evm.getTransactionReceipt({ hash })` | | | `provider.getTransactionCount(addr)` | `evm.getTransactionCount({ address })` | | | `provider.getCode(addr)` | `evm.getCode({ address })` | | | `provider.getStorageAt(addr, slot)` | `evm.getStorageAt({ address, slot })` | | | `provider.getGasPrice()` | `evm.getGasPrice()` | | | `provider.estimateGas(tx)` | `evm.estimateGas(tx)` | | | `provider.getFeeData()` | `evm.getFeeData()` | alias of `estimateFeesPerGas` | | `provider.getBalance(addr)` | `getBalance({ address })` | chain-agnostic, top-level | | `provider.waitForTransaction(h, c, t)` | `evm.waitForTransaction(h, c, t)` | positional alias | | `contract.(args)` (read) | `evm.readContract({ abi, functionName, args })` | explicit form | | `contract.(args)` (write) | `evm.writeContract({ abi, functionName, args })` | returns envelope | | `Multicall contract.aggregate(...)` | `evm.multicall({ contracts })` | Multicall3 default | | `signer.sendTransaction(tx)` | `evm.sendTransaction(tx)` | returns envelope | | `signer.signMessage(msg)` | `signMessage(msg)` | chain-agnostic, top-level | | `signer.signTypedData(d, t, v)` | `evm.signTypedData({ domain, types, primaryType, message })` | params bundled | | `verifyMessage(msg, sig)` | `evm.verifyMessage({ address, message, signature })` | explicit address | | `verifyTypedData(d, t, v, sig)` | `evm.verifyTypedData({ address, domain, types, primaryType, message, signature })` | | | `formatUnits`, `parseUnits` | `formatUnits`, `parseUnits` (top-level) | identical semantics | | `formatEther`, `parseEther` | use `formatUnits(x, 18)` / `parseUnits(x, 18)` | no dedicated export | ## Provider / Signer — gone Ethers is built around a `Provider` (reads) + `Signer` (writes) model. Oviato doesn't expose either — the session *is* the signer, and reads go through our backend RPC. **ethers v6** ```ts import { BrowserProvider, parseEther } from 'ethers'; const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); const balance = await provider.getBalance(address); const tx = await signer.sendTransaction({ to, value: parseEther('0.01') }); await tx.wait(); ``` **Oviato** ```ts import { evm, getBalance, parseUnits } from '@oviato/connect'; const { balance } = await getBalance({ address }); const r = await evm.sendTransaction({ to, value: parseUnits('0.01', 18) }); if (r.status === 'success') { await evm.waitForTransactionReceipt({ hash: r.data.hash }); } ``` ## Contract instance → explicit ABI calls Ethers' `Contract` class wraps a contract with ABI-derived methods. Oviato's equivalent is passing the ABI to `readContract` / `writeContract` explicitly. **ethers** ```ts const usdc = new Contract(USDC_ADDRESS, ERC20_ABI, signer); const bal = await usdc.balanceOf(address); await usdc.transfer(to, amount); ``` **Oviato** ```ts const bal = await evm.readContract({ address: USDC_ADDRESS, abi: ERC20_ABI, functionName: 'balanceOf', args: [address], }); await evm.writeContract({ address: USDC_ADDRESS, abi: ERC20_ABI, functionName: 'transfer', args: [to, amount], }); ``` A touch more verbose per call, but it keeps the ABI at the call site — easier to audit + tree-shake. If you miss the `contract.fn()` style, wrap it yourself: ```ts function tokenContract(address: `0x${string}`, abi: readonly AbiItem[]) { return new Proxy({}, { get(_, fn: string) { return (...args: unknown[]) => evm.readContract({ address, abi, functionName: fn, args }); }, }); } const usdc = tokenContract(USDC_ADDRESS, ERC20_ABI); const bal = await usdc.balanceOf(address); // works ``` (Reads only — for writes you'd branch to `evm.writeContract`. Not recommended for production.) ## The envelope adapter Ethers methods throw on failure and return raw values. Oviato returns a discriminated envelope. Drop-in adapter: ```ts async function unwrap(p: Promise) { const r = await p; if (r.status === 'success') return r.data; throw new Error(r.message ?? 'Unknown error'); } // Now looks like ethers const { hash } = await unwrap(evm.sendTransaction({ to, value })); ``` ## BigNumber → bigint Ethers v5 used `BigNumber` (custom class). Ethers v6 moved to native `bigint`. Oviato is bigint-native — no class wrappers, no `.toBigInt()` calls. If you're on v5, the conversion is: ```ts // v5 const b = BigNumber.from('1000000'); b.add(500).toBigInt(); // Oviato const b = 1_000_000n; b + 500n; ``` ## Signatures: split vs joined Ethers v5 had both split (`{ r, s, v }`) and joined (`string`) signatures depending on the API. Oviato returns joined hex strings (`0x` + 130 bytes) — matching v6 + viem. ```ts // Oviato const r = await evm.signTypedData({ domain, types, primaryType, message }); const sig = r.data.signature; // '0x...' (130 bytes) // If you need split (for a permit() call) const v = parseInt(sig.slice(130, 132), 16); const r_ = `0x${sig.slice(2, 66)}`; const s_ = `0x${sig.slice(66, 130)}`; ``` ## `signTypedData`: domain/types/message bundle Ethers passes domain, types, and value (ethers v5) or message (ethers v6) as three positional args. Oviato bundles them: **ethers v6** ```ts const sig = await signer.signTypedData(domain, types, message); ``` **Oviato** ```ts const r = await evm.signTypedData({ domain, types, primaryType: 'YourType', message, }); ``` Oviato requires `primaryType` explicitly. Ethers infers it from the single top-level type in `types`. If your ethers code relied on that inference, declare the primary type directly. Also: don't include `EIP712Domain` in `types`. Oviato computes the domain hash from the `domain` object directly. Adding it manually changes the hash. ## Chain switching **ethers** doesn't have a standard chain-switch API — you'd call `window.ethereum.request({ method: 'wallet_switchEthereumChain' })`. **Oviato** ```ts import { switchNetwork, Network } from '@oviato/connect'; await switchNetwork(Network.BASEMAINNET); // Also available as evm.switchChain(...) for familiarity await evm.switchChain(Network.BASEMAINNET); ``` Switching requires a passkey tap (server re-issues the JWT for the new chain). Different from ethers' UX where switching is silent. ## What's different by design - **No custom Provider/Signer objects.** The session is implicit global state. You don't thread a provider through your code. - **No local key management.** Oviato holds keys via WebAuthn passkeys. `sendTransaction` triggers a bridge popup, not a local signer. - **No ABI-level type inference.** Ethers v6 types `contract.fn()` return types from the ABI. Oviato's `readContract` asks you to declare `` explicitly — the ABI travels to the server for encoding. - **Server-side nonce tracker.** You can't override the nonce by default — the tracker handles it. Pass `nonce:` explicitly if you really need to. ## What Oviato adds over ethers Same list as for viem — passkey custody, server-side nonce tracking, specialized bridge UI, multi-chain session, curated token registry. ## Side-by-side: a typical tx flow **ethers v6** ```ts import { BrowserProvider, parseEther } from 'ethers'; const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); try { const tx = await signer.sendTransaction({ to: '0x742d35Cc...', value: parseEther('0.01'), }); const rx = await tx.wait(); console.log('Mined in block', rx?.blockNumber); } catch (err) { console.error('Tx failed', err); } ``` **Oviato** ```ts import { evm, parseUnits } from '@oviato/connect'; const r = await evm.sendTransaction({ to: '0x742d35Cc...', value: parseUnits('0.01', 18), }); if (r.status === 'success') { const rx = await evm.waitForTransactionReceipt({ hash: r.data.hash }); console.log('Mined in block', rx?.blockNumber); } else { console.error('Tx failed', r.message); } ``` ## Related - [EVM Overview](/evm/overview) - [Migrating from viem](/evm/migrating-from-viem) --- # Section: UTXO # UTXO Overview > Oviato's UTXO surface — PSBT signing for Bitcoin, scoped under utxo.*. Everything else lives at the top level (sendTransfer, signMessage, getBalance). Canonical URL: https://docs.oviato.com/utxo/overview File: utxo/overview.mdx The `utxo` namespace is narrow on purpose — most UTXO-chain work is either chain-agnostic (covered at the top level) or a PSBT flow. ```ts import { utxo } from '@oviato/connect'; const r = await utxo.signPsbt({ psbt: '70736274ff...', // PSBT hex broadcast: true, }); ``` ## What's in the namespace | Method | Purpose | |---|---| | `utxo.signPsbt` | Sign a Partially-Signed Bitcoin Transaction (PSBT), optionally broadcast after signing. | That's it for v1. More UTXO-specific surface area arrives with future releases. ## What's NOT in the namespace Most of the UTXO workflow lives at the top level because it's chain-agnostic: | Task | Use | |---|---| | Send BTC to an address | [`sendTransfer`](/chain-agnostic/send-transfers) | | Multi-recipient send | [`sendTransfer`](/chain-agnostic/send-transfers) with `recipients: [...]` | | Sign a message | [`signMessage`](/chain-agnostic/sign-messages) | | Fetch balance (confirmed + unconfirmed) | [`rpc.getBalance`](/chain-agnostic/get-balance) | | Switch to Bitcoin | [`switchNetwork`](/chain-agnostic/session-and-network) | Use `utxo.*` specifically when you have a **PSBT** — which happens when: - Coin-selection or output construction runs server-side (your backend builds, frontend signs) - You're integrating with a protocol that hands you a PSBT to co-sign - You need explicit control over inputs, outputs, and sighash flags ## PSBT — the quick explainer A [PSBT](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki) (Partially-Signed Bitcoin Transaction) is Bitcoin's native format for transactions that need external logic or multiple signatures before broadcast. Key ideas: - **It's data, not code.** A PSBT is just a hex string you can pass around. - **It's partial.** Inputs can be unsigned, partially signed (multi-sig), or fully signed. - **It's verifiable.** Anyone can inspect the inputs + outputs before signing — no hidden state. - **It's broadcastable.** Once fully signed, it's serialized into a standard transaction + broadcast. Oviato's `utxo.signPsbt` signs the inputs your session owns (leaving others untouched), and optionally broadcasts after signing. ## When to reach for PSBTs - **Server-side coin selection.** Your backend selects UTXOs and constructs the tx; the frontend just signs. - **Batching with custom outputs.** `sendTransfer` covers multi-recipient; PSBTs give you more control (change address placement, OP_RETURN content, specific sighash flags). - **Pre-built transactions from a third party.** A counterparty hands you a PSBT for you to add your signature to. If you don't need any of the above — just `sendTransfer`. It's simpler. ## Supported UTXO networks - `Network.BTCMAINNET` (`'bitcoin'`) - `Network.BTCTESTNET` (`'bitcointestnet'`) More UTXO chains (Dogecoin, Litecoin) are on the roadmap. ## Session scope A Bitcoin session can't use `evm.*` methods (they throw). Use `switchNetwork(Network.BTCMAINNET)` to scope the session to Bitcoin before calling `utxo.*`. ## Explore --- # Sign PSBT > Sign a Partially-Signed Bitcoin Transaction with the user's passkey. Supports selective input signing, sighash flags, and optional broadcast. Canonical URL: https://docs.oviato.com/utxo/sign-psbt File: utxo/sign-psbt.mdx `utxo.signPsbt` signs the inputs your session owns in a PSBT, leaving all other inputs untouched. Optionally broadcasts the fully-signed result. ```ts import { utxo } from '@oviato/connect'; const r = await utxo.signPsbt({ psbt: '70736274ff01007d...', // PSBT as hex broadcast: true, }); if (r.status === 'success') { if (r.type === 'signPsbtBroadcastSuccess') { console.log('TxID:', r.data.txid); } else if (r.type === 'signPsbtSuccess') { console.log('Signed PSBT:', r.data.psbt); } } ``` ## Parameters | Field | Type | Notes | |---|---|---| | `psbt` | `string` | PSBT as **hex** (not base64). Required. | | `signInputs` | `SignPsbtInput[]?` | If omitted, every owned input is signed. | | `broadcast` | `boolean?` | If `true`, broadcasts after signing. Default `false`. | | `method` | `'popup' \| 'iframe'?` | How the approval UI opens. | ### `SignPsbtInput` ```ts type SignPsbtInput = { index: number; // Which input to sign (0-based) address: string; // The address that owns that input — used for key derivation sighashTypes?: number[]; // Optional SIGHASH flags (default: SIGHASH_ALL) }; ``` ## Response shape Discriminated by whether you asked to broadcast. ```ts type Result = | { status: 'success'; type: 'signPsbtSuccess'; data: { psbt: string } } | { status: 'error'; message: string }; ``` Use this when a different party (a marketplace, aggregator, second cosigner) will broadcast after you sign. ```ts type Result = | { status: 'success'; type: 'signPsbtBroadcastSuccess'; data: { txid: string; explorerUrl?: string } } | { status: 'error'; message: string }; ``` Use this for straightforward "sign + send" flows where you built the PSBT yourself. ## Common flows ### Sign all owned inputs + broadcast The simplest case. `psbt` in, `txid` out. ```ts const r = await utxo.signPsbt({ psbt: rawPsbtHex, broadcast: true, }); if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') { window.open(r.data.explorerUrl!, '_blank'); } ``` ### Sign without broadcasting Useful for marketplace flows — you sign your half, the counterparty signs + broadcasts. ```ts const r = await utxo.signPsbt({ psbt: marketplaceBuiltPsbt, broadcast: false, }); if (r.status === 'success' && r.type === 'signPsbtSuccess') { await fetch('/api/marketplace/complete', { method: 'POST', body: JSON.stringify({ signedPsbt: r.data.psbt }), }); } ``` ### Sign specific inputs only If the PSBT has inputs owned by multiple addresses and you want to sign only some: ```ts import { SigHashTypes } from '@oviato/connect'; const r = await utxo.signPsbt({ psbt: rawPsbtHex, signInputs: [ { index: 0, address: session.address }, { index: 2, address: session.address, sighashTypes: [SigHashTypes.SINGLE | SigHashTypes.ANYONECANPAY] }, ], broadcast: false, }); ``` Default sighash is `SIGHASH_ALL` — signs over all inputs + outputs. Use `SIGHASH_SINGLE | SIGHASH_ANYONECANPAY` for marketplace flows where your signature covers only your input and its matching output. ## Getting a PSBT PSBTs usually come from one of two places: 1. **Your own backend.** You do coin-selection + construct the transaction server-side and pass the PSBT hex to the frontend for signing. 2. **A counterparty or protocol.** A different party builds the PSBT and hands it to you to add your signature. Either way, the PSBT arrives as a hex string that you pass directly to `utxo.signPsbt`. ## Validation before signing The bridge UI decodes the PSBT before showing the user, flagging: - Which inputs they own (green), which they don't (grey) - Which outputs go to them (change), which go elsewhere - Estimated network fee - Any unusual characteristics (OP_RETURN, large opcodes, etc.) The user can always see the full decoded tx before approving. ## Related - [Send Transfers](/chain-agnostic/send-transfers) — simpler native BTC sends - [Recipes](/utxo/recipes) — common UTXO flows --- # UTXO Recipes > Common Bitcoin flows — multi-recipient send, balance prechecks, confirmation polling. Canonical URL: https://docs.oviato.com/utxo/recipes File: utxo/recipes.mdx Copy-paste flows for the Bitcoin tasks most apps need. ## Multi-recipient payout Send to N addresses in one transaction. Saves on fees vs N separate txs, since Bitcoin supports multiple outputs natively. ```ts import { sendTransfer } from '@oviato/connect'; const r = await sendTransfer({ recipients: [ { address: 'bc1q...', amount: 10000 }, { address: 'bc1p...', amount: 5000 }, { address: 'bc1q...', amount: 7500 }, ], fee: 'auto', memo: 'Weekly payouts', }); if (r.status === 'success' && r.type === 'sendTransferSuccess') { console.log('Batch txid:', r.data.txid); } ``` ## Balance precheck before a send ```ts import { rpc, sendTransfer, getSession } from '@oviato/connect'; async function sendWithPrecheck(to: string, amount: number) { const session = getSession()!; const { result } = await rpc.getBalance({ address: session.address }); if (!result || result.chainType !== 'utxo') throw new Error('Not a UTXO session'); const confirmed = result.balance.confirmed.raw; const estFee = 1500; // rough sat estimate; better: query mempool fees if (confirmed < amount + estFee) { throw new Error(`Insufficient confirmed balance: ${confirmed} sats`); } return sendTransfer({ recipient: { address: to, amount }, fee: 'auto', }); } ``` Use `confirmed` (not total) for the precheck. Unconfirmed funds can be replaced via RBF before they mine — relying on them for the next send risks a double-spend. ## Sign a server-built PSBT Common pattern when coin selection happens server-side — your backend builds a PSBT, your frontend calls `utxo.signPsbt` to sign + broadcast. ```ts import { utxo } from '@oviato/connect'; async function acceptListing(listingId: string) { // 1. Backend returns a PSBT const { psbt } = await fetch(`/api/listings/${listingId}/prepare`).then((r) => r.json()); // 2. User signs + broadcasts const r = await utxo.signPsbt({ psbt, broadcast: true }); if (r.status === 'success' && r.type === 'signPsbtBroadcastSuccess') { return { txid: r.data.txid }; } throw new Error(r.status === 'error' ? r.message : 'Sign failed'); } ``` ## Sign-only (counterparty broadcasts) When a different party will broadcast after you sign: ```ts const r = await utxo.signPsbt({ psbt: serverPsbt, broadcast: false }); if (r.status === 'success' && r.type === 'signPsbtSuccess') { await fetch('/api/listings/complete', { method: 'POST', body: JSON.stringify({ signedPsbt: r.data.psbt }), }); } ``` ## Watch for confirmation The SDK doesn't currently ship a `waitForConfirmation` for UTXO. Poll via the RPC client: ```ts import { rpc } from '@oviato/connect'; async function waitForConfirmation(txid: string, address: string, minConfirms = 1) { while (true) { const r = await rpc.getTransactionHistory({ address, limit: 20 }); const match = r.result?.transactions.find((t) => t.txid === txid); if (match && match.confirmations >= minConfirms) return match; await new Promise((r) => setTimeout(r, 15_000)); // 15s poll } } ``` For production, prefer a block-indexer webhook over polling. ## Related - [Sign PSBT](/utxo/sign-psbt) — full `utxo.signPsbt` reference - [Send Transfers](/chain-agnostic/send-transfers) — simpler native BTC sends --- # Section: React Components # React Components Overview > The React-only surface of @oviato/connect — provider, hooks, and drop-in UI components. Canonical URL: https://docs.oviato.com/react-components/overview File: react-components/overview.mdx `@oviato/connect/client` is where the React-specific surface lives — a provider for session state, hooks for accessing it, and pre-built UI components you can drop into your app. ```tsx import { OviConnectProvider, useOviConnect, ConnectButton, Connect, } from '@oviato/connect/client'; ``` Everything else — chain-agnostic actions, `evm.*`, `utxo.*`, RPC — lives at the main `@oviato/connect` entry point (which re-exports `@oviato/sdk`). The `/client` subpath is **strictly for React code**. Not on React? Everything in this section is unavailable. See [Packages → Choosing a Package](/packages/choosing) for the framework-agnostic `@oviato/sdk` path. ## The two-import pattern A normal Oviato React app imports from two places: ```tsx // Actions (works anywhere — just called from React) import { sendTransfer, signMessage, evm, utxo } from '@oviato/connect'; // React-only: provider, hooks, components import { OviConnectProvider, useOviConnect, ConnectButton } from '@oviato/connect/client'; ``` The split keeps your bundle clean — server components and non-React code paths don't pull in React. ## What's in here ### Provider Wraps your app to initialize the SDK + expose session state to hooks. One per tree. ```tsx {children} ``` Config: `appId`, `theme`, `dns`, `type`, `domain`. See [Provider](/react-components/provider). ### Hooks `useOviConnect()` — the primary hook. Gives you `session`, `isLoading`, `disconnect`, plus `signMessage` / `sendTransfer` / `switchNetwork` as callbacks. `useOviStore()` — direct access to the Zustand store for advanced state subscriptions. See [Hooks](/react-components/hooks). ### Components `` — full auth form (username input + passkey flow). Good for dedicated sign-in pages. `` — one-click auth button. Good for headers, modals. Both are styled + theme-aware out of the box. See [Components](/react-components/components). ## SSR-safe Everything in this section is compatible with Next.js App Router, Pages Router, and other SSR frameworks: - Provider mounts client-side only (no hydration flash) - Hooks guard against `useEffect`-less server renders - Components handle `'use client'` boundaries cleanly See the [Quickstart](/getting-started/quickstart) for the exact provider placement pattern. ## CSS The React components ship with their own stylesheet. Import once at the app root: ```tsx import '@oviato/connect/client.css'; ``` Or import the full styles (includes modal, button, overlays): ```tsx import '@oviato/connect/styles.css'; ``` Both are scoped with a CSS-layer prefix so they don't collide with your Tailwind / shadcn / MUI. ## Theme The provider accepts `theme: 'light' | 'dark' | 'auto'`. `'auto'` respects `prefers-color-scheme`. Override via the dashboard for per-app branding (accent color, logo). ## Explore --- # OviConnectProvider > The React provider that initializes the SDK and exposes session state to child hooks + components. Canonical URL: https://docs.oviato.com/react-components/provider File: react-components/provider.mdx `` is the entry point for every React app using Oviato. It initializes the SDK with your `appId`, mounts the session store, and makes the `useOviConnect` hook work across the tree. ```tsx import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; ``` Place this **as high in your tree as possible** — above any component that calls `useOviConnect()` or renders ``. A common pattern is wrapping the whole `` or the root `` layout. ## Where to mount it Create a client-only Providers wrapper (required because the provider uses React context). ```tsx title="app/providers.tsx" 'use client'; import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx title="app/layout.tsx" import { Providers } from './providers'; export default function RootLayout({ children }) { return ( {children} ); } ``` ```tsx title="pages/_app.tsx" import type { AppProps } from 'next/app'; import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; export default function App({ Component, pageProps }: AppProps) { return ( ); } ``` ```tsx title="src/main.tsx" import React from 'react'; import ReactDOM from 'react-dom/client'; import { OviConnectProvider } from '@oviato/connect/client'; import '@oviato/connect/client.css'; import App from './App'; ReactDOM.createRoot(document.getElementById('root')!).render( , ); ``` ## Props | Prop | Type | Default | Notes | |---|---|---|---| | `appId` | `string` | — | **Required.** From the [Developer Dashboard](/developer-dashboard/setting-up-app). | | `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | `'auto'` respects `prefers-color-scheme`. | | `domain` | `string` | session-set | Override the domain the SDK registers under. Useful for custom subdomains. | | `dns` | `string` | auto | Override the backend DNS. For staging or self-hosted deployments. | | `type` | `'production' \| 'staging' \| 'development'` | auto | Forces a specific environment tier; normally detected from `appId`. | | `accent_color` | `string` | dashboard | Hex color for UI accents. Overrides the dashboard setting. | | `logo_url` | `string` | dashboard | Logo shown in the auth popup. Overrides the dashboard setting. | **Do not pass `network:` to the provider.** Earlier versions accepted a provider-level network, but that made per-action network overrides ambiguous. Specify the network on ``, ``, and imperative calls like `login`, `register`, `authenticate` instead. ## What happens on mount The provider is a thin wrapper around `initialize()` from the core SDK. Everything useful flows through the Zustand session store — the provider just sets it up and keeps it alive for the component tree. 1. Client-side hydration runs → provider reads localStorage for a cached session. 2. `initialize()` is called with `{ appId, theme, ... }` → backend app config is fetched + cached. 3. Session state becomes available via `useOviConnect()`. 4. If there's a cached session, `isLoading` flips to `false` with `session` populated. Otherwise, `session` is `null`. ## Dev-mode overrides For local development against a staging backend, you can override the bridge + RPC origins via `window` globals: ```tsx // Before the provider mounts — e.g. in a useEffect in _app.tsx if (process.env.NODE_ENV === 'development') { (window as any).__OVIATO_BRIDGE_ORIGIN__ = 'http://localhost:5173'; (window as any).__OVIATO_RPC_ORIGIN__ = 'http://localhost:8787'; (window as any).__OVI_FORCE_IFRAME__ = true; // popups unreliable in local dev } ``` These are convenience hooks for local iteration — not a production config surface. ## SSR safety The provider is `'use client'` — it never runs on the server. A server-rendered page will see `session` as `undefined` on first paint, then populate on client hydration. Use `isLoading` to avoid layout shift: ```tsx function MyComponent() { const { session, isLoading } = useOviConnect(); if (isLoading) return ; return session ? : ; } ``` ## Multiple providers Don't nest. The provider uses React context + a global Zustand store. Nesting duplicates state but only the innermost wins — confusing failure modes. ## Related - [Hooks](/react-components/hooks) — `useOviConnect`, `useOviStore` - [Components](/react-components/components) — ``, `` - [Quickstart](/getting-started/quickstart) — end-to-end setup --- # Using Hooks > React hooks for Oviato Connect - useOviConnect and useOviStore Canonical URL: https://docs.oviato.com/react-components/hooks File: react-components/hooks.mdx @oviato/connect provides React hooks to access authentication state and SDK functionality in your React components. ## useOviConnect Hook The primary hook for accessing Oviato authentication state and methods. Must be used inside `OviConnectProvider`. ### Basic Usage ```tsx import { useOviConnect } from '@oviato/connect/client'; function MyComponent() { const { session, isLoading, disconnect } = useOviConnect(); if (isLoading) { return
Loading...
; } if (!session) { return
Not connected
; } return (

Address: {session.address}

); } ``` ### Return Values The `useOviConnect` hook returns an object with the following properties: ```typescript { // Current user session (null if not authenticated) session: Session | null; // Whether SDK is initializing isLoading: boolean; // Whether SDK is ready to use ready: boolean; // Update the session setSession: (session: Session) => void; // Disconnect and clear session disconnect: () => void; // Get current initialization options getInitOptions: () => InitOptions | null; } ``` ### Session Object When authenticated, the session object contains: ```typescript type Session = { address: string; // Wallet address publicKey: string; // Public key (hex) username?: string; // OVI.ID username (if set) network: Network; // Current network appId: string; // Your app ID userId: string; // Oviato user ID createdAt: number; // Timestamp }; ``` ### Complete Example ```tsx 'use client'; import { useOviConnect } from '@oviato/connect/client'; import { authenticate, signMessage } from '@oviato/connect'; export default function WalletPage() { const { session, isLoading, ready, disconnect } = useOviConnect(); const handleConnect = async () => { const result = await authenticate(); if (result.status === 'error') { console.error('Auth failed:', result.message); } }; const handleSign = async () => { const result = await signMessage('Hello from Oviato!'); if (result.status === 'success') { console.log('Signature:', result.data.signature); } }; if (isLoading || !ready) { return
Loading Oviato...
; } if (!session) { return (

Connect Your Wallet

); } return (

Connected

Address: {session.address}

{session.username &&

Username: @{session.username}

}

Network: {session.network}

); } ``` ## useOviStore Hook Advanced hook for accessing the Zustand store directly. Provides more granular control over state subscriptions. ### Basic Usage ```tsx import { useOviStore } from '@oviato/connect'; function MyComponent() { // Subscribe to specific state const session = useOviStore(state => state.session); const isAuthenticated = useOviStore(state => state.isAuthenticated()); return (
{isAuthenticated ? (

Address: {session?.address}

) : (

Not connected

)}
); } ``` ### When to Use useOviStore Use `useOviStore` when you need: - Fine-grained subscriptions to avoid unnecessary re-renders - Access to store methods directly - Integration with other state management solutions - Advanced state debugging For most cases, `useOviConnect` is recommended as it provides a simpler API. ### Available State ```typescript { // Session state session: Session | null; isAuthenticated: () => boolean; // Loading state isLoading: boolean; ready: boolean; // Methods setSession: (session: Session) => void; clearSession: () => void; setLoading: (loading: boolean) => void; setReady: (ready: boolean) => void; } ``` ### Performance Optimization ```tsx import { useOviStore } from '@oviato/connect'; function AddressDisplay() { // Only re-renders when address changes const address = useOviStore(state => state.session?.address); return
{address || 'Not connected'}
; } function LoadingIndicator() { // Only re-renders when loading state changes const isLoading = useOviStore(state => state.isLoading); return isLoading ? : null; } ``` ### Combining Multiple State Values ```tsx import { useOviStore } from '@oviato/connect'; function WalletInfo() { // Efficient: single subscription for multiple values const { address, network, username } = useOviStore(state => ({ address: state.session?.address, network: state.session?.network, username: state.session?.username, })); return (

Address: {address}

Network: {network}

{username &&

Username: @{username}

}
); } ``` ## Hook Rules Both hooks must follow React's rules of hooks: - ✅ Must be used inside `OviConnectProvider` - ✅ Only call at the top level of components - ✅ Only call from React function components or custom hooks - ❌ Don't call inside loops, conditions, or nested functions ### Error: Hook Used Outside Provider ```tsx // ❌ Wrong: Used outside provider function App() { const { session } = useOviConnect(); // Error! return
{session?.address}
; } // ✅ Correct: Used inside provider function App() { return ( ); } function MyComponent() { const { session } = useOviConnect(); // Works! return
{session?.address}
; } ``` ## TypeScript Support Both hooks have full TypeScript support: ```tsx import { useOviConnect } from '@oviato/connect/client'; import type { Session } from '@oviato/connect'; function MyComponent() { const { session }: { session: Session | null } = useOviConnect(); // TypeScript knows session can be null if (session) { // Here, TypeScript knows session is Session console.log(session.address); // ✅ Type-safe } } ``` ## Next Steps - **[Components Reference](/react-components/components)** - Explore pre-built UI components - **[Signing Messages](/chain-agnostic/sign-messages)** - Sign messages with hooks - **[Signing Transactions](/chain-agnostic/send-transfers)** - Send transactions with hooks --- # Components Reference > Pre-built UI components for Oviato Connect Canonical URL: https://docs.oviato.com/react-components/components File: react-components/components.mdx @oviato/connect provides pre-built React components for authentication and wallet management. These components handle the complex UI and authentication flow, allowing you to integrate Oviato quickly. ## Connect A pre-built authentication form with username input and availability checking. This is the easiest way to add Oviato authentication to your app. ### Basic Usage ```tsx import { Connect } from '@oviato/connect/client'; function MyComponent() { return ( { console.log('Connected:', data.address); console.log('Public Key:', data.publicKey); }} /> ); } ``` ### Props ```typescript interface ConnectProps { /** Callback fired on successful authentication */ callback?: (data: { address: string; publicKey: string; }) => void; } ``` ### Features The `Connect` component includes: - **Username input** with real-time validation - **Availability checking** - verifies username is not taken - **Auto login/register** - automatically determines if user is new or existing - **Loading states** - shows feedback during authentication - **Built-in styling** - works out of the box with minimal setup ### Complete Example ```tsx 'use client'; import { Connect } from '@oviato/connect/client'; import { useOviConnect } from '@oviato/connect/client'; export default function AuthPage() { const { session } = useOviConnect(); if (session) { return (

Already connected as {session.address}

); } return (

Connect Your Wallet

{ console.log('Successfully connected!'); console.log('Address:', data.address); // Redirect or update UI window.location.href = '/dashboard'; }} />
); } ``` ### Styling The `Connect` component uses class names that you can override: ```css /* Override default styles */ .ovi-connect-form { /* Form container */ } .ovi-connect-input { /* Username input */ } .ovi-connect-button { /* Submit button */ } .ovi-connect-status { /* Status message (username availability) */ } ``` ### Multi-Step Form Example ```tsx 'use client'; import { useState } from 'react'; import { Connect } from '@oviato/connect/client'; export default function OnboardingPage() { const [step, setStep] = useState(1); return (
{step === 1 && (

Step 1: Welcome

)} {step === 2 && (

Step 2: Connect Wallet

{ setStep(3); }} />
)} {step === 3 && (

You're all set!

)}
); } ``` ## ConnectButton A flexible authentication button that triggers the Oviato passkey flow. Use this when you want to build your own UI but need a simple connect button. ### Basic Usage ```tsx import { ConnectButton } from '@oviato/connect/client'; function MyComponent() { return ( { console.log('Connected:', data.address); }} onError={(error) => { console.error('Failed:', error); }} /> ); } ``` ### Props ```typescript interface ConnectButtonProps { /** Text for default button (default: "Signup / Login with Passkey") */ text?: string; /** Custom children to render instead of default button */ children?: ReactNode; /** Callback fired on successful authentication */ onSuccess?: (data: { address: string; publicKey: string; username: string; }) => void; /** Callback fired on authentication error */ onError?: (error: Error) => void; /** Custom className for default button (ignored if children provided) */ className?: string; /** Authentication method (default: 'iframe') */ method?: 'iframe' | 'popup'; } ``` ### Custom Children Use your own button component: ```tsx import { ConnectButton } from '@oviato/connect/client'; function MyComponent() { return ( console.log(data)}> ); } ``` ### With Callback Handling ```tsx import { ConnectButton } from '@oviato/connect/client'; import { useState } from 'react'; function MyComponent() { const [error, setError] = useState(null); return (
{ console.log('Address:', data.address); console.log('Username:', data.username); }} onError={(error) => { setError(error.message); }} /> {error &&

{error}

}
); } ``` ### Popup vs Iframe Method Choose between popup or iframe authentication: ```tsx // Iframe (default) - opens in modal overlay // Popup - opens in new browser window ``` **When to use each:** - **Iframe** (recommended): Better UX, stays in-page, works on mobile - **Popup**: Use when iframe is blocked or you need popup-specific behavior ### Custom Auth Flow Example ```tsx 'use client'; import { ConnectButton, useOviConnect } from '@oviato/connect/client'; export default function AuthPage() { const { session, disconnect } = useOviConnect(); if (session) { return (

Connected: {session.address}

); } return (

Welcome to My App

Connect your wallet to get started

{ console.log('Connected successfully!'); }} />
); } ``` ## Styling Components All components support className and style props for customization: ```tsx // Using className // Using inline styles // Using custom children with full control ``` ## TypeScript Support All components have full TypeScript support: ```tsx import type { ConnectButtonProps, ConnectProps } from '@oviato/connect/client'; const buttonProps: ConnectButtonProps = { text: 'Connect', onSuccess: (data) => { // data is fully typed console.log(data.address); }, }; const connectProps: ConnectProps = { callback: (data) => { // data is fully typed console.log(data.publicKey); }, }; ``` ## Next Steps - **[Using Hooks](/react-components/hooks)** - Learn about useOviConnect and useOviStore - **[Signing Messages](/chain-agnostic/sign-messages)** - Sign messages with the wallet - **[Signing Transactions](/chain-agnostic/send-transfers)** - Send transactions --- # Section: x402 Payments # x402 Payments > Enable HTTP 402 Payment Required flows with @oviato/connect Canonical URL: https://docs.oviato.com/x402/overview File: x402/overview.mdx x402 is a protocol for native web payments using HTTP 402 (Payment Required) responses. Oviato SDK provides built-in support for handling x402 payments on Base. ## What is x402? x402 enables seamless micropayments on the web: 1. Server returns `402 Payment Required` with payment details 2. Client signs the payment request with user's wallet 3. Server verifies signature and grants access This enables pay-per-use APIs, premium content, and micropayments without traditional payment processors. ## Quick Start: x402.pay() The easiest way to handle x402 payments — one line handles everything: ```javascript import { x402 } from "@oviato/connect"; // Fetch with automatic payment handling const { response, paid, amount, token } = await x402.pay( "https://api.example.com/premium" ); const data = await response.json(); if (paid) { console.log(`Paid ${amount} ${token} for access`); } ``` `x402.pay()` automatically: 1. Makes the initial request 2. Detects 402 Payment Required 3. Opens the payment popup for user approval 4. Signs the payment 5. Retries with the signed payment header ### With Fetch Options ```javascript const { response, paid } = await x402.pay("https://api.example.com/data", { method: "POST", body: JSON.stringify({ query: "..." }), headers: { "Content-Type": "application/json" }, }); ``` ## Low-Level: x402.sign() For more control, use `x402.sign()` to sign payment headers manually: ```javascript import { x402 } from "@oviato/connect"; // Make initial request const response = await fetch("https://api.example.com/premium"); if (response.status === 402) { // Get payment header const paymentHeader = response.headers.get("payment-required"); // Sign the payment const { signedPayload } = await x402.sign({ paymentHeader }); // Retry with signature const paidResponse = await fetch("https://api.example.com/premium", { headers: { "payment-signature": signedPayload }, }); const data = await paidResponse.json(); } ``` ## Helper Functions ### Check if Payment Required ```javascript import { x402 } from "@oviato/connect"; const response = await fetch(url); if (x402.isPaymentRequired(response)) { // Handle 402... } ``` ### Get Payment Details ```javascript import { x402 } from "@oviato/connect"; const response = await fetch(url); const details = x402.getPaymentDetails(response); if (details) { console.log("Amount:", details.accepts[0].amount); console.log("Network:", details.accepts[0].network); } ``` ## Supported Networks x402 payments are currently supported on Base: - `base` - Base Mainnet - `basesepolia` - Base Sepolia ## API Reference ### x402.pay(url, options?) Fetch a URL with automatic x402 payment handling. ```typescript type X402PayOptions = RequestInit & { acceptIndex?: number; // Which payment option to use (default: 0) method?: "popup" | "iframe"; // Bridge method }; type X402PayResult = { response: Response; // Final HTTP response paid: boolean; // Whether payment was made amount?: string; // Formatted amount paid token?: string; // Token name (e.g., "USDC") network?: string; // Network name }; ``` ### x402.sign(options) Sign a payment-required header. ```typescript type X402SignOptions = { paymentHeader: string | X402PaymentRequired; // Base64 header or parsed object acceptIndex?: number; // Which payment option (default: 0) method?: "popup" | "iframe"; // Bridge method }; type X402SignResult = { signedPayload: string; // Signed payment to send in header authorization: object; // Authorization details signature: string; // Raw signature network: string; // Network used chainId: number; // Chain ID }; ``` ## Error Handling ```javascript import { x402 } from "@oviato/connect"; try { const { response, paid } = await x402.pay(url); // Success... } catch (error) { if (error.message.includes("User rejected")) { console.log("User cancelled payment"); } else if (error.message.includes("Insufficient")) { console.log("Not enough balance"); } else { console.log("Payment failed:", error.message); } } ``` ## Security Considerations 1. **Verify payment details** - Always show users what they're paying for 2. **Check amounts** - Validate payment amounts before signing 3. **Trusted servers only** - Only pay servers you trust 4. **Review permissions** - Users approve each payment in the signing modal ## Use Cases - **Pay-per-API** - Charge per API call - **Premium content** - Unlock articles, videos, data - **Micropayments** - Sub-cent transactions - **AI inference** - Pay for compute on-demand - **Data feeds** - Real-time market data access ## Next Steps - **[Wallet Operations](/chain-agnostic/overview)** - Other wallet functions - **[RPC & Utilities](/reference/rpc)** - Query balances and transactions --- # Section: Reference # RPC & Utilities > Query blockchain data, resolve usernames, and use utility helpers with @oviato/connect Canonical URL: https://docs.oviato.com/reference/rpc File: reference/rpc.mdx @oviato/connect provides a powerful RPC client for querying blockchain data and a collection of utility functions for common tasks. ## RPC Client Overview The RPC client uses JSON-RPC 2.0 protocol to communicate with Oviato's backend services. It automatically handles: - **Authentication** - Includes auth tokens when available - **Network context** - Routes requests to the correct network - **Rate limiting** - Respects backend rate limits - **Error handling** - Provides typed error responses ```javascript import { rpc, rpcRequest } from "@oviato/connect"; // Use the default client const balance = await rpc.getBalance({ address: "bc1q..." }); ``` ## Get Balance Retrieve the balance for any address. The response format varies based on network type. ```javascript import { rpc, getSession } from "@oviato/connect"; const session = getSession(); const result = await rpc.getBalance({ address: session.address, }); if (result.result) { const balance = result.result; console.log("Address:", balance.address); console.log("Network:", balance.networkName); console.log("Symbol:", balance.symbol); // Balance is returned in three formats console.log("Raw (satoshis/wei):", balance.balance.raw); console.log("Decimal value:", balance.balance.value); console.log("Formatted:", balance.balance.formatted); // e.g., "1.5 BTC" } ``` ### EVM Balance Response For Ethereum and EVM-compatible networks: ```typescript interface EvmBalanceResponse { address: string; network: string; // e.g., "ethmainnet" networkName: string; // e.g., "Ethereum Mainnet" chainType: "evm"; symbol: string; // e.g., "ETH" decimals: number; // e.g., 18 balance: { raw: string; // Balance in wei value: string; // Decimal value (e.g., "1.50000000") formatted: string; // Human-readable (e.g., "1.5 ETH") }; } ``` ### UTXO Balance Response For Bitcoin and UTXO-based networks, includes confirmed/unconfirmed breakdown: ```typescript interface UtxoBalanceResponse { address: string; network: string; // e.g., "bitcoinmainnet" networkName: string; // e.g., "Bitcoin Mainnet" chainType: "utxo"; symbol: string; // e.g., "BTC" decimals: number; // e.g., 8 balance: { raw: string; // Total balance in satoshis value: string; // Decimal value (e.g., "0.00100000") formatted: string; // Human-readable (e.g., "0.001 BTC") confirmed: { // On-chain confirmed balance raw: string; value: string; formatted: string; }; unconfirmed: { // Mempool unconfirmed balance raw: string; value: string; formatted: string; }; }; } ``` ### Display Balance Example ```javascript import { rpc, getSession } from "@oviato/connect"; async function displayBalance() { const session = getSession(); const result = await rpc.getBalance({ address: session.address }); if (!result.result) { console.error("Failed to fetch balance"); return; } const { balance, chainType, symbol } = result.result; // Show formatted balance console.log(`Balance: ${balance.formatted}`); // For UTXO networks, show confirmed/unconfirmed if (chainType === "utxo") { console.log(`Confirmed: ${balance.confirmed.formatted}`); console.log(`Pending: ${balance.unconfirmed.formatted}`); } } ``` ## Resolve OVI.ID Username Look up blockchain addresses associated with an OVI.ID username: ```javascript import { resolveUsername } from "@oviato/connect"; const result = await resolveUsername("alice"); if (result.status === "success") { console.log("Names:", result.data.names); // Addresses for different networks result.data.addresses.forEach((addr) => { console.log(`${addr.network}: ${addr.address}`); }); } ``` ### Using RPC Client ```javascript import { rpc } from "@oviato/connect"; const result = await rpc.resolveUsername({ username: "alice" }); if (result.result) { const { names, addresses } = result.result; // Find Bitcoin address const btcAddress = addresses.find(a => a.network === "bitcoinmainnet"); if (btcAddress) { console.log("Bitcoin address:", btcAddress.address); console.log("Public key:", btcAddress.publicKey); } } ``` ### Username Resolution Response ```typescript interface ResolveUsernameResponse { names: Record; // Name mappings addresses: Array<{ network: string; // Network identifier address: string; // Blockchain address publicKey: string; // Public key path: number; // Derivation path index }>; } ``` ## Custom RPC Calls Make arbitrary RPC calls for advanced use cases: ```javascript import { rpcRequest, rpc } from "@oviato/connect"; // Using rpcRequest directly const result = await rpcRequest("customMethod", { param1: "value1", param2: "value2", }); // Using the client's call method const result2 = await rpc.call("anotherMethod", { data: "example", }); // With type parameter const typedResult = await rpcRequest<{ custom: string }>("getCustomData", { id: "123", }); ``` ### Request Headers The RPC client automatically includes these headers: | Header | Description | | ------------------- | ------------------------------------ | | `X-Ovi-App-Id` | Your application ID | | `X-Ovi-Network` | Current network (e.g., bitcoinmainnet) | | `X-Ovi-Type` | Network type (e.g., utxo, evm) | | `X-Ovi-Environment` | Environment (mainnet, testnet) | | `X-Ovi-Version` | SDK version | | `X-Ovi-Origin` | Request origin | | `X-Ovi-Address` | User's address (if authenticated) | | `Authorization` | Bearer token (if authenticated) | ## RPC Error Handling Handle RPC errors gracefully: ```javascript import { rpc } from "@oviato/connect"; try { const result = await rpc.getBalance({ address: "invalid" }); if (result.error) { console.error("RPC Error:", result.error.message); console.error("Error Code:", result.error.code); } } catch (error) { console.error("Request failed:", error.message); } ``` ### Error Codes Standard JSON-RPC 2.0 error codes plus custom Oviato codes: | Code | Name | Description | | -------- | ------------------- | ------------------------------ | | `-32700` | Parse Error | Invalid JSON | | `-32600` | Invalid Request | Malformed request | | `-32601` | Method Not Found | Unknown RPC method | | `-32602` | Invalid Params | Invalid method parameters | | `-32603` | Internal Error | Server-side error | | `-32001` | Unauthorized | Authentication required | | `-32002` | Rate Limit | Too many requests | | `-32003` | Insufficient Funds | Not enough balance | ## Utility Functions @oviato/connect includes utility functions for common tasks. ### Browser Detection Detect browser environment and capabilities: ```javascript import { isBrowser, isIOS, isSafari, supportsWebAuthn, getOrigin, } from "@oviato/connect/utils/browser"; // Check if running in browser (not SSR) if (isBrowser()) { console.log("Running in browser"); } // Detect iOS devices if (isIOS()) { console.log("iOS device detected"); } // Detect Safari browser if (isSafari()) { console.log("Safari browser"); } // Check WebAuthn support if (supportsWebAuthn()) { console.log("WebAuthn is supported"); } else { console.log("WebAuthn not available"); } // Get current origin const origin = getOrigin(); console.log("Origin:", origin); // e.g., "https://example.com" ``` ### Validation Utilities Validate URLs, colors, and domains: ```javascript import { isValidUrl, isValidImageUrl, isValidHexColor, isValidDomain, normalizeDomain, } from "@oviato/connect/utils/validation"; // Validate any URL isValidUrl("https://example.com"); // true isValidUrl("not-a-url"); // false // Validate image URLs isValidImageUrl("https://example.com/logo.png"); // true isValidImageUrl("https://example.com/file.pdf"); // false // Validate hex colors isValidHexColor("#ff0000"); // true isValidHexColor("#f00"); // true isValidHexColor("red"); // false // Validate domains isValidDomain("example.com"); // true isValidDomain("sub.example.com"); // true // Normalize domain (remove protocol and trailing slash) normalizeDomain("https://example.com/"); // "example.com" ``` ### Storage Utilities SSR-safe localStorage and sessionStorage helpers: ```javascript import { getItem, setItem, removeItem, getJSON, setJSON, clear, } from "@oviato/connect/utils/storage"; // Basic string storage setItem("myKey", "myValue"); const value = getItem("myKey"); removeItem("myKey"); // JSON storage (auto serialize/deserialize) setJSON("user", { name: "Alice", id: 123 }); const user = getJSON("user"); console.log(user.name); // "Alice" // Use session storage instead of local setItem("tempKey", "tempValue", "session"); const temp = getItem("tempKey", "session"); // Clear all storage clear(); // Clear localStorage clear("session"); // Clear sessionStorage ``` ### Storage Type Reference ```typescript type StorageType = "local" | "session"; // All functions accept optional storage type (default: "local") getItem(key: string, type?: StorageType): string | null; setItem(key: string, value: string, type?: StorageType): boolean; removeItem(key: string, type?: StorageType): boolean; getJSON(key: string, type?: StorageType): T | null; setJSON(key: string, value: T, type?: StorageType): boolean; clear(type?: StorageType): boolean; ``` ## Complete RPC Example Here's a comprehensive example using RPC and utilities: ```javascript import { initialize, authenticate, getSession, rpc, resolveUsername, } from "@oviato/connect"; async function main() { // Initialize SDK await initialize({ appId: "your-app-id", network: "bitcoinmainnet", }); // Authenticate user const authResult = await authenticate(); if (authResult.status !== "success") { console.error("Authentication failed"); return; } const session = getSession(); console.log("Logged in as:", session.username); // Get balance with detailed breakdown const balanceResult = await rpc.getBalance({ address: session.address, }); if (balanceResult.result) { const { balance, chainType } = balanceResult.result; console.log("\n--- Balance ---"); console.log("Total:", balance.formatted); if (chainType === "utxo") { console.log("Confirmed:", balance.confirmed.formatted); console.log("Pending:", balance.unconfirmed.formatted); } } // Get recent transactions const historyResult = await rpc.getTransactionHistory({ address: session.address, limit: 5, }); if (historyResult.result) { console.log("\n--- Recent Transactions ---"); historyResult.result.transactions.forEach((tx) => { const type = tx.type === "received" ? "+" : "-"; const date = new Date(tx.timestamp * 1000).toLocaleDateString(); console.log(`${type}${tx.amount} sats | ${tx.confirmations} conf | ${date}`); }); } // Resolve a friend's username const friendResult = await resolveUsername("bob"); if (friendResult.status === "success") { console.log("\n--- Bob's Addresses ---"); friendResult.data.addresses.forEach((addr) => { console.log(`${addr.network}: ${addr.address}`); }); } } main(); ``` ## Security Considerations ### Read-Only Access The RPC client is **read-only** by design: - Cannot sign transactions - Cannot send funds - Cannot modify wallet state All signing operations require WebAuthn authentication through the SDK's wallet methods. ### Token Security - Auth tokens are automatically included when available - Tokens expire and require re-authentication - Tokens are domain-scoped (only work from registered origins) ### Rate Limiting - Requests are rate-limited per app/token/IP - Handle `-32002` error codes gracefully - Implement exponential backoff for retries ```javascript async function fetchWithRetry(fn, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (error.message.includes("-32002") && i < maxRetries - 1) { // Rate limited - wait before retry await new Promise(r => setTimeout(r, 1000 * (i + 1))); continue; } throw error; } } } ``` ## Next Steps - **[Wallet Operations](/chain-agnostic/overview)** - Sign messages and send transactions - **[Authentication](/chain-agnostic/authentication)** - Learn about user authentication - **[Installation](/getting-started/quickstart)** - SDK setup guide --- # Section: Changelog # Changelog > Combined release history for @oviato/sdk and @oviato/connect. Canonical URL: https://docs.oviato.com/changelog File: changelog.mdx Release history for **`@oviato/sdk`** and **`@oviato/connect`** in a single timeline. Because `@oviato/connect` re-exports `@oviato/sdk`, most versions ship in both packages together — those entries show once, annotated with both package names. When a release has React-only or SDK-only content, the affected package is labelled explicitly. Releases are managed via [changesets](https://github.com/changesets/changesets) and published to npm via GitHub Actions. This page regenerates on every docs deploy. ## 0.4.1 **Packages:** `@oviato/sdk` + `@oviato/connect` - Restore missing type declarations. --- ## 0.4.0 **Packages:** `@oviato/sdk` + `@oviato/connect` _Introducing EVM wallet support_ **`@oviato/sdk`** Adds full EVM wallet support on **Ethereum Mainnet, Base, Ethereum Sepolia, and Base Sepolia**. Networks share one `Session` and one passkey; switch via `switchNetwork()` or per-action `network` overrides. New top-level **`evm.*`** namespace, shaped like viem/ethers so existing EVM devs drop in with familiar idioms. UTXO (Bitcoin) moves under `utxo.*`. **Reads:** `readContract`, `multicall` (Multicall3), `getTokenBalance`, `getTokenAllowance`, `getNftOwner`, `getNftBalance`, `getNftMetadata` (resolves `data:` / `ipfs://` / `ar://` / `https://` and normalizes image URL), `getTokens` (curated ERC-20 registry per network — USDC, USDT, WETH, WBTC/cbBTC where canonical). **Writes:** `sendTransaction` (native + raw calldata), `writeContract` (ABI-aware, with decoded confirmation sheets for ERC-20 transfer, approve, ERC-721, ERC-1155, and generic contract calls), `waitForTransactionReceipt`. **Signing:** `signMessage` (EIP-191), `signTypedData` (EIP-712), plus offchain `verifyMessage` / `verifyTypedData`. **Payments:** `x402` helpers for gasless USDC payments (ERC-3009 `transferWithAuthorization`). **Breaking changes (one-line migrations):** - Flat EVM exports are now namespaced: `import { writeContract }` → `import { evm }` then `evm.writeContract()`. Applies to every EVM primitive. - EVM `Transaction` type renamed to `EvmTransaction` to disambiguate from the UTXO `Transaction`. **`@oviato/connect`** - Tracks `@oviato/sdk` 0.4.0 — exposes the new `evm.*` and `utxo.*` namespaces through the React provider. `useOviConnect`, `OviConnectProvider`, `Connect`, and `ConnectButton` surfaces unchanged. --- ## 0.3.1 **Packages:** `@oviato/sdk` + `@oviato/connect` **`@oviato/sdk`** - Enhanced bridge experience with themed loading states - Improved SDK initialization and configuration handling - Added dark mode theme propagation to popup and iframe **`@oviato/connect`** - Added dark mode support with automatic system preference detection - Added `invertLogo` prop to `` component for dark logo compatibility - Improved UI styling and component consistency --- ## 0.3.0 **Packages:** `@oviato/sdk` + `@oviato/connect` _x402 payments, typed-data signing, and Base network_ - Add x402 payment signing, EIP-712 typed data signing, and BASE network support - x402 payment signing support for HTTP 402 payment flows - EIP-712 typed data signing (`signTypedData`) for EVM chains - BASE network support (mainnet and Sepolia testnet) - Tailwind CSS 4.2.2 upgrade with CSS isolation fix (no more global style conflicts) - Enter key support on Connect form for better UX --- ## 0.2.1 **Packages:** `@oviato/sdk` + `@oviato/connect` - Patch for UX improvement on choosing between iframe/popup for localhost --- ## 0.2.0 **Packages:** `@oviato/sdk` + `@oviato/connect` _Minor changes_ - Add PSBT signing with prepare/decode support, multi-recipient transfers for UTXO chains, SigHash types, fee options, and enhanced RPC methods --- ## 0.1.15 **Packages:** `@oviato/sdk` + `@oviato/connect` - Added common getBalance functionality for EVM and UTXO chains --- ## 0.1.14 **Packages:** `@oviato/sdk` + `@oviato/connect` - Improved app configuration caching and reactivity. Config updates now reflect immediately in the UI, and ETag-based caching reduces unnecessary network requests. --- ## 0.1.13 **Packages:** `@oviato/sdk` + `@oviato/connect` - Bug fixes --- ## 0.1.12 **Packages:** `@oviato/sdk` + `@oviato/connect` - Optimized build output size by removing debug logs in production --- ## 0.1.11 **Packages:** `@oviato/sdk` + `@oviato/connect` - Bridge method selection improvements: removed flawed Storage Access API check and redundant Safari/iOS fallback, allowing iframe mode to work correctly on HTTPS sites --- ## 0.1.10 **Packages:** `@oviato/sdk` + `@oviato/connect` **`@oviato/sdk`** ### Improved - Bridge method selection logic for better iframe usage on HTTPS sites **`@oviato/connect`** - ### Improved - Bridge method selection logic for better iframe usage on HTTPS sites --- ## 0.1.9 **Packages:** `@oviato/sdk` + `@oviato/connect` - Fix workspace dependency handling for manual publishing workflow --- ## 0.1.8 **Packages:** `@oviato/sdk` + `@oviato/connect` - Add hex validation to signPsbt function - Make signInputs parameter optional (signs all inputs if omitted) - Update signPsbt documentation with improved examples - Fix SSR hydration issue in Next.js and other SSR frameworks --- ---