import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'dart:async'; import 'dart:isolate'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_decred/api/libdcrwallet_bindings.dart'; import 'package:cw_decred/api/util.dart'; final int ErrCodeNotSynced = 1; final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux. ? 'libdcrwallet.so' : 'cw_decred.framework/cw_decred'; class Libwallet { final SendPort _commands; final ReceivePort _responses; final Map> _activeRequests = {}; int _idCounter = 0; bool _closed = false; static Future spawn() async { // Create a receive port and add its initial message handler. final initPort = RawReceivePort(); final connection = Completer<(ReceivePort, SendPort)>.sync(); initPort.handler = (initialMessage) { final commandPort = initialMessage as SendPort; connection.complete(( ReceivePort.fromRawReceivePort(initPort), commandPort, )); }; // Spawn the isolate. try { await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); } on Object { initPort.close(); rethrow; } final (ReceivePort receivePort, SendPort sendPort) = await connection.future; return Libwallet._(receivePort, sendPort); } Libwallet._(this._responses, this._commands) { _responses.listen(_handleResponsesFromIsolate); } void _handleResponsesFromIsolate(dynamic message) { final (int id, Object? response) = message as (int, Object?); final completer = _activeRequests.remove(id)!; if (response is RemoteError) { completer.completeError(response); } else { completer.complete(response); } if (_closed && _activeRequests.isEmpty) _responses.close(); } static void _handleCommandsToIsolate( ReceivePort receivePort, SendPort sendPort, ) { final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName)); receivePort.listen((message) { if (message == 'shutdown') { receivePort.close(); return; } final (int id, Map args) = message as (int, Map); var res = PayloadResult("", "", 0); final method = args["method"] ?? ""; try { switch (method) { case "initlibdcrwallet": final logDir = args["logdir"] ?? ""; final level = args["level"] ?? ""; final cLogDir = logDir.toCString(); final cLevel = level.toCString(); executePayloadFn( fn: () => dcrwalletApi.initialize(cLogDir, cLevel), ptrsToFree: [cLogDir, cLevel], ); break; case "createwallet": final config = args["config"] ?? ""; final cConfig = config.toCString(); executePayloadFn( fn: () => dcrwalletApi.createWallet(cConfig), ptrsToFree: [cConfig], ); break; case "createwatchonlywallet": final config = args["config"] ?? ""; final cConfig = config.toCString(); executePayloadFn( fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig), ptrsToFree: [cConfig], ); break; case "loadwallet": final config = args["config"] ?? ""; final cConfig = config.toCString(); executePayloadFn( fn: () => dcrwalletApi.loadWallet(cConfig), ptrsToFree: [cConfig], ); break; case "startsync": final name = args["name"] ?? ""; final peers = args["peers"] ?? ""; final cName = name.toCString(); final cPeers = peers.toCString(); executePayloadFn( fn: () => dcrwalletApi.syncWallet(cName, cPeers), ptrsToFree: [cName, cPeers], ); break; case "closewallet": final name = args["name"] ?? ""; final cName = name.toCString(); executePayloadFn( fn: () => dcrwalletApi.closeWallet(cName), ptrsToFree: [cName], ); break; case "changewalletpassword": final name = args["name"] ?? ""; final oldPass = args["oldpass"] ?? ""; final newPass = args["newpass"] ?? ""; final cName = name.toCString(); final cOldPass = oldPass.toCString(); final cNewPass = newPass.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass), ptrsToFree: [cName, cOldPass, cNewPass], ); break; case "walletseed": final name = args["name"] ?? ""; final pass = args["pass"] ?? ""; final cName = name.toCString(); final cPass = pass.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.walletSeed(cName, cPass), ptrsToFree: [cName, cPass], ); break; case "syncstatus": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.syncWalletStatus(cName), ptrsToFree: [cName], ); break; case "balance": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.walletBalance(cName), ptrsToFree: [cName], ); break; case "estimatefee": final name = args["name"] ?? ""; final numBlocks = args["numblocks"] ?? ""; final cName = name.toCString(); final cNumBlocks = numBlocks.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks), ptrsToFree: [cName, cNumBlocks], ); break; case "createsignedtransaction": final name = args["name"] ?? ""; final signReq = args["signreq"] ?? ""; final cName = name.toCString(); final cSignReq = signReq.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), ptrsToFree: [cName, cSignReq], ); break; case "sendrawtransaction": final name = args["name"] ?? ""; final txHex = args["txhex"] ?? ""; final cName = name.toCString(); final cTxHex = txHex.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex), ptrsToFree: [cName, cTxHex], ); break; case "listtransactions": final name = args["name"] ?? ""; final from = args["from"] ?? ""; final count = args["count"] ?? ""; final cName = name.toCString(); final cFrom = from.toCString(); final cCount = count.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount), ptrsToFree: [cName, cFrom, cCount], ); break; case "bestblock": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.bestBlock(cName), ptrsToFree: [cName], ); break; case "listunspents": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.listUnspents(cName), ptrsToFree: [cName], ); break; case "rescanfromheight": final name = args["name"] ?? ""; final height = args["height"] ?? ""; final cName = name.toCString(); final cHeight = height.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight), ptrsToFree: [cName, cHeight], ); break; case "signmessage": final name = args["name"] ?? ""; final message = args["message"] ?? ""; final address = args["address"] ?? ""; final pass = args["pass"] ?? ""; final cName = name.toCString(); final cMessage = message.toCString(); final cAddress = address.toCString(); final cPass = pass.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass), ptrsToFree: [cName, cMessage, cAddress, cPass], ); break; case "verifymessage": final name = args["name"] ?? ""; final message = args["message"] ?? ""; final address = args["address"] ?? ""; final sig = args["sig"] ?? ""; final cName = name.toCString(); final cMessage = message.toCString(); final cAddress = address.toCString(); final cSig = sig.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig), ptrsToFree: [cName, cMessage, cAddress, cSig], ); break; case "newexternaladdress": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.newExternalAddress(cName), ptrsToFree: [cName], skipErrorCheck: true, ); break; case "defaultpubkey": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.defaultPubkey(cName), ptrsToFree: [cName], ); break; case "addresses": final name = args["name"] ?? ""; final nUsed = args["nused"] ?? ""; final nUnused = args["nunused"] ?? ""; final cName = name.toCString(); final cNUsed = nUsed.toCString(); final cNUnused = nUnused.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused), ptrsToFree: [cName, cNUsed, cNUnused], ); break; case "birthstate": final name = args["name"] ?? ""; final cName = name.toCString(); res = executePayloadFn( fn: () => dcrwalletApi.birthState(cName), ptrsToFree: [cName], ); break; case "shutdown": final name = args["name"] ?? ""; // final cName = name.toCString(); executePayloadFn( fn: () => dcrwalletApi.shutdown(), ptrsToFree: [], ); break; default: res = PayloadResult("", "unknown libwallet method ${method}", 0); } sendPort.send((id, res)); } catch (e) { final errMsg = e.toString(); printV("decred libwallet returned an error for method ${method}: ${errMsg}"); sendPort.send((id, PayloadResult("", errMsg, 0))); } }); } static void _startRemoteIsolate(SendPort sendPort) { final receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); _handleCommandsToIsolate(receivePort, sendPort); } // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets // it ready for use. This must be done before attempting to create, load or use // a wallet. An empty string can be used to log to stdout and create no log files. Future initLibdcrwallet(String logDir, String level) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "initlibdcrwallet", "logdir": logDir, "level": level, }; _commands.send((id, req)); await completer.future; } Future createWallet(String config) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "createwallet", "config": config, }; _commands.send((id, req)); await completer.future; } Future createWatchOnlyWallet(String config) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "createwatchonlywallet", "config": config, }; _commands.send((id, req)); await completer.future; } Future loadWallet(String config) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "loadwallet", "config": config, }; _commands.send((id, req)); await completer.future; } Future startSync(String walletName, String peers) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "startsync", "name": walletName, "peers": peers, }; _commands.send((id, req)); await completer.future; } Future closeWallet(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "closewallet", "name": walletName, }; _commands.send((id, req)); await completer.future; } Future changeWalletPassword( String walletName, String currentPassword, String newPassword) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "changewalletpassword", "name": walletName, "oldpass": currentPassword, "newpass": newPassword }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future walletSeed(String walletName, String walletPassword) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "walletseed", "name": walletName, "pass": walletPassword, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future syncStatus(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "syncstatus", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future balance(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "balance", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; try { return jsonDecode(res.payload); } catch (_) { return {}; } } Future estimateFee(String walletName, int numBlocks) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "estimatefee", "name": walletName, "numblocks": numBlocks.toString(), }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future createSignedTransaction( String walletName, String createSignedTransactionReq) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "createsignedtransaction", "name": walletName, "signreq": createSignedTransactionReq, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future sendRawTransaction(String walletName, String txHex) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "sendrawtransaction", "name": walletName, "txhex": txHex, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future listTransactions(String walletName, String from, String count) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "listtransactions", "name": walletName, "from": from, "count": count, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future bestBlock(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "bestblock", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future listUnspents(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "listunspents", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future rescanFromHeight(String walletName, String height) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "rescanfromheight", "name": walletName, "height": height, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future signMessage( String walletName, String message, String address, String walletPass) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "signmessage", "name": walletName, "message": message, "address": address, "pass": walletPass, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future verifyMessage( String walletName, String message, String address, String sig) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "verifymessage", "name": walletName, "message": message, "address": address, "sig": sig, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future newExternalAddress(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "newexternaladdress", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; if (res.errCode == ErrCodeNotSynced) { // Wallet is not synced. We do not want to give out a used address so give // nothing. return null; } checkErr(res.err); return res.payload; } Future defaultPubkey(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "defaultpubkey", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future addresses(String walletName, String nUsed, String nUnused) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "addresses", "name": walletName, "nused": nUsed, "nunused": nUnused, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future birthState(String walletName) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "birthstate", "name": walletName, }; _commands.send((id, req)); final res = await completer.future as PayloadResult; return res.payload; } Future shutdown() async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; final req = { "method": "shutdown", }; _commands.send((id, req)); await completer.future as PayloadResult; } void close() { if (!_closed) { _closed = true; _commands.send('shutdown'); if (_activeRequests.isEmpty) _responses.close(); } } }