Skip to main content

AccountMapping Precompile

Address: 0x0000000000000000000000000000000000000800

The AccountMapping precompile exposes pallet-account-mapping to EVM clients. A single precompile covers five distinct feature areas: EVM↔Substrate account linking, on-chain aliases, chain links, Private Link, and account metadata.


Overview

PropertyValue
Address0x0000000000000000000000000000000000000800
Index2048 (0x800)
Sourceframe/evm/precompile/account-mapping/
Cratepallet-evm-precompile-account-mapping
StatusImplemented (MVP)

Caller Identity

The precompile derives the caller's AccountId32 from the EVM H160 sender via AddressMapping:

AccountId32 = H160 ++ [0x00; 12]

All state-changing calls are signed implicitly by the EVM transaction. The derived AccountId32 is the identity on the Substrate side.


Function Reference

Account Mapping

mapAccount

Emits an AccountMapped event signaling the association between the caller's EVM H160 and their AccountId32. For secp256k1 accounts, this is a pure event — no storage is written, since the mapping is already implicit from the AccountId32 suffix pattern ([H160 | 0x00×12]). Calling it is optional and intended for indexers.

function mapAccount() external;

No arguments required. Reverts with NativeAccountCannotBeMapped if the caller's AccountId32 does not follow the EVM suffix pattern (i.e. bytes 20–31 are not all zero).

Selector: 0xdca49d0e


unmapAccount

Removes the H160 → AccountId32 mapping for the caller.

function unmapAccount() external;

Selector: 0x08f57367


Account Composability Helpers

These read-only functions were added as part of the Account Mapping Alignment Plan (Fase 3) to enable composable EVM interactions involving AccountId32 resolution. All three are pure view functions — no state is mutated and no transaction is required.

isEvmSuffixAccount (read-only)

Checks whether an AccountId32 follows the secp256k1 suffix pattern ([H160 | 0x00×12]). No storage access — O(1) byte inspection.

function isEvmSuffixAccount(bytes32 accountId) external view returns (bool);
ParameterTypeDescription
accountIdbytes32Raw AccountId32 to inspect

Returns true if bytes 20–31 of accountId are all zero (secp256k1 implicit account). Returns false for Sr25519/Ed25519 accounts.

Selector: 0x96e69f8a


toEvmAddress (read-only)

Derives the H160 address from an AccountId32. For secp256k1 accounts (suffix pattern), the derivation is implicit and requires no storage lookup. Returns address(0) for Sr25519/Ed25519 accounts.

function toEvmAddress(bytes32 accountId) external view returns (address);
ParameterTypeDescription
accountIdbytes32Raw AccountId32

Selector: 0x0f5b3052


resolveAliasFull (read-only)

Extended alias resolution that returns the full identity tuple required to interact with BalancesPrecompile. The returned accountId32 is directly usable as the recipient argument in BalancesPrecompile.transfer(bytes32, uint256).

function resolveAliasFull(string calldata alias)
external view
returns (bytes32 accountId32, address evmAddress, bool isEvmSuffix);
ParameterTypeDescription
aliasstringAlias to resolve

Returns (bytes32(0), address(0), false) if the alias is not registered.

For secp256k1 accounts, evmAddress is derived implicitly from accountId32 bytes 0–19 (no storage lookup); isEvmSuffix is true. For accounts where isEvmSuffix is false, evmAddress may be address(0) if no explicit mapping exists.

Selector: 0x2e40772b


Aliases

registerAlias

Reserves a human-readable alias for the caller's account.

function registerAlias(string calldata alias) external;
ParameterTypeDescription
aliasstringUTF-8 alias (max 64 bytes)

Selector: 0x2f8839c3


releaseAlias

Releases the alias owned by the caller.

function releaseAlias() external;

Selector: 0x7fac359e


transferAlias

Transfers ownership of the caller's alias to another address.

function transferAlias(address newOwner) external;
ParameterTypeDescription
newOwneraddressNew owner's EVM address

Selector: 0x5ac998e7


putAliasOnSale

Lists the caller's alias for sale at the given price.

function putAliasOnSale(uint256 price, address[] calldata allowedBuyers) external;
ParameterTypeDescription
priceuint256Sale price in the native token (Planck)
allowedBuyersaddress[]Whitelist of allowed buyers; empty array = public listing

Selector: 0x32091192


cancelSale

Cancels an active alias sale listing.

function cancelSale() external;

Selector: 0x4d023ab9


buyAlias

Purchases an alias that is listed for sale.

function buyAlias(string calldata alias) external;
ParameterTypeDescription
aliasstringAlias to purchase

Selector: 0x1625df3a


resolveAlias (read-only)

Returns the AccountId32 (as bytes32) associated with an alias.

function resolveAlias(string calldata alias) external view returns (bytes32);
ParameterTypeDescription
aliasstringAlias to look up

Selector: 0xd03149ab


getAliasOf (read-only)

