Tier 2 chains reuse existing Dynamic embedded wallet keys to produce valid addresses and signatures on additional chains — no new wallets or user re-authentication required.
Dynamic supports three embedded wallets: an EVM wallet (secp256k1), a Solana wallet (Ed25519), and a Bitcoin wallet (secp256k1/BIP-340). Because many blockchains share these same elliptic curves, you can derive addresses and sign transactions on additional chains without creating new keys.
| Chain | Curve | Root Wallet | Address Format |
|---|
| Aptos | Ed25519 | Solana | 0x + SHA3-256 hex |
| Algorand | Ed25519 | Solana | base32(pubkey ‖ checksum) |
| Cosmos | secp256k1 | EVM | bech32 (cosmos) |
| Starknet | secp256k1 → Stark | EVM | Pedersen contract address |
| Tron | secp256k1 | EVM | base58check (T) |
| NEAR | Ed25519 | Solana | 64-char hex |
| Cardano | Ed25519 | Solana | bech32 (addr_test) |
| Mavryk | Ed25519 | Solana | base58check (mv1) |
| XRP | secp256k1 | EVM | base58check with Ripple alphabet |
Tier 2 chains are not natively represented in Dynamic’s UI or session model. You are responsible for managing the user-facing experience: displaying derived addresses, associating them with your user records, and handling any chain-specific session or account state yourself.
Exporting the root wallet key exposes all derived addresses. The EVM and Solana embedded wallet keys are the cryptographic root of every address you derive from them. Never expose or transmit root wallet keys unless you have explicitly designed for that use case.
Setup
Create both EVM and Solana embedded wallets during app initialization:
const wallets = dynamicClient.wallets.userWallets;
if (!wallets.find(w => w.chain === "EVM")) {
await dynamicClient.wallets.embedded.createWallet({ chain: "Evm" });
}
if (!wallets.find(w => w.chain === "SOL")) {
await dynamicClient.wallets.embedded.createWallet({ chain: "Sol" });
}
Install dependencies
npm install @noble/curves @noble/hashes bs58 bech32 @scure/starknet
Signing APIs
Ed25519 chains (Solana wallet)
Use dynamicClient.wallets.signMessage for all Ed25519-based derived chains (Aptos, Algorand, NEAR, Cardano, Mavryk):
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
const { signedMessage } = await dynamicClient.wallets.signMessage({
wallet,
message: hexDigest, // hex-encoded bytes to sign
});
// signedMessage is base58/base64/hex encoded — use decodeSig() to normalize
secp256k1 chains (EVM wallet)
Use dynamicClient.wallets.waas.signRawMessage for all secp256k1-based derived chains (Cosmos, Tron, Starknet, XRP):
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: hexDigest, // 32-byte hex digest (no 0x prefix)
});
// Returns hex string — may have 0x prefix, strip if needed
Shared utilities
These helpers are used across all chain implementations:
import bs58 from "bs58";
function strip0x(hex: string): string {
return hex.startsWith("0x") ? hex.slice(2) : hex;
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
}
function hexToBytes(hex: string): Uint8Array {
const clean = strip0x(hex);
const bytes = new Uint8Array(clean.length / 2);
for (let i = 0; i < clean.length; i += 2) {
bytes[i / 2] = parseInt(clean.slice(i, i + 2), 16);
}
return bytes;
}
function concatBytes(...arrays: Uint8Array[]): Uint8Array {
const total = arrays.reduce((s, a) => s + a.length, 0);
const result = new Uint8Array(total);
let offset = 0;
for (const arr of arrays) { result.set(arr, offset); offset += arr.length; }
return result;
}
// Convert Solana base58 address → 32-byte Ed25519 public key
function solanaAddressToPubkey(solanaAddress: string): Uint8Array {
return bs58.decode(solanaAddress);
}
// Decode an Ed25519 signature from hex (128 chars), base64, or base58
function decodeSig(sig: string): Uint8Array {
const clean = sig.startsWith("0x") ? sig.slice(2) : sig.trim();
if (/^[0-9a-fA-F]+$/.test(clean) && clean.length === 128) {
return hexToBytes(clean);
}
try {
const bin = atob(clean);
if (bin.length === 64) {
const bytes = new Uint8Array(64);
for (let i = 0; i < 64; i++) bytes[i] = bin.charCodeAt(i);
return bytes;
}
} catch { /* not base64 */ }
return bs58.decode(clean);
}
secp256k1 public key recovery
secp256k1 chains (Cosmos, XRP, Starknet) require the compressed public key, which must be recovered from the EVM wallet by signing a known message:
import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha2";
async function recoverEvmPubkey(label: string): Promise<Uint8Array> {
const msgHash = sha256(new TextEncoder().encode(label)) as Uint8Array;
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: bytesToHex(msgHash),
});
const sigBytes = hexToBytes(signature.replace(/^0x/, ""));
const rsHex = bytesToHex(sigBytes.slice(0, 64));
let v = sigBytes[64];
if (v >= 27) v -= 27;
const sig = secp256k1.Signature.fromHex(rsHex).addRecoveryBit(v);
return sig.recoverPublicKey(msgHash).toBytes(true) as Uint8Array;
}
Cache the result per session — recovery only needs to happen once.
Working example
The rn-multichain-demo shows all chains working end-to-end in a single Expo app.
For full per-chain implementation details (transaction serialization, RPC calls, balance queries), see the JavaScript reference for chains with Tier 2 support.