Overview
Cardano uses the Ed25519 elliptic curve — the same as Solana. You derive a Cardano enterprise address by hashing the Solana wallet’s public key with Blake2b-224 and encoding with bech32. Transactions use CBOR encoding and are submitted via the Blockfrost API.
| Property | Value |
|---|
| Curve | Ed25519 |
| Root Wallet | Solana |
| Address Format | bech32 (addr_test for preprod) |
| Hashing | Blake2b-224 |
| Serialization | CBOR |
| Smallest Unit | Lovelace (1 ADA = 10^6 lovelace) |
Dependencies
npm install @noble/curves @noble/hashes bs58 bech32
Derive Address
Decode the Solana base58 address to a 32-byte Ed25519 public key, hash with Blake2b-224, prepend the enterprise address header byte, and bech32-encode:
import { blake2b } from "@noble/hashes/blake2";
import { bech32 } from "bech32";
import bs58 from "bs58";
function deriveCardanoAddress(solanaAddress: string): string {
const pubkey = bs58.decode(solanaAddress);
const keyHash = blake2b(pubkey, { dkLen: 28 }); // 28-byte payment key hash
const payload = new Uint8Array(29);
payload[0] = 0x60; // enterprise address, preprod testnet
payload.set(keyHash, 1);
return bech32.encode("addr_test", bech32.toWords(payload), 108);
}
The header byte 0x60 indicates an enterprise address on preprod testnet. For mainnet, use 0x61 with the addr bech32 prefix.
Sign a Message
Sign the raw UTF-8 bytes of the message using the Solana wallet:
import { signMessage } from "@dynamic-labs-sdk/client";
import type { WalletAccount } from "@dynamic-labs-sdk/client";
async function signCardanoMessage(
message: string,
solWallet: WalletAccount,
): Promise<string> {
const messageBytes = new TextEncoder().encode(message);
const result = await signMessage({
walletAccount: solWallet,
message: bytesToHex(messageBytes),
});
return result.signature;
}
Verify a Signature
Verify the Ed25519 signature against the raw message bytes:
import { ed25519 } from "@noble/curves/ed25519";
function verifyCardanoSignature(
message: string,
signature: string,
solanaAddress: string,
): boolean {
const pubkey = bs58.decode(solanaAddress);
const messageBytes = new TextEncoder().encode(message);
const sigBytes = decodeSig(signature);
return ed25519.verify(sigBytes, messageBytes, pubkey);
}
Check Balance
Query the ADA balance via the Blockfrost API:
async function getCardanoBalance(cardanoAddress: string): Promise<string> {
const res = await fetch(
`https://cardano-preprod.blockfrost.io/api/v0/addresses/${cardanoAddress}`,
{ headers: { project_id: "YOUR_BLOCKFROST_KEY" } },
);
if (res.status === 404) return "0";
if (!res.ok) throw new Error(`Blockfrost error: ${res.status}`);
const data = await res.json();
const lovelace = data.amount?.find(
(a: { unit: string; quantity: string }) => a.unit === "lovelace",
);
if (!lovelace) return "0";
return (Number(lovelace.quantity) / 1_000_000).toString();
}
Send a Transfer
Build a CBOR-encoded transaction, sign with the Solana wallet, and submit via Blockfrost.
CBOR Encoding Primitives
function cborHeader(majorType: number, value: number): Uint8Array {
const mt = majorType << 5;
if (value < 24) return new Uint8Array([mt | value]);
if (value < 0x100) return new Uint8Array([mt | 24, value]);
if (value < 0x10000)
return new Uint8Array([mt | 25, (value >> 8) & 0xff, value & 0xff]);
if (value < 0x100000000)
return new Uint8Array([
mt | 26,
(value >>> 24) & 0xff, (value >>> 16) & 0xff,
(value >>> 8) & 0xff, value & 0xff,
]);
const high = Math.floor(value / 0x100000000);
const low = value % 0x100000000;
return new Uint8Array([
mt | 27,
(high >>> 24) & 0xff, (high >>> 16) & 0xff,
(high >>> 8) & 0xff, high & 0xff,
(low >>> 24) & 0xff, (low >>> 16) & 0xff,
(low >>> 8) & 0xff, low & 0xff,
]);
}
function cborUint(n: number): Uint8Array { return cborHeader(0, n); }
function cborBytes(data: Uint8Array): Uint8Array {
return concatBytes(cborHeader(2, data.length), data);
}
function cborArray(items: Uint8Array[]): Uint8Array {
return concatBytes(cborHeader(4, items.length), ...items);
}
function cborMap(entries: [Uint8Array, Uint8Array][]): Uint8Array {
const parts: Uint8Array[] = [cborHeader(5, entries.length)];
for (const [k, v] of entries) parts.push(k, v);
return concatBytes(...parts);
}
function cborTag258(inner: Uint8Array): Uint8Array {
return concatBytes(new Uint8Array([0xd9, 0x01, 0x02]), inner);
}
Full Transfer Implementation
import { blake2b } from "@noble/hashes/blake2";
import { signMessage } from "@dynamic-labs-sdk/client";
async function sendCardanoTransfer(
recipient: string,
amountAda: number,
cardanoAddress: string,
solWallet: WalletAccount,
): Promise<string> {
const amountLovelace = BigInt(Math.round(amountAda * 1_000_000));
// Fetch UTXOs, protocol params, and latest slot in parallel
const [utxos, params, currentSlot] = await Promise.all([
fetchUtxos(cardanoAddress),
fetchProtocolParams(),
fetchLatestSlot(),
]);
const ttl = currentSlot + 7200; // ~2 hours
// Select UTXOs to cover amount + estimated fee
const estimatedFee = BigInt(params.min_fee_a) * BigInt(300) + BigInt(params.min_fee_b);
const { selected, totalLovelace } = selectUtxos(utxos, amountLovelace + estimatedFee);
// Build inputs and outputs
const inputs = selected.map(u => ({
txHash: hexToBytes(u.tx_hash),
index: u.output_index,
}));
const senderBytes = addressToBytes(cardanoAddress);
const recipientBytes = addressToBytes(recipient);
const change = totalLovelace - amountLovelace - estimatedFee;
const outputs = [
{ address: recipientBytes, lovelace: amountLovelace },
];
if (change >= BigInt(1_000_000)) {
outputs.push({ address: senderBytes, lovelace: change });
}
// Build transaction body CBOR
const txBody = buildTxBodyCbor(inputs, outputs, estimatedFee, ttl);
// Hash the transaction body
const txBodyHash = blake2b(txBody, { dkLen: 32 });
// Sign via SOL wallet
const signResult = await signMessage({
walletAccount: solWallet,
message: bytesToHex(txBodyHash),
});
const sigBytes = decodeSig(signResult.signature);
// Build witness set
const pubkey = bs58.decode(solWallet.address);
const witnessCbor = cborMap([
[cborUint(0), cborTag258(
cborArray([cborArray([cborBytes(pubkey), cborBytes(sigBytes)])])
)],
]);
// Assemble full transaction: [body, witness, true, null]
const CBOR_TRUE = new Uint8Array([0xf5]);
const CBOR_NULL = new Uint8Array([0xf6]);
const fullTx = cborArray([txBody, witnessCbor, CBOR_TRUE, CBOR_NULL]);
// Submit to Blockfrost
const res = await fetch(
"https://cardano-preprod.blockfrost.io/api/v0/tx/submit",
{
method: "POST",
headers: {
"Content-Type": "application/cbor",
project_id: "YOUR_BLOCKFROST_KEY",
},
body: fullTx,
},
);
if (!res.ok) throw new Error(`Tx submission failed: ${res.status}`);
return res.json(); // returns tx hash
}