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 |
| Cosmos | secp256k1 | EVM | bech32 (cosmos) |
| 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:
final wallets = DynamicSDK.instance.wallets.userWallets;
if (!wallets.any((w) => w.chain == 'EVM')) {
await DynamicSDK.instance.wallets.embedded.createWallet(chain: EmbeddedWalletChain.evm);
}
if (!wallets.any((w) => w.chain == 'SOL')) {
await DynamicSDK.instance.wallets.embedded.createWallet(chain: EmbeddedWalletChain.sol);
}
Install dependencies
Add the following to your pubspec.yaml:
dependencies:
# Cryptography
pointycastle: ^3.9.1 # secp256k1, SHA-256, SHA-3, Keccak, RIPEMD-160
hashlib: ^1.19.2 # Blake2b with configurable output size
# Encoding
bs58: ^1.0.2 # Base58 encoding (Tron, Ripple, Mavryk, Solana)
bech32: ^0.2.2 # Bech32 encoding (Cosmos, Cardano)
convert: ^3.1.1 # Hex encoding/decoding
# HTTP
http: ^1.2.2 # HTTP client for RPC calls
Then run:
Signing APIs
Ed25519 chains (Solana wallet)
Use the Solana signer for all Ed25519-based derived chains (Aptos, NEAR, Cardano, Mavryk):
final solWallet = DynamicSDK.instance.wallets.userWallets
.firstWhere((w) => w.chain == 'SOL');
final signer = DynamicSDK.instance.solana.createSigner(wallet: solWallet);
final signature = await signer.signMessage(message: hexDigest);
// signature is a hex, base64, or base58 string — normalize before use
secp256k1 chains (EVM wallet)
Use signRawMessage for all secp256k1-based derived chains (Cosmos, Tron, XRP):
final evmWallet = DynamicSDK.instance.wallets.userWallets
.firstWhere((w) => w.chain == 'EVM');
final signature = await DynamicSDK.instance.wallets.signRawMessage(
walletId: evmWallet.id,
accountAddress: evmWallet.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. Add them to a utils/bytes.dart file in your project:
import 'dart:typed_data';
import 'package:bs58/bs58.dart';
String strip0x(String hex) => hex.startsWith('0x') ? hex.substring(2) : hex;
String bytesToHex(Uint8List bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
Uint8List hexToBytes(String hex) {
final clean = strip0x(hex);
final result = Uint8List(clean.length ~/ 2);
for (var i = 0; i < clean.length; i += 2) {
result[i ~/ 2] = int.parse(clean.substring(i, i + 2), radix: 16);
}
return result;
}
Uint8List concatBytes(List<Uint8List> arrays) {
final total = arrays.fold(0, (s, a) => s + a.length);
final result = Uint8List(total);
var offset = 0;
for (final arr in arrays) {
result.setAll(offset, arr);
offset += arr.length;
}
return result;
}
// Convert Solana base58 address to 32-byte Ed25519 public key
Uint8List solanaAddressToPubkey(String solanaAddress) {
return Uint8List.fromList(base58.decode(solanaAddress));
}
// Decode an Ed25519 signature from hex (128 chars), base64, or base58
Uint8List decodeSig(String sig) {
final clean = sig.startsWith('0x') ? sig.substring(2) : sig.trim();
if (RegExp(r'^[0-9a-fA-F]+$').hasMatch(clean) && clean.length == 128) {
return hexToBytes(clean);
}
try {
import 'dart:convert';
final bin = base64.decode(clean);
if (bin.length == 64) return Uint8List.fromList(bin);
} catch (_) {}
return Uint8List.fromList(base58.decode(clean));
}
secp256k1 public key recovery
secp256k1 chains (Cosmos, XRP, Tron) require the compressed public key. You recover it once per session by signing a known message with the EVM wallet and running ecrecover:
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
import 'package:pointycastle/ecc/ecc_fp.dart' as fp;
/// Recover the compressed secp256k1 public key from an EVM wallet.
/// [recoveryMessage] is chain-specific (e.g. 'COSMOS_PUBKEY_RECOVERY').
/// [signEip191] is a callback that signs via the Dynamic SDK EIP-191 flow.
/// [evmAddress] is used to validate the recovered key matches the wallet.
Future<Uint8List> recoverEvmPublicKey({
required String recoveryMessage,
required Future<String> Function(String) signEip191,
required String evmAddress,
}) async {
// signEip191 signs: keccak256("\x19Ethereum Signed Message:\n" + len + message)
final signature = await signEip191(recoveryMessage);
final prefix = '\x19Ethereum Signed Message:\n${recoveryMessage.length}';
final combined = concatBytes([
Uint8List.fromList(prefix.codeUnits),
Uint8List.fromList(recoveryMessage.codeUnits),
]);
final msgHash = Digest('Keccak/256').process(combined);
final sigBytes = hexToBytes(strip0x(signature));
final r = BigInt.parse(bytesToHex(sigBytes.sublist(0, 32)), radix: 16);
final s = BigInt.parse(bytesToHex(sigBytes.sublist(32, 64)), radix: 16);
var v = sigBytes[64];
if (v >= 27) v -= 27;
final recovered = _ecRecover(msgHash, r, s, v);
if (recovered == null) throw Exception('Public key recovery failed');
final xBytes = _bigIntToBytes(recovered.x!.toBigInteger()!, 32);
final prefix02or03 = recovered.y!.toBigInteger()!.isOdd ? 0x03 : 0x02;
return Uint8List.fromList([prefix02or03, ...xBytes]);
}
Cache the result per session — recovery only needs to happen once.
Working example
The flutter-multichain-demo shows all chains working end-to-end in a single Flutter app.
For full per-chain implementation details (transaction serialization, RPC calls, balance queries), see the JavaScript reference for chains with Tier 2 support.