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'];
}