Algorand x EVM
Algorand x EVM lets you use your existing Ethereum wallet to control an Algorand account. No new wallet, no new seed phrase, no new app to install.
Algorand x EVM derives an Algorand address from your EVM address and uses an on-chain Smart Account to verify your EVM signatures.
- One wallet - your EVM wallet works on both Ethereum and Algorand
- One identity - your EVM address maps to exactly one Algorand address
- Full self-custody - only your wallet signature can authorize Algorand x EVM transactions
Early Adopter Integration Guide
[!IMPORTANT]
The xChain Accounts protocol is in Beta.
Currently, only React frontends are supported.
Interested in integrating xChain Accounts? Consider filling in a quick survey.
0. Remove deprecated packages
If you previously installed @d13co/liquid-ui or liquid-accounts-evm, remove them before proceeding.
1. Install packages
Use npm aliases to install the experimental @d13co builds under the @txnlab package names. This way your imports stay as @txnlab/use-wallet-react — no find-and-replace needed.
Required (RainbowKit for EVM wallet connection):
# pnpm
pnpm add @txnlab/use-wallet@npm:@d13co/use-wallet@latest \
@txnlab/use-wallet-react@npm:@d13co/use-wallet-react@latest \
@txnlab/use-wallet-ui-react@npm:@d13co/use-wallet-ui-react@latest \
algo-x-evm-sdk@latest @d13co/algo-x-evm-ui@latest @rainbow-me/rainbowkit
# npm
npm install @txnlab/use-wallet@npm:@d13co/use-wallet@latest \
@txnlab/use-wallet-react@npm:@d13co/use-wallet-react@latest \
@txnlab/use-wallet-ui-react@npm:@d13co/use-wallet-ui-react@latest \
algo-x-evm-sdk@latest @d13co/algo-x-evm-ui@latest @rainbow-me/rainbowkit
# yarn
yarn add @txnlab/use-wallet@npm:@d13co/use-wallet@latest \
@txnlab/use-wallet-react@npm:@d13co/use-wallet-react@latest \
@txnlab/use-wallet-ui-react@npm:@d13co/use-wallet-ui-react@latest \
algo-x-evm-sdk@latest @d13co/algo-x-evm-ui@latest @rainbow-me/rainbowkit
Recommended (Allbridge cross-chain bridge support):
pnpm add @allbridge/bridge-core-sdk
# npm install @allbridge/bridge-core-sdk
# yarn add @allbridge/bridge-core-sdk
Recommended (Haystack router for swap support):
pnpm add @txnlab/haystack-router
# npm install @txnlab/haystack-router
# yarn add @txnlab/haystack-router
Note: This uses use-wallet v4. Migration should be straightforward/painless if you are on v2 or v3:
- https://txnlab.gitbook.io/use-wallet/v3/guides/migrating-from-v2.x
- https://txnlab.gitbook.io/use-wallet/guides/migrating-from-v3.x
2. Usage
- Add global polyfills for the bridge SDK (at the top of your entry file)
- Create a wagmi config with
algorandChainfromalgo-x-evm-sdk - Add
WalletId.RAINBOWKITto yourWalletManager, passingwagmiConfig - Wrap your app in
<WalletUIProvider wagmiConfig={wagmiConfig} swapRouter={haystackRouter}>— it auto-wiresWagmiProvider,RainbowKitProvider, the bridge component, and (withswapRouter) the Swap tab - Place
<WalletButton />as your connect/account button
WalletUIProvider reads signTransactions from the surrounding <WalletProvider> itself — no wrapper component needed. If you don’t want the Swap tab, omit swapRouter (and the router instance and its import).
// At the top of your entry file (e.g. main.tsx)
// Required by the Allbridge bridge SDK
if (!(globalThis as any).TronWebProto) {
;(globalThis as any).TronWebProto = { Transaction: {} }
}
// ...other imports...
import {
WalletProvider, WalletManager, WalletId, NetworkId,
} from '@txnlab/use-wallet-react'
import {
WalletUIProvider, WalletButton,
} from '@txnlab/use-wallet-ui-react'
import { algorandChain } from 'algo-x-evm-sdk'
// Custom getDefaultConfig from use-wallet-ui-react instead of rainbowkit
// removes the Base Account web wallet from the default list (not supported)
import { getDefaultConfig } from '@txnlab/use-wallet-ui-react/rainbowkit'
// styling for use-wallet-ui and rainbowkit
// see also https://github.com/TxnLab/use-wallet-ui/blob/2e196e9059a7ddc9dcce5a394df6773207df6289/README.md#customization
import '@txnlab/use-wallet-ui-react/dist/style.css'
import '@rainbow-me/rainbowkit/styles.css'
// Swap router — omit this import (and the haystackRouter module-scope
// instance below) if you don't want the Swap tab.
import { RouterClient } from '@txnlab/haystack-router'
// Create wagmi config with the Algorand EVM chain
// replace values with your project name and WC ID
const wagmiConfig = getDefaultConfig({
appName: 'My xChain EVM App',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // from cloud.walletconnect.com
chains: [algorandChain],
// debug: true, // logs wagmi state changes, connector events, and EIP-1193 RPC traffic
})
const walletManager = new WalletManager({
wallets: [
{
id: WalletId.RAINBOWKIT,
options: { wagmiConfig },
},
WalletId.PERA,
WalletId.DEFLY,
WalletId.EXODUS,
// WalletId.LUTE, WalletId.KMD, etc.
],
defaultNetwork: NetworkId.MAINNET,
})
// Swap: module-scope router instance so caches aren't rebuilt per render.
// Omit this block — and `swapRouter={...}` below — if you don't want the Swap tab.
const haystackRouter = new RouterClient({
apiKey: 'YOUR_HAYSTACK_API_KEY',
autoOptIn: true,
})
function Root() {
return (
<WalletProvider manager={walletManager}>
<WalletUIProvider theme="system" wagmiConfig={wagmiConfig} swapRouter={haystackRouter}>
{/* your app */}
{/* somewhere in header */}
<WalletButton />
</WalletUIProvider>
</WalletProvider>
)
}
WalletUIProvider must be nested inside WalletProvider. It handles:
- Transaction review dialogs (before signing)
- Wallet Management UI (send ALGO, asset optins)
- RainbowKit/Wagmi provider setup (when
wagmiConfigis passed) - Swap tab (when
swapRouteris passed — or pre-builtswapoptions) - Theme injection (
'light'|'dark'|'system') - Optional
queryClientprop if you already have a@tanstack/react-queryprovider
The Swap tab wires fetchQuote to router.newQuote and executeSwap to router.newSwap(...).execute(), using the wallet signer from <WalletProvider>. The onSigned hook fires the moment the wallet returns so the panel transitions from “signing” to “sending” before submit + confirmation. Consumers who need to intercept the signer or consume UseSwapOptions outside WalletUIProvider can pre-build the options themselves with useHaystackSwapConfig({ router }) and pass the result as the swap prop instead of swapRouter.
Network switching
If your app supports multiple networks, call walletManager.setActiveNetwork(network) when the user switches. This updates internal state and reinitializes connections:
function setNetwork(network: 'localnet' | 'testnet' | 'mainnet') {
localStorage.setItem('algorand-network', network)
walletManager.setActiveNetwork(network)
}
3. Manage xChain Account
After connecting your EVM account, you can manage it via:
{WalletButton} → ⚡ Manage
To opt in to ASAs, use the Receive view.
4. Allbridge (cross-chain bridge)
The bridge UI is built into WalletUIProvider. No additional setup is required beyond installing @allbridge/bridge-core-sdk.
The buffer package and TronWebProto stub in your entry file are required by Allbridge’s bundled TronWeb dependency.
Access it from {WalletButton} → ⚡ Manage → # Bridge
5. Notices (disclaimers & info dialogs)
WalletUIProvider accepts an optional notices prop that injects gating UI (or always-visible copy) in front of certain flows. There are three kinds of notices:
disclaimer— custom text +I understand the risks and wish to proceedcheckbox +Acceptbutton. Acceptance is required to proceed.info— custom title/body + singleContinuebutton. Acknowledged on click.footer— passive text rendered inline at the bottom of a host panel. Not gated, no acknowledgement.
The disclaimer and info kinds share a single localStorage object (__wui_notices_ack__) keyed by id. A missing entry means not yet acknowledged; once acknowledged, the gate is replaced by its children on the next render in every tab. The footer kind has no acknowledgement state. If a given id has no config, the corresponding host renders as if the notice didn’t exist — every notice is fully opt-in per consumer and per id.
Built-in ids
Five ids are auto-wired by WalletUIProvider. Provide a config for any of them to enable the corresponding UI; omit it to skip it entirely.
| id | kind | When it shows |
|---|---|---|
bridge |
disclaimer |
Inline inside the Bridge panel before the bridge UI renders |
bridgeFooter |
footer |
Inline footer below the Bridge submit button (always visible) |
bridgeSign |
info |
Inline gate replacing the Bridge submit button after each click (every time) |
sign |
info |
Inline inside the Before-Sign dialog before the transaction review |
evm-connect |
disclaimer |
Modal overlay after wagmi reports a fresh EVM wallet connection |
The evm-connect gate is enforced — cancelling the modal calls wagmi’s disconnect() (no acknowledgement is recorded), so the gate re-appears on the next connect. bridge and sign render inline inside their host dialogs and acknowledge once. bridgeSign intercepts every Bridge submit click — Continue fires the bridge action, Cancel returns to the form — and is not persisted, so it warns on every attempt. bridgeFooter is purely informational and never blocks an action.
Configuring
Pass notices to WalletUIProvider:
import { WalletUIProvider, type NoticesConfig } from '@txnlab/use-wallet-ui-react'
const notices: NoticesConfig = {
'evm-connect': {
kind: 'disclaimer',
text: (
<>
This app is non-custodial. By connecting an EVM wallet you agree to our{' '}
<a href="/terms">Terms of Service</a> and <a href="/privacy">Privacy Policy</a>.
</>
),
},
bridge: {
kind: 'disclaimer',
text: <>Cross-chain transfers are powered by Allbridge and are irreversible…</>,
},
sign: {
kind: 'info',
title: 'About signing',
body: <>Review the transactions below before approving in your wallet.</>,
},
bridgeFooter: {
kind: 'footer',
text: 'Cross-chain transfers are facilitated by Allbridge, a third-party provider…',
},
}
function Root() {
return (
<WalletProvider manager={walletManager}>
<WalletUIProvider wagmiConfig={wagmiConfig} notices={notices}>
{/* your app */}
</WalletUIProvider>
</WalletProvider>
)
}
text, title, and body accept any ReactNode, so you can embed links, formatted blocks, etc. — content is rendered with the existing --wui-color-* theme variables.
Custom gates in your own UI
The same primitives are re-exported from @txnlab/use-wallet-ui-react for app-side gating:
import { Disclaimer, InfoDialog } from '@txnlab/use-wallet-ui-react'
// Add a config entry under your own id…
const notices: NoticesConfig = {
'risky-action': { kind: 'disclaimer', text: <>This action is irreversible.</> },
}
// …then wrap whatever the gate should guard.
<Disclaimer id="risky-action">
<RiskyActionPanel />
</Disclaimer>
<Disclaimer> and <InfoDialog> both accept an optional className to override the default container styling. With no matching config in notices, both render children directly.
For lower-level control, useNotice(id) returns { config, isAcknowledged, acknowledge, reset } and re-renders on changes from any tab.
Resetting acknowledgements
import { clearAllNoticeAcks, readNoticeAcks, NOTICES_PERSIST_KEY } from '@txnlab/use-wallet-ui-react'
readNoticeAcks() // → { bridge: 1730000000000, sign: …, … }
clearAllNoticeAcks() // wipe every acknowledgement (all gates re-appear)
// Or per-id: useNotice('bridge').reset()
Acknowledgements live in localStorage[NOTICES_PERSIST_KEY] as Record<id, timestamp>; cross-tab updates propagate via the storage event automatically.
Troubleshooting
Buffer errors
Install buffer
pnpm add buffer
# npm install buffer
# yarn add buffer
Add the buffer polyfill:
// vite.config.ts
export default defineConfig({
plugins: [react()],
define: {
global: 'globalThis',
},
resolve: {
alias: {
buffer: 'buffer',
},
},
optimizeDeps: {
include: ['buffer'],
},
})
// At the top of your entry file (e.g. main.tsx)
// Required by the Allbridge bridge SDK
import { Buffer } from 'buffer'
;(globalThis as any).Buffer = Buffer
if (!(globalThis as any).TronWebProto) {
;(globalThis as any).TronWebProto = { Transaction: {} }
}
Package resolution errors
Add deduplication entries:
// vite.config.ts
export default defineConfig({
plugins: [react()],
resolve: {
dedupe: [
'react',
'react-dom',
'@tanstack/react-query',
'@txnlab/use-wallet-react',
'wagmi',
'@wagmi/core',
'@rainbow-me/rainbowkit',
],
},
})