Returns the alias registered for a given EVM address as a UTF-8 string.

function getAliasOf(address account) external view returns (string memory);
ParameterTypeDescription
accountaddressEVM address to look up

Selector: 0x7a0ed62c


Chain links associate an Orbinum account with an address on another chain (e.g. Ethereum mainnet, Solana). The link is verified by a signature produced by the external key.

Adds a verified link to an external chain address.

function addChainLink(
uint32 chainId,
bytes calldata externalAddr,
bytes calldata signature
) external;
ParameterTypeDescription
chainIduint32SLIP-0044 chain identifier
externalAddrbytesRaw address bytes on the external chain
signaturebytesSignature produced by the external key over the Orbinum AccountId32

The signature scheme (EIP-191 for EVM chains, Ed25519 for others) is resolved from SupportedChains storage by chainId — it is not passed directly.

Selector: 0x5f3e837c


Removes the chain link for the specified chain.

function removeChainLink(uint32 chainId) external;
ParameterTypeDescription
chainIduint32SLIP-0044 chain identifier of the link to remove

Selector: 0x6f579c0c


dispatchAsLinkedAccount

Dispatches a SCALE-encoded RuntimeCall as the owner account, authorized by the external wallet registered as a chain link. Any EVM address can act as relayer — the relayer pays gas but the call executes with origin = Signed(owner).

function dispatchAsLinkedAccount(
bytes32 owner,
uint32 chainId,
bytes calldata externalAddr,
bytes calldata signature,
bytes calldata call
) external;
ParameterTypeDescription
ownerbytes32Raw AccountId32 of the Orbinum account that owns the chain link
chainIduint32Chain ID of the registered link
externalAddrbytesRaw address bytes of the external wallet (e.g. 20 bytes for EVM)
signaturebytesExternal wallet signature over keccak256(SCALE(call)) (EIP-191, 65 bytes for EVM)
callbytesSCALE-encoded RuntimeCall to dispatch as owner

Selector: 0x0630cef9

Execution flow:

  1. Decodes owner as AccountId32 and call as RuntimeCall (SCALE).
  2. Looks up (chainId, externalAddr) in ReverseChainLinks and verifies it belongs to owner.
  3. Verifies the external wallet's signature over encode(call) using the scheme configured for chainId.
  4. Dispatches call with origin = Signed(owner).

On success, emits ProxyCallExecuted { owner, chain_id, address }.

Signature scheme for EIP-191 chains:

The pallet hashes the payload as follows before verifying the ECDSA signature:

msg_hash   = keccak256(SCALE(call))
final_hash = keccak256("\x19Ethereum Signed Message:\n32" || msg_hash)

In ethers.js, this is equivalent to:

const scaleCallBytes = api.tx.system.remark(new Uint8Array(0)).method.toU8a();
const sig = await wallet.signMessage(ethers.getBytes(ethers.keccak256(scaleCallBytes)));
Native chain ID rejected

Calls using the Orbinum native EVM chain ID (2700) are rejected with UseNativeSignatureForEvmAccounts. Native secp256k1 (MetaMask) accounts sign Substrate extrinsics directly with OrbinumSignature::Ecdsa — no proxy dispatch needed.

Relayer pays gas

The EVM transaction sender (msg.sender) pays the EVM gas cost. The owner account does not need to hold ORB or interact with the chain directly. This enables Ethereum or Solana wallets to control an Orbinum identity via any willing relayer.


Private Link allows an account to prove ownership of an address on another chain without revealing that address publicly. The flow has two phases: register (commit) and reveal (open).

The commitment is computed off-chain as:

commitment = Poseidon(chain_id, owner_account_id, external_address, blinding)

This uses the same Poseidon implementation as the ZK circuits.

Publishes a commitment to a private cross-chain link without revealing the external address.

function registerPrivateLink(
uint32 chainId,
bytes32 commitment
) external;
ParameterTypeDescription
chainIduint32SLIP-0044 chain identifier
commitmentbytes32Poseidon(chain_id, owner_account_id, external_address, blinding)

Selector: 0xc04e98f4


Removes a previously registered commitment.

function removePrivateLink(bytes32 commitment) external;
ParameterTypeDescription
commitmentbytes32Commitment to remove

Selector: 0xdfd8b57e


Reveals the external address behind a commitment by providing the opening.

function revealPrivateLink(
bytes32 commitment,
bytes calldata externalAddr,
bytes32 blinding,
bytes calldata signature
) external;
ParameterTypeDescription
commitmentbytes32Previously registered commitment
externalAddrbytesPlaintext external address
blindingbytes32Blinding factor used to compute the commitment
signaturebytesSignature from the external key over the Orbinum AccountId32

The runtime recomputes Poseidon(chain_id, owner_account_id, externalAddr, blinding) and verifies it matches the registered commitment. If it matches and the signature is valid, the link is promoted to a public chain link.

Selector: 0x4df1f33d


