One ledger, two doors. DMS can move as a native balances extrinsic or as an EVM value transfer, and both end up in the same state. Here is the same transfer written three ways, with the trade-offs.
1. Native, with Polkadot.js API
import { ApiPromise, WsProvider, Keyring } from "@polkadot/api";
const api = await ApiPromise.create({
provider: new WsProvider("wss://rpc.testnet.empooriochain.org"),
});
const keyring = new Keyring({ type: "sr25519", ss58Format: 2026 });
const sender = keyring.addFromUri(process.env.SENDER_SEED!);
const amount = 10n * 10n ** 18n; // 10 DMS, 18 decimals
const tx = api.tx.balances.transferKeepAlive(RECIPIENT_SS58, amount);
const unsub = await tx.signAndSend(sender, ({ status, dispatchError }) => {
if (dispatchError) throw new Error(dispatchError.toString());
if (status.isInBlock) console.log("included in", status.asInBlock.toHex());
if (status.isFinalized) { console.log("finalized"); unsub(); }
});
Polkadot.js reads the runtime metadata at connection time, so it encodes the two EmpoorioChain-specific signed extensions (CheckPqcPolicy, CheckDormantAccount) without you doing anything. Note the two callbacks: included is not finalized, and a payment UI should show them differently. transferKeepAlive refuses to drop the sender below the existential deposit; transferAllowDeath does not.
Cost on the testnet snapshot: 0.000025 DMS.
2. Native, with the Rust SDK
use empoorio_sdk::{EmpoorioClient, SdkConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cfg = SdkConfig::new("wss://rpc.testnet.empooriochain.org");
let client = EmpoorioClient::connect(cfg).await?;
let signer = empoorio_sdk::signer::from_seed(std::env::var("SENDER_SEED")?)?;
let receipt = client
.balances()
.transfer_keep_alive(&signer, RECIPIENT, 10 * 10u128.pow(18))
.await?; // waits for finality
println!("finalized in block {}", receipt.block_hash);
Ok(())
}
The SDK's typed clients hide SCALE encoding and metadata; for pallets without a typed client it exposes dynamic Subxt calls. If you compile against your own metadata.scale, take it from a node at the current spec version — the metadata changes in every runtime upgrade.
3. EVM, with viem
import { createWalletClient, http, parseEther, defineChain } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const empooriochain = defineChain({
id: 2026,
name: "EmpoorioChain Testnet",
nativeCurrency: { name: "DRACMA", symbol: "DMS", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.testnet.empooriochain.org"] } },
blockExplorers: { default: { name: "EmpooScan", url: "https://empooscan.com" } },
});
const client = createWalletClient({
chain: empooriochain,
transport: http(),
account: privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`),
});
const hash = await client.sendTransaction({
to: "0xRecipient…",
value: parseEther("10"),
});
The chain id must be 2026: under EIP-155 it is inside the signature, and a wrong id yields a transaction the node rejects. Gas is 1 gwei since runtime 220. The EVM address maps to a native account deterministically, so a native transfer to that account and an EVM transfer to the H160 land in the same balance.
Which door
| Native | EVM | |
|---|---|---|
| Tooling | Polkadot.js, Rust SDK, Dart SDK, Eoonia | MetaMask, Hardhat, viem, ethers |
| Fee model | Weight-based, ~0.000025 DMS per transfer | EIP-1559, 1 gwei base |
| Reaches native pallets (staking, DEX, identity, NFTs) | Directly | Through precompiles 0xAA, 0x72, 0x1155 or not at all |
| Signed extensions | Must encode CheckPqcPolicy (Polkadot.js and the SDKs do) | Standard Ethereum signature |
Use native when you need the chain's own features; use the EVM when you are bringing Ethereum tooling or contracts. Verify either on EmpooScan — and for a contract deployment on the EVM side, verify by nonce rather than eth_getCode(…, 'latest'), which was found to return 0x for deployed contracts at latest on runtime 213.
Transfer fee from FEE_MODEL_AND_LOW_COST_STRATEGY.md; chain parameters from RED.json; extension encoding from FORMATO_DE_EXTRINSECOS.md.


