Skip to main content

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

Mavryk (a fork of Tezos) uses the Ed25519 elliptic curve. Addresses are derived with Blake2b-160 hashing and base58check-encoded with the mv1 prefix. Transactions are forged via RPC and injected directly — no webmavryk dependency required. This guide covers Ed25519 (mv1) addresses only. Mavryk also supports Secp256k1 (mv2), P-256 (mv3), and BLS12-381 (mv4); Dynamic’s embedded wallet flow here assumes Ed25519 keys.
PropertyValue
CurveEd25519
Address Formatbase58check (mv1...)
HashingBlake2b-160 (address), Blake2b-256 (signing)
SerializationMicheline / node-forged binary
Smallest UnitMumav (1 MAV = 10^6 mumav)

Dependencies

npm install @noble/hashes bs58 @noble/curves

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/blake2b";
import { concatBytes } from "@noble/hashes/utils";
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(walletAddress: string): string {
  const pubkey = bs58.decode(walletAddress);
  const hash = blake2b(pubkey, { dkLen: 20 });
  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/blake2b";
import { concatBytes, bytesToHex } from "@noble/hashes/utils";
import type { BaseClient } from "@dynamic-labs/client";

async function signMavrykMessage(
  message: string,
  dynamicClient: BaseClient,
): 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 });
  const { signedMessage } = await dynamicClient.wallets.signMessage({
    wallet,
    message: bytesToHex(digest),
  });
  return signedMessage;
}
Dynamic’s React Native embedded wallet currently exposes Mavryk signing through the Solana (SOL) chain entry for the embedded key. Use the wallet returned by that lookup until Dynamic exposes a Mavryk-specific chain identifier.

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/blake2b";
import { concatBytes, bytesToHex, hexToBytes } from "@noble/hashes/utils";
import bs58 from "bs58";
import type { BaseClient } from "@dynamic-labs/client";

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

async function sendMavrykTransfer(
  to: string,
  amountMav: number,
  fromAddress: string,
  walletAddress: string,
  dynamicClient: BaseClient,
): Promise<string> {
  const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
  const pubkeyBytes = bs58.decode(walletAddress);
  const encodedPubKey = mavrykB58cencode(pubkeyBytes, PREFIX_EDPK);
  const mumav = Math.round(amountMav * 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: mumav, 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 });

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

  // Dynamic returns the signature as base58; normalize to hex for injection
  const sigHex = bytesToHex(bs58.decode(signedMessage));

  const injectRes = await fetch(`${MAVRYK_RPC}/injection/operation`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: `"${forgedHex}${sigHex}"`,
  });
  if (!injectRes.ok) throw new Error(`Injection failed: ${injectRes.status}`);
  return injectRes.json();
}
fee, gas_limit, and storage_limit are illustrative values. For production, estimate fees via your node’s RPC (for example helpers/scripts/operation helpers) or another estimator — insufficient limits will cause the node to reject the operation.

Verify a Signature

import { ed25519 } from "@noble/curves/ed25519";
import { blake2b } from "@noble/hashes/blake2b";
import { concatBytes, hexToBytes } from "@noble/hashes/utils";
import bs58 from "bs58";

function verifyMavrykSignature(
  message: string,
  signature: string,
  walletAddress: string,
): boolean {
  const pubkey = bs58.decode(walletAddress);
  const messageBytes = new TextEncoder().encode(message);
  const watermarked = concatBytes(new Uint8Array([0x05]), messageBytes);
  const digest = blake2b(watermarked, { dkLen: 32 });
  // Dynamic returns signatures as base58; decode before verifying
  const sigBytes = bs58.decode(signature);
  return ed25519.verify(sigBytes, digest, pubkey);
}

Check Balance

async function getMavrykBalance(address: string): Promise<string> {
  try {
    const res = await fetch(
      `https://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";
  }
}