Skip to main content

Overview

Mavryk (a Tezos fork) uses the Ed25519 elliptic curve — the same as Solana. Addresses are derived by hashing the public key with Blake2b-160 (20 bytes), prepending the mv1 version prefix bytes, and base58check-encoding.
PropertyValue
CurveEd25519
Root WalletSolana
Address Formatbase58check (mv1 prefix)
HashingBlake2b-160 (address), Blake2b-256 (signing)
SerializationTezos binary (forged via node)
Smallest Unitmutez (1 MVK = 1,000,000 mutez)

Dependencies

Add to pubspec.yaml:
dependencies:
  hashlib: ^1.19.2       # Blake2b with configurable output size
  pointycastle: ^3.9.1   # SHA-256 (base58check)
  bs58: ^1.0.2           # Base58 encoding
  http: ^1.2.2           # HTTP client

Derive Address

Mavryk mv1 addresses use Blake2b-160 of the public key with version prefix bytes [0x05, 0xba, 0xc4]:
import 'dart:typed_data';
import 'package:hashlib/hashlib.dart' as hashlib;
import 'package:pointycastle/pointycastle.dart';
import 'package:bs58/bs58.dart';

/// Derive a Mavryk mv1... address from a Solana address.
/// blake2b(pubkey, 20) → prefix [0x05, 0xba, 0xc4] → base58check
String deriveMavrykAddress(String solanaAddress) {
  final pubkey = Uint8List.fromList(base58.decode(solanaAddress));
  final hash = hashlib.Blake2b(20).convert(pubkey).bytes;
  final prefix = [0x05, 0xba, 0xc4];
  final payload = Uint8List.fromList([...prefix, ...hash]);
  return _base58check(payload);
}

String _base58check(Uint8List payload) {
  final sha = Digest('SHA-256');
  final hash1 = sha.process(payload);
  final hash2 = sha.process(hash1);
  final checksum = hash2.sublist(0, 4);
  return base58.encode(Uint8List.fromList([...payload, ...checksum]));
}

// Usage
final solWallet = DynamicSDK.instance.wallets.userWallets
    .firstWhere((w) => w.chain == 'SOL');
final mavrykAddress = deriveMavrykAddress(solWallet.address);
// → "mv1..." (36-char base58check)

Sign a Message

Mavryk uses the standard Tezos off-chain signing convention: apply the 0x05 “generic” watermark byte, then hash with Blake2b-256:
import 'dart:convert';
import 'dart:typed_data';
import 'package:hashlib/hashlib.dart' as hashlib;

/// Compute the Mavryk message digest for signing.
/// Applies the 0x05 "generic" watermark then blake2b-256.
String mavrykMessageDigest(String message) {
  final msgBytes = Uint8List.fromList(utf8.encode(message));
  final watermarked = Uint8List.fromList([0x05, ...msgBytes]);
  return bytesToHex(
    Uint8List.fromList(hashlib.blake2b256.convert(watermarked).bytes),
  );
}

Future<String> signMavrykMessage(String message) async {
  final solWallet = DynamicSDK.instance.wallets.userWallets
      .firstWhere((w) => w.chain == 'SOL');

  final hexDigest = mavrykMessageDigest(message);

  final signer = DynamicSDK.instance.solana.createSigner(wallet: solWallet);
  final signature = await signer.signMessage(message: hexDigest);

  return signature;
}

Sign a Transaction

Mavryk transactions are forged by the node, then signed as: Blake2b-256(0x03 ‖ forged_bytes). The node also handles the optional reveal operation for first-time senders:
import 'dart:convert';
import 'dart:typed_data';
import 'package:hashlib/hashlib.dart' as hashlib;
import 'package:bs58/bs58.dart';
import 'package:pointycastle/pointycastle.dart';
import 'package:http/http.dart' as http;

const _mavrykRpc = 'https://rpc.mavryk.network';

