Skip to main content

Overview

Dynamic embedded wallets give you cryptographic key material for two curves:
  • secp256k1 via the EVM wallet — used by Bitcoin, Ethereum, Tron, Cosmos, XRP, Starknet, and many others
  • Ed25519 via the Solana wallet — used by Solana, NEAR, Cardano, Aptos, Mavryk, Algorand, and others
Any blockchain that uses one of these curves can be supported without creating new wallets or prompting the user to re-authenticate. This page explains how to wire up a new chain yourself.

Decision: which root wallet to use?

Your chain usesRoot walletSigning method
secp256k1EVM walletDynamicSDK.instance.wallets.signRawMessage(...)
Ed25519Solana walletDynamicSDK.instance.solana.createSigner(wallet: ...).signMessage(...)
If your chain uses a derived curve (e.g., Starknet derives its Stark key from secp256k1), start from the EVM wallet and apply the key-derivation step locally in Dart.

Step 1: Add Dart crypto dependencies

Choose the packages you need based on the chain’s address encoding and hash functions:
# pubspec.yaml
dependencies:
  # Core cryptography (covers most chains)
  pointycastle: ^3.9.1     # secp256k1, SHA-256, SHA-3, Keccak, RIPEMD-160

  # Extended hashing
  hashlib: ^1.19.2         # Blake2b (Cardano, Mavryk, NEAR transactions)

  # Encoding
  bs58: ^1.0.2             # Base58 / base58check (Bitcoin, Tron, XRP, Mavryk, Solana)
  bech32: ^0.2.2           # Bech32 (Cosmos, Cardano)
  convert: ^3.1.1          # Hex utilities

  # Networking
  http: ^1.2.2
Run flutter pub get after updating pubspec.yaml.

Step 2: Derive the address

secp256k1 chains

secp256k1 chains need the compressed 33-byte public key. You recover it once per session by signing a deterministic message 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;

// Cache this per session — recovery only needs to happen once
Uint8List? _cachedPubkey;

Future<Uint8List> getEvmCompressedPubkey() async {
  if (_cachedPubkey != null) return _cachedPubkey!;

  const recoveryMessage = 'MY_CHAIN_PUBKEY_RECOVERY';
  final evmWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'EVM');

  // Sign EIP-191 prefixed message to get a recoverable signature
  final signature = await DynamicSDK.instance.wallets.signMessage(
    walletId: evmWallet.id,
    accountAddress: evmWallet.address,
    message: recoveryMessage,
  );

  // Reconstruct the hash that was actually signed
  final prefix = '\x19Ethereum Signed Message:\n${recoveryMessage.length}';
  final combined = Uint8List.fromList([...prefix.codeUnits, ...recoveryMessage.codeUnits]);
  final msgHash = Digest('Keccak/256').process(combined);

  // ecrecover → compressed pubkey
  final sigBytes = hexToBytes(strip0x(signature));
  var v = sigBytes[64];
  if (v >= 27) v -= 27;
  _cachedPubkey = _ecRecover(
    msgHash,
    BigInt.parse(bytesToHex(sigBytes.sublist(0, 32)), radix: 16),
    BigInt.parse(bytesToHex(sigBytes.sublist(32, 64)), radix: 16),
    v,
  );
  return _cachedPubkey!;
}
Once you have the compressed public key, derive the address using your chain’s rules (SHA-256 + RIPEMD-160 for Bitcoin/Cosmos, keccak for Ethereum/Tron, etc.).

Ed25519 chains

Ed25519 chains use the Solana wallet’s public key directly. The Solana address is base58 encoding of the 32-byte Ed25519 public key:
import 'package:bs58/bs58.dart';

Uint8List getSolanaPublicKey() {
  final solWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'SOL');
  return Uint8List.fromList(base58.decode(solWallet.address));
}

Step 3: Sign payloads

The signing flow is the same for all chains on the same curve — only the hash/serialization before calling the SDK differs.

secp256k1 signing

Future<String> signWithEvmWallet(Uint8List digest32) async {
  final evmWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'EVM');

  // digest32 must be exactly 32 bytes, passed as hex without 0x
  final signature = await DynamicSDK.instance.wallets.signRawMessage(
    walletId: evmWallet.id,
    accountAddress: evmWallet.address,
    message: bytesToHex(digest32),
  );

  // Returns 65-byte hex (r + s + v) — strip 0x prefix if present
  return strip0x(signature);
}
Many chains require low-S normalization. Apply this before encoding the signature:
final _secp256k1N = BigInt.parse(
  'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141',
  radix: 16,
);

BigInt normalizeLowS(BigInt s) {
  final halfN = _secp256k1N >> 1;
  return s > halfN ? _secp256k1N - s : s;
}

Ed25519 signing

Future<Uint8List> signWithSolanaWallet(String hexDigest) async {
  final solWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'SOL');

  final signer = DynamicSDK.instance.solana.createSigner(wallet: solWallet);
  final result = await signer.signMessage(message: hexDigest);

  // The result may be hex, base64, or base58 — normalize:
  return decodeSig(result);
}
The decodeSig helper handles all three encodings:
import 'dart:convert';
import 'package:bs58/bs58.dart';

Uint8List decodeSig(String sig) {
  final clean = sig.startsWith('0x') ? sig.substring(2) : sig.trim();
  if (RegExp(r'^[0-9a-fA-F]{128}$').hasMatch(clean)) return hexToBytes(clean);
  try {
    final bin = base64.decode(clean);
    if (bin.length == 64) return Uint8List.fromList(bin);
  } catch (_) {}
  return Uint8List.fromList(base58.decode(clean));
}

Step 4: Query balances and broadcast transactions

Use package:http to call the chain’s RPC or REST API. The signing flow is chain-agnostic; only the serialization and endpoint differ.
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<Map<String, dynamic>> jsonRpc(
  String url,
  String method,
  dynamic params,
) async {
  final res = await http.post(
    Uri.parse(url),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}),
  );
  final data = json.decode(res.body) as Map<String, dynamic>;
  if (data['error'] != null) throw Exception(data['error']['message']);
  return data['result'] as Map<String, dynamic>;
}

Shared byte utilities

Add these to lib/utils/bytes.dart:
import 'dart:typed_data';

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;
}

Checklist for a new chain

  1. Identify the curve (secp256k1 or Ed25519) and the root wallet to use
  2. Look up the address encoding (bech32, base58check, hex, etc.) in the chain’s spec
  3. Implement deriveAddress(String rootWalletAddress) → String
  4. Implement digestForSigning(String message) → String (hex of hash)
  5. Call signWithEvmWallet or signWithSolanaWallet with the digest
  6. Post-process the signature if needed (low-S normalization, DER encoding, etc.)
  7. Serialize and broadcast the transaction via the chain’s RPC
See the existing chain implementations for complete examples: