Unified Accounts
Use one 0x-style address for both EVM and Substrate operations on Selendra
Use a single 0x-style Ethereum address for both EVM contracts and native Substrate operations.
The Problem
Most dual-VM chains require two addresses:
- EVM:
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb(H160, 20 bytes) - Substrate:
5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY(AccountId32, 32 bytes)
This creates friction: separate wallets, complex transfers, poor UX.
Selendra's Solution
pallet-unified-accounts creates bidirectional mapping between EVM (H160) and Substrate (AccountId32):
EvmToNative: Map<H160, AccountId32>
NativeToEvm: Map<AccountId32, H160>
Once mapped:
- Same balance and identity across both VMs
- Use MetaMask for Substrate governance
- Use Polkadot.js for EVM contracts
- One address, full chain access
Mapping Methods
1. Default (Automatic)
- Derives H160 from AccountId32 using Blake2-256
- No signature needed
- Best for new users
2. Custom (Bring Your Own)
- Link existing MetaMask address
- Requires EIP-712 signature
- Transfers balance from default address if exists
Cost & Warnings
Fee: 0.01 SEL (one-time, burned to prevent spam)
Permanent: Mappings cannot be changed or deleted
Non-native assets: Mapping only transfers native SEL. Transfer XC20 tokens, staking rewards, etc. BEFORE mapping or lose them forever.
Using Unified Accounts
Method 1: Default Mapping
Via Polkadot.js Apps:
- Go to Developer → Extrinsics
- Select
unifiedAccounts.claimDefaultEvmAddress() - Sign and submit (0.01 SEL fee)
Via Code:
import { ApiPromise, WsProvider } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.selendra.org"),
});
const tx = api.tx.unifiedAccounts.claimDefaultEvmAddress();
await tx.signAndSend(account, ({ status, events }) => {
if (status.isInBlock) {
events.forEach(({ event }) => {
if (api.events.unifiedAccounts.AccountClaimed.is(event)) {
const [accountId, evmAddress] = event.data;
console.log(`Mapped ${accountId} to ${evmAddress}`);
}
});
}
});
Method 2: Custom Mapping
Requirements:
- Substrate account with ≥ 0.01 SEL
- EVM private key
- Neither address has existing mapping
Generate EIP-712 Signature:
import { ethers } from "ethers";
const wallet = new ethers.Wallet(evmPrivateKey);
const domain = {
name: "Selendra EVM Claim",
version: "1",
chainId: 1961,
salt: "0x...", // Genesis block hash from chain state
};
const types = {
Claim: [{ name: "substrateAddress", type: "bytes" }],
};
const signature = await wallet._signTypedData(domain, types, {
substrateAddress: substrateAccount, // 32-byte AccountId
});
Submit Claim:
const tx = api.tx.unifiedAccounts.claimEvmAddress(evmAddress, signature);
await tx.signAndSend(account, ({ status, events }) => {
if (status.isInBlock) {
events.forEach(({ event }) => {
if (api.events.unifiedAccounts.AccountClaimed.is(event)) {
console.log(`✅ Custom mapping created`);
}
});
}
});
Querying Mappings
EVM → Substrate:
const accountId = await api.query.unifiedAccounts.evmToNative(evmAddress);
console.log(accountId.isSome ? accountId.unwrap() : "No mapping");
Substrate → EVM:
const evmAddr = await api.query.unifiedAccounts.nativeToEvm(substrateAccount);
console.log(evmAddr.isSome ? evmAddr.unwrap() : "No mapping");
Use Cases
MetaMask + Governance:
// Deploy with MetaMask
const contract = await factory.deploy();
// Vote with same address via Polkadot.js
await api.tx.democracy.vote(proposalId, vote).signAndSend(account);
Cross-VM Contracts:
contract CrossVM {
function stakeTokens(uint256 amount) public {
STAKING_PRECOMPILE.bond(amount); // Uses unified account
}
}
Troubleshooting
| Error | Solution |
|---|---|
AlreadyMapped | Check existing mappings with query functions |
InvalidSignature | Verify domain (chain ID 1961, genesis hash), message format |
FundsUnavailable | Ensure ≥ 0.01 SEL + existential deposit |
Summary
| Feature | Value |
|---|---|
| Types | Default (auto) or Custom (signed) |
| Fee | 0.01 SEL (burned) |
| Permanence | Cannot change/delete |
| Standard | EIP-712 signatures |
One address for complete chain access—EVM and Substrate together.
Next: Deploy contracts with your unified address
Found an issue or want to contribute?
Help us improve this documentation by editing this page on GitHub.