Future<String> sendMavrykTransfer({
  required String to,
  required double amount,
  required String address,
  required String solanaAddress,
  required SignWithSolana signFn, // callback: (String hexDigest) → Future<String>
}) async {
  final pubkey = Uint8List.fromList(base58.decode(solanaAddress));

  final mutez = (amount * 1000000).round().toString();

  // Encode public key as edpk... (for reveal operation)
  final edpk = _base58check(
    Uint8List.fromList([0x0d, 0x0f, 0x25, 0xd9, ...pubkey]),
  );

  // Fetch block hash, counter, and manager_key in parallel
  final results = await Future.wait([
    _mavrykGet('/chains/main/blocks/head/hash'),
    _mavrykGet('/chains/main/blocks/head/context/contracts/$address/counter'),
    _mavrykGet('/chains/main/blocks/head/context/contracts/$address/manager_key')
        .catchError((_) => null),
  ]);

  final blockHash = results[0] as String;
  var counter = int.parse(results[1].toString());
  final managerKey = results[2];
  final contents = <Map<String, dynamic>>[];

  // Prepend reveal operation if public key not yet published
  if (managerKey == null) {
    counter++;
    contents.add({
      'kind': 'reveal',
      'source': address,
      'fee': '1000',
      'counter': '$counter',
      'gas_limit': '1000',
      'storage_limit': '0',
      'public_key': edpk,
    });
  }

  // Build transaction operation
  counter++;
  contents.add({
    'kind': 'transaction',
    'source': address,
    'fee': '1000',
    'counter': '$counter',
    'gas_limit': '10300',
    'storage_limit': '0',
    'amount': mutez,
    'destination': to,
  });

  // Forge the operation via the node
  final forgedHex = await _mavrykPost(
    '/chains/main/blocks/head/helpers/forge/operations',
    {'branch': blockHash, 'contents': contents},
  );

  // Sign: blake2b-256(0x03 + forged_bytes)
  final forgedBytes = hexToBytes(forgedHex.toString());
  final watermarked = Uint8List.fromList([0x03, ...forgedBytes]);
  final digest = hashlib.blake2b256.convert(watermarked).bytes;
  final hexDigest = bytesToHex(Uint8List.fromList(digest));

  final signResult = await signFn(hexDigest);
  final sigBytes = decodeSig(signResult);
  final sigHex = bytesToHex(sigBytes);

  // Inject: POST the forged hex + signature hex
  final opHash = await _mavrykPost(
    '/injection/operation',
    forgedHex.toString() + sigHex,
  );

  return opHash.toString();
}

Future<dynamic> _mavrykGet(String path) async {
  final res = await http.get(Uri.parse('$_mavrykRpc$path'));
  if (res.statusCode != 200) throw Exception('Mavryk GET $path: ${res.statusCode}');
  return json.decode(res.body);
}

Future<dynamic> _mavrykPost(String path, dynamic body) async {
  final res = await http.post(
    Uri.parse('$_mavrykRpc$path'),
    headers: {'Content-Type': 'application/json'},
    body: json.encode(body),
  );
  if (res.statusCode != 200) {
    throw Exception('Mavryk POST $path: ${res.statusCode} ${res.body}');
  }
  return json.decode(res.body);
}

Check Balance

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> getMavrykBalance(String address) async {
  try {
    final res = await http.get(
      Uri.parse('$_mavrykRpc/chains/main/blocks/head/context/contracts/$address/balance'),
    );
    if (res.statusCode != 200) return '0';
    final mutez = BigInt.parse(json.decode(res.body).toString());
    final divisor = BigInt.from(1000000);
    final whole = mutez ~/ divisor;
    final frac = mutez % divisor;
    final fracStr = frac.toString().padLeft(6, '0').replaceAll(RegExp(r'0+$'), '');
    return fracStr.isEmpty ? whole.toString() : '$whole.$fracStr';
  } catch (_) {
    return '0';
  }
}