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 themv1 version prefix bytes, and base58check-encoding.
| Property | Value |
|---|---|
| Curve | Ed25519 |
| Root Wallet | Solana |
| Address Format | base58check (mv1 prefix) |
| Hashing | Blake2b-160 (address), Blake2b-256 (signing) |
| Serialization | Tezos binary (forged via node) |
| Smallest Unit | mutez (1 MVK = 1,000,000 mutez) |
Dependencies
Add topubspec.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
Mavrykmv1 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 the0x05 “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';
}
}