Skip to main content

Overview

Aptos uses the Ed25519 elliptic curve — the same as Solana. The Aptos single-key account address is derived by appending the scheme byte 0x00 to the public key and computing SHA3-256, producing a 32-byte hex address with 0x prefix.
PropertyValue
CurveEd25519
Root WalletSolana
Address Format0x + SHA3-256(pubkey ‖ 0x00) hex
HashingSHA3-256
SerializationAptos REST API (encode_submission)
Smallest Unitocta (1 APT = 10^8 octas)

Dependencies

Add to pubspec.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 its encode_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 legacy CoinStore 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';
}