Skip to main content

Overview

NEAR uses the Ed25519 elliptic curve — the same as Solana. NEAR implicit accounts are 64-character hex strings representing the raw 32-byte Ed25519 public key. You derive the NEAR address directly from the Solana address (both are the same 32-byte pubkey, just encoded differently).
PropertyValue
CurveEd25519
Root WalletSolana
Address Format64-char lowercase hex (implicit account)
HashingSHA-256 (message digest), SHA-256 (tx hash)
SerializationBorsh
Smallest UnityoctoNEAR (1 NEAR = 10^24 yoctoNEAR)

Dependencies

Add to pubspec.yaml:
dependencies:
  pointycastle: ^3.9.1   # SHA-256
  bs58: ^1.0.2           # Base58 (for Solana address decoding)
  http: ^1.2.2           # HTTP client

Derive Address

NEAR implicit accounts are the hex-encoded 32-byte Ed25519 public key. Since the Solana address is base58-encoded form of the same 32-byte key, the derivation is straightforward:
import 'package:bs58/bs58.dart';

/// Derive a NEAR implicit account ID from a Solana address.
/// Both chains use Ed25519; NEAR implicit accounts are the hex-encoded 32-byte pubkey.
String deriveNearAddress(String solanaAddress) {
  final pubkey = Uint8List.fromList(base58.decode(solanaAddress));
  return bytesToHex(pubkey);
}

// Usage
final solWallet = DynamicSDK.instance.wallets.userWallets
    .firstWhere((w) => w.chain == 'SOL');
final nearAddress = deriveNearAddress(solWallet.address);
// → "8f7e9b2a3c..." (64-char hex)

Sign a Message

NEAR message signing uses a prefix similar to Ethereum EIP-191, then hashes with SHA-256:
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';

/// Compute the NEAR message digest for signing.
/// SHA-256( "\x19NEAR Signed Message:\n" + len(messageBytes) + messageBytes )
String nearMessageDigest(String message) {
  final messageBytes = utf8.encode(message);
  final prefix = utf8.encode('\x19NEAR Signed Message:\n');
  final lengthStr = utf8.encode('${messageBytes.length}');
  final toHash = Uint8List.fromList([...prefix, ...lengthStr, ...messageBytes]);
  return bytesToHex(Digest('SHA-256').process(toHash));
}

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

  final hexDigest = nearMessageDigest(message);

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

  return signature;
}

Sign a Transaction

NEAR transactions are Borsh-serialized, then SHA-256 hashed. The hash is signed with the Ed25519 key:
import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';
import 'package:bs58/bs58.dart';
import 'package:http/http.dart' as http;

const _nearRpc = 'https://rpc.testnet.near.org'; // testnet

Future<String> sendNearTransfer({
  required String recipient,
  required double amountNear,
  required String nearAddress,
  required String solanaAddress,
  required SignWithSolana signFn, // callback: (String hexDigest) → Future<String>
}) async {
  final pubkey = Uint8List.fromList(base58.decode(solanaAddress));

  // Convert NEAR to yoctoNEAR
  final parts = amountNear.toString().split('.');
  final wholePart = parts[0];
  final fracPart = parts.length > 1 ? parts[1] : '';
  final paddedFrac = fracPart.padRight(24, '0').substring(0, 24);
  final amountYocto = BigInt.parse(wholePart + paddedFrac);

  // Fetch access key and recent block hash in parallel
  final results = await Future.wait([
    _nearRpcCall('query', {
      'request_type': 'view_access_key',
      'finality': 'final',
      'account_id': nearAddress,
      'public_key': 'ed25519:${base58.encode(pubkey)}',
    }),
    _nearRpcCall('block', {'finality': 'final'}),
  ]);

  final nonce = BigInt.from(results[0]['nonce']) + BigInt.one;
  final blockHash = Uint8List.fromList(
    base58.decode(results[1]['header']['hash'] as String),
  );

  // Borsh-serialize the transaction
  final transferAction = concatBytes([
    _borshU8(3), // Transfer action variant
    _borshU128(amountYocto),
  ]);
  final txBytes = concatBytes([
    _borshString(nearAddress),
    _borshU8(0), // PublicKey enum variant 0 = ed25519
    pubkey,
    _borshU64(nonce),
    _borshString(recipient),
    blockHash,
    _borshU32(1), // 1 action
    transferAction,
  ]);

  // SHA-256 hash and sign
  final txHash = Digest('SHA-256').process(txBytes);
  final signResult = await signFn(bytesToHex(txHash));
  final sigBytes = decodeSig(signResult);

  // Build signed transaction
  final signedTxBytes = concatBytes([
    txBytes,
    _borshU8(0), // Signature enum variant 0 = ed25519
    sigBytes,
  ]);

  final result = await _nearRpcCall(
    'broadcast_tx_commit',
    [base64.encode(signedTxBytes)],
  );
  return result['transaction']['hash'] as String;
}

// Borsh helpers
Uint8List _borshU8(int value) => Uint8List.fromList([value & 0xff]);

Uint8List _borshU32(int value) => Uint8List.fromList([
  value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff,
]);

Uint8List _borshU64(BigInt value) {
  final buf = Uint8List(8);
  for (var i = 0; i < 8; i++) {
    buf[i] = ((value >> (i * 8)) & BigInt.from(0xff)).toInt();
  }
  return buf;
}

Uint8List _borshU128(BigInt value) {
  final buf = Uint8List(16);
  for (var i = 0; i < 16; i++) {
    buf[i] = ((value >> (i * 8)) & BigInt.from(0xff)).toInt();
  }
  return buf;
}

Uint8List _borshString(String s) {
  final encoded = utf8.encode(s);
  return concatBytes([_borshU32(encoded.length), Uint8List.fromList(encoded)]);
}

Future<dynamic> _nearRpcCall(String method, dynamic params) async {
  final res = await http.post(
    Uri.parse(_nearRpc),
    headers: {'Content-Type': 'application/json'},
    body: json.encode({'jsonrpc': '2.0', 'id': '1', 'method': method, 'params': params}),
  );
  final data = json.decode(res.body) as Map<String, dynamic>;
  if (data['error'] != null) throw Exception(data['error']['message']);
  return data['result'];
}

Check Balance

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

Future<String> getNearBalance(String accountId) async {
  try {
    final result = await _nearRpcCall('query', {
      'request_type': 'view_account',
      'finality': 'final',
      'account_id': accountId,
    });
    final yocto = BigInt.parse(result['amount'] as String);
    final divisor = BigInt.from(10).pow(24);
    final whole = yocto ~/ divisor;
    final frac = yocto % divisor;
    final fracStr = frac.toString().padLeft(24, '0').replaceAll(RegExp(r'0+$'), '');
    return fracStr.isEmpty ? whole.toString() : '$whole.$fracStr';
  } catch (e) {
    if (e.toString().contains('does not exist') ||
        e.toString().contains('UNKNOWN_ACCOUNT')) {
      return '0';
    }
    rethrow;
  }
}