Documentation

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:

  1. Go to Developer → Extrinsics
  2. Select unifiedAccounts.claimDefaultEvmAddress()
  3. 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

ErrorSolution
AlreadyMappedCheck existing mappings with query functions
InvalidSignatureVerify domain (chain ID 1961, genesis hash), message format
FundsUnavailableEnsure ≥ 0.01 SEL + existential deposit

Summary

FeatureValue
TypesDefault (auto) or Custom (signed)
Fee0.01 SEL (burned)
PermanenceCannot change/delete
StandardEIP-712 signatures

One address for complete chain access—EVM and Substrate together.


Next: Deploy contracts with your unified address

Contribute

Found an issue or want to contribute?

Help us improve this documentation by editing this page on GitHub.

Edit this page on GitHub
Selendra - Build on Cambodian Blockchain with Testnet