import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:ffi/ffi.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus.dart'; import 'package:ledger_flutter_plus/ledger_flutter_plus_dart.dart'; import 'package:monero/monero.dart' as monero; LedgerConnection? gLedger; Timer? _ledgerExchangeTimer; Timer? _ledgerKeepAlive; void enableLedgerExchange(monero.wallet ptr, LedgerConnection connection) { _ledgerExchangeTimer?.cancel(); _ledgerExchangeTimer = Timer.periodic(Duration(milliseconds: 1), (_) async { final ledgerRequestLength = monero.Wallet_getSendToDeviceLength(ptr); final ledgerRequest = monero.Wallet_getSendToDevice(ptr) .cast() .asTypedList(ledgerRequestLength); if (ledgerRequestLength > 0) { _ledgerKeepAlive?.cancel(); final Pointer emptyPointer = malloc(0); monero.Wallet_setDeviceSendData( ptr, emptyPointer.cast(), 0); malloc.free(emptyPointer); _logLedgerCommand(ledgerRequest, false); final response = await exchange(connection, ledgerRequest); _logLedgerCommand(response, true); if (ListEquality().equals(response, [0x55, 0x15])) { await connection.disconnect(); // // TODO: Show POPUP pls unlock your device // await Future.delayed(Duration(seconds: 15)); // response = await exchange(connection, ledgerRequest); } final Pointer result = malloc(response.length); for (var i = 0; i < response.length; i++) { result.asTypedList(response.length)[i] = response[i]; } monero.Wallet_setDeviceReceivedData( ptr, result.cast(), response.length); malloc.free(result); keepAlive(connection); } }); } void keepAlive(LedgerConnection connection) { if (connection.connectionType == ConnectionType.ble) { _ledgerKeepAlive = Timer.periodic(Duration(seconds: 10), (_) async { try { UniversalBle.setNotifiable( connection.device.id, connection.device.deviceInfo.serviceId, connection.device.deviceInfo.notifyCharacteristicKey, BleInputProperty.notification, ); } catch (_) {} }); } } void disableLedgerExchange() { _ledgerExchangeTimer?.cancel(); _ledgerKeepAlive?.cancel(); gLedger?.disconnect(); gLedger = null; } Future exchange(LedgerConnection connection, Uint8List data) async => connection.sendOperation(ExchangeOperation(data)); class ExchangeOperation extends LedgerRawOperation { final Uint8List inputData; ExchangeOperation(this.inputData); @override Future read(ByteDataReader reader) async => reader.read(reader.remainingLength); @override Future> write(ByteDataWriter writer) async => [inputData]; } const _ledgerMoneroCommands = { 0x00: "INS_NONE", 0x02: "INS_RESET", 0x20: "INS_GET_KEY", 0x21: "INS_DISPLAY_ADDRESS", 0x22: "INS_PUT_KEY", 0x24: "INS_GET_CHACHA8_PREKEY", 0x26: "INS_VERIFY_KEY", 0x28: "INS_MANAGE_SEEDWORDS", 0x30: "INS_SECRET_KEY_TO_PUBLIC_KEY", 0x32: "INS_GEN_KEY_DERIVATION", 0x34: "INS_DERIVATION_TO_SCALAR", 0x36: "INS_DERIVE_PUBLIC_KEY", 0x38: "INS_DERIVE_SECRET_KEY", 0x3A: "INS_GEN_KEY_IMAGE", 0x3B: "INS_DERIVE_VIEW_TAG", 0x3C: "INS_SECRET_KEY_ADD", 0x3E: "INS_SECRET_KEY_SUB", 0x40: "INS_GENERATE_KEYPAIR", 0x42: "INS_SECRET_SCAL_MUL_KEY", 0x44: "INS_SECRET_SCAL_MUL_BASE", 0x46: "INS_DERIVE_SUBADDRESS_PUBLIC_KEY", 0x48: "INS_GET_SUBADDRESS", 0x4A: "INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY", 0x4C: "INS_GET_SUBADDRESS_SECRET_KEY", 0x70: "INS_OPEN_TX", 0x72: "INS_SET_SIGNATURE_MODE", 0x74: "INS_GET_ADDITIONAL_KEY", 0x76: "INS_STEALTH", 0x77: "INS_GEN_COMMITMENT_MASK", 0x78: "INS_BLIND", 0x7A: "INS_UNBLIND", 0x7B: "INS_GEN_TXOUT_KEYS", 0x7D: "INS_PREFIX_HASH", 0x7C: "INS_VALIDATE", 0x7E: "INS_MLSAG", 0x7F: "INS_CLSAG", 0x80: "INS_CLOSE_TX", 0xA0: "INS_GET_TX_PROOF", 0xC0: "INS_GET_RESPONSE" }; void _logLedgerCommand(Uint8List command, [bool isResponse = true]) { String toHexString(Uint8List data) => data.map((e) => e.toRadixString(16).padLeft(2, '0')).join(); if (isResponse) { printV("< ${toHexString(command)}"); } else { printV( "> ${_ledgerMoneroCommands[command[1]]} ${toHexString(command.sublist(2))}"); } }