import 'dart:convert';
import 'dart:typed_data';
import 'package:pointycastle/pointycastle.dart';
import 'package:http/http.dart' as http;
const _xrpRpcUrl = 'https://s.altnet.rippletest.net:51234'; // testnet
/// DER-encode an ECDSA signature from compact r+s hex (128 chars), normalizing S to low-S.
Uint8List _sigToDER(String rsHex) {
final r = hexToBytes(rsHex.substring(0, 64));
final sNorm = normalizeLowS(BigInt.parse(rsHex.substring(64), radix: 16));
final s = hexToBytes(sNorm.toRadixString(16).padLeft(64, '0'));
Uint8List intBytes(Uint8List v) {
var start = 0;
while (start < v.length - 1 && v[start] == 0) start++;
final trimmed = v.sublist(start);
return (trimmed[0] & 0x80) != 0
? Uint8List.fromList([0, ...trimmed])
: trimmed;
}
final rDer = intBytes(r);
final sDer = intBytes(s);
final contentLen = 2 + rDer.length + 2 + sDer.length;
final result = Uint8List(2 + contentLen);
result[0] = 0x30; // SEQUENCE
result[1] = contentLen;
result[2] = 0x02; result[3] = rDer.length;
result.setAll(4, rDer);
result[4 + rDer.length] = 0x02;
result[5 + rDer.length] = sDer.length;
result.setAll(6 + rDer.length, sDer);
return result;
}
Future<String> sendXrpTransfer({
required String to,
required double amount,
required String xrpAddress,
required Uint8List compressedPubkey,
required SignWithEvm signRaw, // callback: (Uint8List digest) → Future<String>
}) async {
final accountId = _xrpAddressToAccountId(xrpAddress);
final destinationId = _xrpAddressToAccountId(to);
final drops = BigInt.from((amount * 1000000).round());
// Fetch account info and current fee in parallel
final results = await Future.wait([
_xrpRpc('account_info', [{'account': xrpAddress, 'ledger_index': 'current'}]),
_xrpRpc('fee'),
]);
final sequence = results[0]['account_data']['Sequence'] as int;
final openFee = int.parse(results[1]['drops']?['open_ledger_fee'] ?? '12');
final fee = BigInt.from(openFee > 12 ? openFee : 12);
final currentLedger = (results[1]['ledger_current_index'] ?? 0) as int;
final lastLedger = currentLedger + 75;
// Serialize the unsigned transaction
final serialized = _serializePayment(
account: accountId,
destination: destinationId,
amount: drops,
fee: fee,
sequence: sequence,
lastLedgerSequence: lastLedger,
signingPubKey: compressedPubkey,
);
// Signing hash: SHA-512 first half of (0x53545800 + serialized)
final hashPrefix = Uint8List.fromList([0x53, 0x54, 0x58, 0x00]);
final hash = Digest('SHA-512')
.process(Uint8List.fromList([...hashPrefix, ...serialized]))
.sublist(0, 32);
final signature = await signRaw(hash);
final sigRaw = hexToBytes(strip0x(signature));
final rsHex = bytesToHex(sigRaw.sublist(0, 64));
final derSig = _sigToDER(rsHex);
// Build signed transaction and submit
final signedSerialized = _serializePayment(
account: accountId,
destination: destinationId,
amount: drops,
fee: fee,
sequence: sequence,
lastLedgerSequence: lastLedger,
signingPubKey: compressedPubkey,
txnSignature: derSig,
);
final result = await _xrpRpc('submit', [
{'tx_blob': bytesToHex(signedSerialized).toUpperCase()}
]);
final code = (result['engine_result'] ?? '') as String;
if (!code.startsWith('tes') && !code.startsWith('ter')) {
throw Exception('XRP submit failed [$code]: ${result['engine_result_message']}');
}
return result['tx_json']?['hash'] as String? ?? bytesToHex(
Digest('SHA-512').process(
Uint8List.fromList([0x54, 0x58, 0x4e, 0x00, ...signedSerialized]),
).sublist(0, 32),
).toUpperCase();
}
Future<Map<String, dynamic>> _xrpRpc(String method, [List<dynamic>? params]) async {
final res = await http.post(
Uri.parse(_xrpRpcUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({'method': method, 'params': params ?? []}),
);
final data = json.decode(res.body) as Map<String, dynamic>;
if (data['result']?['error'] != null) {
throw Exception('XRP RPC: ${data['result']['error_message']}');
}
return data['result'] as Map<String, dynamic>;
}