Checks whether a given alias has a private commitment registered for a specific commitment hash.

function hasPrivateLink(
string calldata alias,
bytes32 commitment
) external view returns (bool);
ParameterTypeDescription
aliasstringAlias of the account to query
commitmentbytes32Commitment to look up

Selector: 0x47e05c6c


Account Metadata

setAccountMetadata

Sets up to three metadata fields for the caller's account. Pass empty bytes to leave a field unchanged.

function setAccountMetadata(
bytes calldata displayName,
bytes calldata bio,
bytes calldata avatar
) external;
ParameterTypeMax sizeDescription
displayNamebytes128 bytesUTF-8 display name
biobytes128 bytesUTF-8 short biography
avatarbytes128 bytesURL or IPFS CID

Selector: 0x776cf9ff


Selector Reference

SelectorFunctionType
0xdca49d0emapAccount()write
0x08f57367unmapAccount()write
0x96e69f8aisEvmSuffixAccount(bytes32)read
0x0f5b3052toEvmAddress(bytes32)read
0x2e40772bresolveAliasFull(string)read
0x2f8839c3registerAlias(string)write
0x7fac359ereleaseAlias()write
0x5ac998e7transferAlias(address)write
0x32091192putAliasOnSale(uint256,address[])write
0x4d023ab9cancelSale()write
0x1625df3abuyAlias(string)write
0xd03149abresolveAlias(string)read
0x7a0ed62cgetAliasOf(address)read
0x5f3e837caddChainLink(uint32,bytes,bytes)write
0x6f579c0cremoveChainLink(uint32)write
0x0630cef9dispatchAsLinkedAccount(bytes32,uint32,bytes,bytes,bytes)write
0xc04e98f4registerPrivateLink(uint32,bytes32)write
0xdfd8b57eremovePrivateLink(bytes32)write
0x4df1f33drevealPrivateLink(bytes32,bytes,bytes32,bytes)write
0x47e05c6chasPrivateLink(string,bytes32)read
0x776cf9ffsetAccountMetadata(bytes,bytes,bytes)write

Usage Example

import { ethers } from "ethers";
import { buildPoseidon } from "circomlibjs";

const ADDRESS = "0x0000000000000000000000000000000000000800";

const abi = [
"function registerPrivateLink(uint32 chainId, bytes32 commitment) external",
"function revealPrivateLink(bytes32 commitment, bytes externalAddr, bytes32 blinding, bytes signature) external",
"function hasPrivateLink(string alias, bytes32 commitment) external view returns (bool)",
"function registerAlias(string alias) external",
];

const precompile = new ethers.Contract(ADDRESS, abi, signer);

// Step 1 — register alias
await precompile.registerAlias("alice");

// Step 2 — register a private link to Ethereum mainnet (SLIP-0044: 60)
const poseidon = await buildPoseidon();
const chainId = 60;
const externalAddr = ethers.getBytes("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
const blinding = ethers.randomBytes(32);
const ownerAccountId = ethers.getBytes(substrateAccountId32); // 32 bytes

const raw = poseidon([chainId, ownerAccountId, externalAddr, blinding]);
const commitment = ethers.hexlify(poseidon.F.toObject(raw));

await precompile.registerPrivateLink(chainId, commitment);

// Step 3 — reveal later by providing the opening + signature from the external key
await precompile.revealPrivateLink(commitment, externalAddr, blinding, externalSignature);

Composability: resolving aliases for BalancesPrecompile

resolveAliasFull combined with BalancesPrecompile allows any EVM contract to send ORB to an Orbinum alias without knowing whether the recipient is a secp256k1 or Sr25519 account:

interface IAccountMapping {
function resolveAliasFull(string calldata alias)
external view
returns (bytes32 accountId32, address evmAddress, bool isEvmSuffix);
}

interface IBalances {
function transfer(bytes32 accountId32, uint256 amount) external;
}

address constant ACCOUNT_MAPPING = 0x0000000000000000000000000000000000000800;
address constant BALANCES = 0x0000000000000000000000000000000000000802;

function sendToAlias(string calldata alias, uint256 amount) external {
(bytes32 accountId32, , ) =
IAccountMapping(ACCOUNT_MAPPING).resolveAliasFull(alias);
require(accountId32 != bytes32(0), "alias not found");
IBalances(BALANCES).transfer(accountId32, amount);
}

This pattern works for both secp256k1 and Sr25519 recipients. accountId32 is passed directly to transfer without additional conversion.


Notes

  • Read-only functions (resolveAlias, getAliasOf, hasPrivateLink, isEvmSuffixAccount, toEvmAddress, resolveAliasFull) do not require a signed transaction and can be called with eth_call.
  • All write functions consume gas proportional to the underlying Substrate extrinsic weight via GasWeightMapping.
  • The Private Link commitment uses Poseidon (BN254 scalar field) — not keccak256. Use circomlibjs or @orbinum/proof-generator to compute it off-chain.