Selendra

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