Overview
Aptos uses the Ed25519 elliptic curve — the same as Solana. The Aptos single-key account address is derived by appending the scheme byte0x00 to the public key and computing SHA3-256, producing a 32-byte hex address with 0x prefix.
| Property | Value |
|---|---|
| Curve | Ed25519 |
| Root Wallet | Solana |
| Address Format | 0x + SHA3-256(pubkey ‖ 0x00) hex |
| Hashing | SHA3-256 |
| Serialization | Aptos REST API (encode_submission) |
| Smallest Unit | octa (1 APT = 10^8 octas) |
Dependencies
Add topubspec.yaml:
dependencies:
pointycastle: ^3.9.1 # SHA3-256 (SHA3Digest)
bs58: ^1.0.2 # Base58 (Solana address decoding)
http: ^1.2.2 # HTTP client
Derive Address
Aptos single-key Ed25519 accounts use SHA3-256(pubkey ‖ scheme_byte) as the address:import 'dart:typed_data';
import 'package:pointycastle/digests/sha3.dart';
import 'package:bs58/bs58.dart';
/// Derive an Aptos address from a Solana address.
/// Aptos single-key Ed25519: SHA3-256(pubkey || 0x00) → 0x-prefixed hex
String deriveAptosAddress(String solanaAddress) {
final pubkey = Uint8List.fromList(base58.decode(solanaAddress));
final payload = Uint8List(33);
payload.setAll(0, pubkey);
payload[32] = 0x00; // single-key scheme byte
final hash = SHA3Digest(256).process(payload);
return '0x${bytesToHex(hash)}';
}
// Usage
final solWallet = DynamicSDK.instance.wallets.userWallets
.firstWhere((w) => w.chain == 'SOL');
final aptosAddress = deriveAptosAddress(solWallet.address);
// → "0x8d6f3c..." (64-char hex with 0x prefix)
Sign a Message
Aptos message signing uses SHA3-256 of the raw message bytes:import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/digests/sha3.dart';
/// Compute the Aptos message digest: SHA3-256(messageBytes)
String aptosMessageDigest(String message) {
final msgBytes = Uint8List.fromList(utf8.encode(message));
return bytesToHex(SHA3Digest(256).process(msgBytes));
}
Future<String> signAptosMessage(String message) async {
final solWallet = DynamicSDK.instance.wallets.userWallets
.firstWhere((w) => w.chain == 'SOL');
final hexDigest = aptosMessageDigest(message);
final signer = DynamicSDK.instance.solana.createSigner(wallet: solWallet);
final signature = await signer.signMessage(message: hexDigest);
return signature;
}
Sign a Transaction
Aptos uses itsencode_submission REST endpoint to get the signing bytes, avoiding manual BCS serialization:
import 'dart:convert';
import 'dart:typed_data';
import 'package:bs58/bs58.dart';
import 'package:http/http.dart' as http;
const _aptosRpc = 'https://fullnode.testnet.aptoslabs.com/v1'; // testnet
Future<String> sendAptosTransfer({
required String recipient,
required double amountApt,
required String aptosAddress,
required String solanaAddress,
required SignWithSolana signFn, // callback: (String hexDigest) → Future<String>
}) async {
final pubkey = Uint8List.fromList(base58.decode(solanaAddress));
if (!RegExp(r'^0x[0-9a-fA-F]{1,64}$').hasMatch(recipient)) {
throw Exception('Invalid Aptos address');
}
final octas = (amountApt * 1e8).round().toString();
// Fetch account and ledger info in parallel
final results = await Future.wait([
http.get(Uri.parse('$_aptosRpc/accounts/$aptosAddress')),
http.get(Uri.parse(_aptosRpc)),
]);
final account = json.decode(results[0].body) as Map<String, dynamic>;
final ledger = json.decode(results[1].body) as Map<String, dynamic>;
final sequenceNumber = account['sequence_number'] as String;
final expiry = (int.parse(ledger['ledger_timestamp'] as String) ~/ 1000000 + 600).toString();
final txPayload = {
'sender': aptosAddress,
'sequence_number': sequenceNumber,
'max_gas_amount': '20000',
'gas_unit_price': '100',
'expiration_timestamp_secs': expiry,
'payload': {
'type': 'entry_function_payload',
'function': '0x1::aptos_account::transfer',
'type_arguments': <String>[],
'arguments': [recipient, octas],
},
};
// Get signing bytes from the Aptos API
final encodeRes = await http.post(
Uri.parse('$_aptosRpc/transactions/encode_submission'),
headers: {'Content-Type': 'application/json'},
body: json.encode(txPayload),
);
if (encodeRes.statusCode != 200) {
final err = json.decode(encodeRes.body) as Map<String, dynamic>;
throw Exception(err['message'] ?? 'Encode failed');
}
final signingBytesHex = json.decode(encodeRes.body) as String;
final hexDigest = signingBytesHex.startsWith('0x')
? signingBytesHex.substring(2)
: signingBytesHex;
final signResult = await signFn(hexDigest);
final sigBytes = decodeSig(signResult);
final signedTx = {
...txPayload,
'signature': {
'type': 'ed25519_signature',
'public_key': '0x${bytesToHex(pubkey)}',
'signature': '0x${bytesToHex(sigBytes)}',
},
};
final submitRes = await http.post(
Uri.parse('$_aptosRpc/transactions'),
headers: {'Content-Type': 'application/json'},
body: json.encode(signedTx),
);
if (submitRes.statusCode != 200 && submitRes.statusCode != 202) {
final err = json.decode(submitRes.body) as Map<String, dynamic>;
throw Exception(err['message'] ?? 'Submit failed');
}
final result = json.decode(submitRes.body) as Map<String, dynamic>;
return result['hash'] as String;
}
Check Balance
Aptos supports two balance models. The code tries the legacyCoinStore model first, then falls back to the FungibleAsset model:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<String> getAptosBalance(String address) async {
// Try legacy CoinStore model first
try {
final res = await http.get(Uri.parse(
'$_aptosRpc/accounts/$address/resource/'
'0x1%3A%3Acoin%3A%3ACoinStore%3C0x1%3A%3Aaptos_coin%3A%3AAptosCoin%3E',
));
if (res.statusCode == 200) {
final data = json.decode(res.body) as Map<String, dynamic>;
final value = (data['data'] as Map<String, dynamic>?)?['coin']?['value'] as String?;
if (value != null) {
final octas = BigInt.parse(value);
if (octas > BigInt.zero) return _formatOctas(octas);
}
}
} catch (_) {}
// Fall back to Fungible Asset model
try {
final res = await http.post(
Uri.parse('$_aptosRpc/view'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'function': '0x1::primary_fungible_store::balance',
'type_arguments': ['0x1::fungible_asset::Metadata'],
'arguments': [
address,
'0x000000000000000000000000000000000000000000000000000000000000000a',
],
}),
);
if (res.statusCode == 200) {
final data = json.decode(res.body) as List;
return _formatOctas(BigInt.parse(data[0].toString()));
}
} catch (_) {}
return '0';
}
String _formatOctas(BigInt octas) {
const divisor = 100000000; // 1e8
final d = BigInt.from(divisor);
final whole = octas ~/ d;
final frac = octas % d;
final fracStr = frac.toString().padLeft(8, '0').replaceAll(RegExp(r'0+$'), '');
return fracStr.isEmpty ? whole.toString() : '$whole.$fracStr';
}