Skip to main content

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.
PropertyValue
CurveEd25519
Root WalletSolana
Address Formatbech32 (addr_test for preprod)
HashingBlake2b-224
SerializationCBOR
Smallest UnitLovelace (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
}