Skip to main content

Overview

Mavryk (formerly Tezos) uses the Ed25519 elliptic curve — the same as Solana. Addresses are derived with Blake2b-160 hashing and base58check-encoded with the mv1 prefix. Transactions are forged via RPC and injected directly — no Taquito dependency required.
PropertyValue
CurveEd25519
Root WalletSolana
Address Formatbase58check (mv1...)
HashingBlake2b-160 (address), Blake2b-256 (signing)
SerializationMicheline / node-forged binary
Smallest UnitMutez (1 MAV = 10^6 mutez)

Dependencies

npm install @noble/hashes bs58

Derive Address

Hash the Ed25519 public key with Blake2b-160 and base58check-encode with the mv1 prefix bytes [5, 186, 196]:
import { blake2b } from "@noble/hashes/blake2";
import { sha256 } from "@noble/hashes/sha2";
import bs58 from "bs58";

const PREFIX_MV1 = new Uint8Array([5, 186, 196]);

function mavrykB58cencode(payload: Uint8Array, prefixBytes: Uint8Array): string {
  const combined = concatBytes(prefixBytes, payload);
  const checksum = sha256(sha256(combined)).slice(0, 4);
  return bs58.encode(concatBytes(combined, checksum));
}

function deriveMavrykAddress(solanaAddress: string): string {
  const pubkey = bs58.decode(solanaAddress);
  const hash = blake2b(pubkey, { dkLen: 20 }) as Uint8Array;
  return mavrykB58cencode(hash, PREFIX_MV1);
}

Sign a Message

Mavryk uses a 0x05 watermark for arbitrary data signing, followed by Blake2b-256:
import { blake2b } from "@noble/hashes/blake2";

async function signMavrykMessage(message: string): Promise<string> {
  const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
  const msgBytes = new TextEncoder().encode(message);
  const watermarked = concatBytes(new Uint8Array([0x05]), msgBytes);
  const digest = blake2b(watermarked, { dkLen: 32 }) as Uint8Array;
  const { signedMessage } = await dynamicClient.wallets.signMessage({
    wallet,
    message: bytesToHex(digest),
  });
  return signedMessage;
}

Sign a Transaction

Forge the operation via RPC, apply the 0x03 watermark, hash with Blake2b-256, sign, and inject. Automatically prepends a reveal operation if the manager key hasn’t been published yet:
import { blake2b } from "@noble/hashes/blake2";
import bs58 from "bs58";

const MAVRYK_RPC = "https://atlasnet.rpc.mavryk.network";
// Ed25519 public key prefix for Mavryk (edpk)
const PREFIX_EDPK = new Uint8Array([13, 15, 37, 217]);

async function sendMavrykTransfer(
  to: string,
  amountMvk: number,
  fromAddress: string,
  solanaAddress: string,
): Promise<string> {
  const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
  const pubkeyBytes = bs58.decode(solanaAddress);
  const encodedPubKey = mavrykB58cencode(pubkeyBytes, PREFIX_EDPK);
  const microMvk = Math.round(amountMvk * 1_000_000).toString();

  const [blockHead, counterStr, managerKey] = await Promise.all([
    fetch(`${MAVRYK_RPC}/chains/main/blocks/head/header`).then(r => r.json()),
    fetch(`${MAVRYK_RPC}/chains/main/blocks/head/context/contracts/${fromAddress}/counter`).then(r => r.json()),
    fetch(`${MAVRYK_RPC}/chains/main/blocks/head/context/contracts/${fromAddress}/manager_key`).then(r => r.json()).catch(() => null),
  ]);

  const branch = blockHead.hash;
  let counter = parseInt(counterStr) + 1;
  const isRevealed = managerKey !== null && managerKey !== "";
  const contents: object[] = [];

  if (!isRevealed) {
    contents.push({ kind: "reveal", source: fromAddress, fee: "1000",
      counter: counter.toString(), gas_limit: "1000", storage_limit: "0",
      public_key: encodedPubKey });
    counter++;
  }

  contents.push({ kind: "transaction", source: fromAddress, fee: "1000",
    counter: counter.toString(), gas_limit: "10300", storage_limit: "0",
    amount: microMvk, destination: to });

  const forgeRes = await fetch(`${MAVRYK_RPC}/chains/main/blocks/head/helpers/forge/operations`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ branch, contents }),
  });
  const forgedHex: string = await forgeRes.json();

  // Mavryk operation signing: blake2b-256( 0x03 || forged_bytes )
  const forgedBytes = hexToBytes(forgedHex);
  const watermarked = concatBytes(new Uint8Array([0x03]), forgedBytes);
  const digest = blake2b(watermarked, { dkLen: 32 }) as Uint8Array;

  const { signedMessage } = await dynamicClient.wallets.signMessage({
    wallet,
    message: bytesToHex(digest),
  });
  const sigBytes = decodeSig(signedMessage);

  const injectRes = await fetch(`${MAVRYK_RPC}/injection/operation`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: `"${forgedHex}${bytesToHex(sigBytes)}"`,
  });
  if (!injectRes.ok) throw new Error(`Injection failed: ${injectRes.status}`);
  return injectRes.json();
}

Verify a Signature

import { ed25519 } from "@noble/curves/ed25519";
import bs58 from "bs58";

function verifyMavrykSignature(
  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

async function getMavrykBalance(address: string): Promise<string> {
  try {
    const res = await fetch(
      `https://atlasnet.rpc.mavryk.network/chains/main/blocks/head/context/contracts/${address}/balance`,
    );
    if (!res.ok) return "0";
    return (parseInt(await res.json()) / 1_000_000).toString();
  } catch {
    return "0";
  }
}