cake_wallet/cw_decred/lib/api/libdcrwallet.dart
Omar Hatem 0aa563ead7
v4.24.0 ()
* - Add Decred Wallet
- Remove Haven Wallet
- Fix and Improve Solana Wallet
- Improve app usability and user experience
- User interface enhancements
- Bug fixes

* Release candidate feedback fixes

* Release candidate feedback fixes

* update release notes [skip ci]

* fix iOS stupidity [skip ci]

* minor fix

* decred: Do not create log file. () ()

Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com>

* fix Monero HWW

* fix Monero view only wallet

* fix background sync for hardware and viewonly wallets

* check for status on correct wptr

* minor fixes [skip ci]

---------

Co-authored-by: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com>
Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
2025-03-25 03:49:54 +02:00

699 lines
22 KiB
Dart

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<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
bool _closed = false;
static Future<Libwallet> 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<String, String> args) = message as (int, Map<String, String>);
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<void> initLibdcrwallet(String logDir, String level) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "initlibdcrwallet",
"logdir": logDir,
"level": level,
};
_commands.send((id, req));
await completer.future;
}
Future<void> createWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "createwallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> createWatchOnlyWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "createwatchonlywallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> loadWallet(String config) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "loadwallet",
"config": config,
};
_commands.send((id, req));
await completer.future;
}
Future<void> startSync(String walletName, String peers) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "startsync",
"name": walletName,
"peers": peers,
};
_commands.send((id, req));
await completer.future;
}
Future<void> closeWallet(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
final req = {
"method": "closewallet",
"name": walletName,
};
_commands.send((id, req));
await completer.future;
}
Future<String> changeWalletPassword(
String walletName, String currentPassword, String newPassword) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String?> walletSeed(String walletName, String walletPassword) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> syncStatus(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<Map> balance(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> estimateFee(String walletName, int numBlocks) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> createSignedTransaction(
String walletName, String createSignedTransactionReq) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> sendRawTransaction(String walletName, String txHex) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> listTransactions(String walletName, String from, String count) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> bestBlock(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> listUnspents(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> rescanFromHeight(String walletName, String height) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> signMessage(
String walletName, String message, String address, String walletPass) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> verifyMessage(
String walletName, String message, String address, String sig) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String?> newExternalAddress(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> defaultPubkey(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> addresses(String walletName, String nUsed, String nUnused) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<String> birthState(String walletName) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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<void> shutdown() async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.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();
}
}
}