Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Cosmos SDK chains use the secp256k1 elliptic curve — the same as Ethereum. You recover the compressed public key from the EVM wallet, then derive bech32-encoded addresses. The same key works across all Cosmos chains by changing the bech32 prefix (e.g., cosmos for Cosmos Hub, osmo for Osmosis).
| Property | Value |
|---|
| Curve | secp256k1 |
| Root Wallet | EVM |
| Address Format | bech32 (prefix varies by chain) |
| Hashing | SHA-256 + RIPEMD-160 |
| Serialization | Amino JSON (sign) / Protobuf (broadcast) |
| Smallest Unit | Varies (e.g., uatom for Cosmos Hub) |
Dependencies
npm install @noble/curves @noble/hashes bech32
Derive Address
Recover the compressed secp256k1 public key from the EVM wallet, then compute RIPEMD-160(SHA-256(pubkey)) and bech32-encode:
import { sha256 } from "@noble/hashes/sha2";
import { ripemd160 } from "@noble/hashes/legacy";
import { bech32 } from "bech32";
function addressFromPubkey(compressedPubkey: Uint8Array, prefix: string): string {
const hash160 = ripemd160(sha256(compressedPubkey));
return bech32.encode(prefix, bech32.toWords(hash160));
}
async function deriveCosmosAddress(bech32Prefix: string = "cosmos"): Promise<{
address: string;
publicKey: string;
}> {
const compressedPubkey = await recoverEvmPubkey("COSMOS_PUBKEY_RECOVERY");
return {
address: addressFromPubkey(compressedPubkey, bech32Prefix),
publicKey: bytesToHex(compressedPubkey),
};
}
Cache the compressedPubkey — key recovery only needs to happen once per session.
Sign a Message
import { sha256 } from "@noble/hashes/sha2";
async function signCosmosMessage(
message: string,
displayDenom: string = "ATOM",
): Promise<string> {
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const messageBytes = new TextEncoder().encode(message);
const prefix = new TextEncoder().encode(`\x19${displayDenom} Signed Message:\n`);
const lengthStr = new TextEncoder().encode(String(messageBytes.length));
const digest = sha256(concatBytes(prefix, lengthStr, messageBytes));
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: bytesToHex(digest),
});
return signature;
}
Sign a Transaction
Build an Amino JSON sign document, sign the SHA-256 hash, normalize to low-S, then encode in Protobuf for broadcasting:
import { sha256 } from "@noble/hashes/sha2";
async function signCosmosTransaction(signDoc: object): Promise<Uint8Array> {
const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "EVM")!;
const signDocBytes = new TextEncoder().encode(JSON.stringify(signDoc));
const digest = sha256(signDocBytes);
const signature = await dynamicClient.wallets.waas.signRawMessage(wallet.id, {
accountAddress: wallet.address,
message: bytesToHex(digest),
});
// Normalize to low-S (required by Cosmos SDK)
const sigRaw = hexToBytes(signature.replace(/^0x/, ""));
const SECP256K1_N = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");
const HALF_N = SECP256K1_N / BigInt(2);
const s = BigInt("0x" + bytesToHex(sigRaw.slice(32, 64)));
const sNorm = s > HALF_N ? SECP256K1_N - s : s;
const result = new Uint8Array(64);
result.set(sigRaw.slice(0, 32));
const sBytes = hexToBytes(sNorm.toString(16).padStart(64, "0"));
result.set(sBytes, 32);
return result;
}
For full transaction serialization (Protobuf encoding, broadcasting) see the JavaScript Cosmos guide. The only difference in React Native is the signing call.
Verify a Signature
import { secp256k1 } from "@noble/curves/secp256k1";
import { sha256 } from "@noble/hashes/sha2";
function verifyCosmosSignature(
message: string,
signature: string,
publicKey: string,
displayDenom: string = "ATOM",
): boolean {
const messageBytes = new TextEncoder().encode(message);
const prefix = new TextEncoder().encode(`\x19${displayDenom} Signed Message:\n`);
const lengthStr = new TextEncoder().encode(String(messageBytes.length));
const digest = sha256(concatBytes(prefix, lengthStr, messageBytes));
const sigBytes = hexToBytes(signature.replace(/^0x/, ""));
const rsHex = bytesToHex(sigBytes.slice(0, 64));
let v = sigBytes[64];
if (v >= 27) v -= 27;
try {
const sig = secp256k1.Signature.fromHex(rsHex).addRecoveryBit(v);
return sig.recoverPublicKey(digest).toHex(true) === publicKey.toLowerCase();
} catch {
return false;
}
}
Check Balance
async function getCosmosBalance(address: string, lcdUrl: string, denom: string): Promise<string> {
const res = await fetch(`${lcdUrl}/cosmos/bank/v1beta1/balances/${address}`);
if (!res.ok) return "0";
const data = await res.json();
const coin = (data.balances || []).find((b: { denom: string }) => b.denom === denom);
if (!coin) return "0";
return (Number(BigInt(coin.amount)) / 1e6).toString();
}