diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 9bf6f5b97..b908f713c 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -287,8 +287,20 @@ class Node extends HiveObject with Keyable { return false; } } -} Future<bool> requestDecredNode() async { + final decredMainnetPort = 9108; + if (uri.host == "" && uri.port == decredMainnetPort) { + // Just show default port as ok. The wallet will connect to a list of known + // nodes automatically. return true; + } + try { + final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } } diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart index 02c9f0c74..367b0e013 100644 --- a/cw_decred/lib/api/libdcrwallet.dart +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -19,11 +19,10 @@ final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName)); /// a wallet. void initLibdcrwallet(String logDir) { final cLogDir = logDir.toCString(); - final res = executePayloadFn( + executePayloadFn( fn: () => dcrwalletApi.initialize(cLogDir), ptrsToFree: [cLogDir], ); - print(res.payload); } /// createWalletAsync calls the libdcrwallet's createWallet function @@ -46,11 +45,10 @@ void createWalletSync(Map<String, String> args) { final password = args["password"]!.toCString(); final network = "testnet".toCString(); - final res = executePayloadFn( + executePayloadFn( fn: () => dcrwalletApi.createWallet(name, dataDir, network, password), ptrsToFree: [name, dataDir, network, password], ); - print(res.payload); } /// loadWalletAsync calls the libdcrwallet's loadWallet function asynchronously. @@ -67,15 +65,35 @@ void loadWalletSync(Map<String, String> args) { final name = args["name"]!.toCString(); final dataDir = args["dataDir"]!.toCString(); final network = "testnet".toCString(); - final res = executePayloadFn( + executePayloadFn( fn: () => dcrwalletApi.loadWallet(name, dataDir, network), ptrsToFree: [name, dataDir, network], ); - print(res.payload); +} + +Future<void> startSyncAsync({required String name, required String peers}) { + final args = <String, String>{ + "name": name, + "peers": peers, + }; + return compute(startSync, args); +} + +void startSync(Map<String, String> args) { + final name = args["name"]!.toCString(); + final peers = args["peers"]!.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.syncWallet(name, peers), + ptrsToFree: [name, peers], + ); } void closeWallet(String walletName) { - // TODO. + final name = walletName.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.closeWallet(name), + ptrsToFree: [name], + ); } Future<void> changeWalletPassword( @@ -110,6 +128,15 @@ String? currentReceiveAddress(String walletName) { return res.payload; } +String syncStatus(String walletName) { + final cName = walletName.toCString(); + final res = executePayloadFn( + fn: () => dcrwalletApi.syncWalletStatus(cName), + ptrsToFree: [cName], + ); + return res.payload; +} + Map balance(String walletName) { final cName = walletName.toCString(); final res = executePayloadFn( diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart index 940c8d737..9cc6b6ab3 100644 --- a/cw_decred/lib/wallet.dart +++ b/cw_decred/lib/wallet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_decred/pending_transaction.dart'; @@ -38,9 +40,10 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, // password is currently only used for seed display, but would likely also be // required to sign inputs when creating transactions. final String _password; + bool connecting = false; + String persistantPeer = ""; + Timer? syncTimer; - // TODO: Set up a way to change the balance and sync status when dcrlibwallet - // changes. Long polling probably? @override @observable SyncStatus syncStatus; @@ -65,19 +68,138 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, Future<void> init() async { updateBalance(); - // TODO: update other wallet properties such as syncStatus, walletAddresses - // and transactionHistory with data from libdcrwallet. } + void performBackgroundTasks() { + if (!checkSync()) { + return; + } + updateBalance(); + } + + bool checkSync() { + final syncStatusJSON = libdcrwallet.syncStatus(walletInfo.name); + final decoded = json.decode(syncStatusJSON); + + final syncStatusCode = decoded["syncstatuscode"] ?? 0; + final syncStatusStr = decoded["syncstatus"] ?? ""; + final targetHeight = decoded["targetheight"] ?? 1; + final numPeers = decoded["numpeers"] ?? 0; + // final cFiltersHeight = decoded["cfiltersheight"] ?? 0; + final headersHeight = decoded["headersheight"] ?? 0; + final rescanHeight = decoded["rescanheight"] ?? 0; + + if (numPeers == 0) { + syncStatus = NotConnectedSyncStatus(); + return false; + } + + // Sync codes: + // NotStarted = 0 + // FetchingCFilters = 1 + // FetchingHeaders = 2 + // DiscoveringAddrs = 3 + // Rescanning = 4 + // Complete = 5 + + if (syncStatusCode > 4) { + syncStatus = SyncedSyncStatus(); + return true; + } + + if (syncStatusCode == 0) { + syncStatus = ConnectedSyncStatus(); + return false; + } + + if (syncStatusCode == 1) { + syncStatus = SyncingSyncStatus(targetHeight, 0.0); + return false; + } + + if (syncStatusCode == 2) { + final headersProg = headersHeight / targetHeight; + // Only allow headers progress to go up half way. + syncStatus = + SyncingSyncStatus(targetHeight - headersHeight, headersProg / 2); + return false; + } + + // TODO: This step takes a while so should really get more info to the UI + // that we are discovering addresses. + if (syncStatusCode == 3) { + // Hover at half. + syncStatus = SyncingSyncStatus(0, .5); + return false; + } + + if (syncStatusCode == 4) { + // Start at 75%. + final rescanProg = rescanHeight / targetHeight / 4; + syncStatus = + SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg); + return false; + } + return false; + } + + @action @override Future<void> connectToNode({required Node node}) async { - //throw UnimplementedError(); + if (connecting) { + throw "decred already connecting"; + } + connecting = true; + String addr = ""; + if (node.uri.host != "") { + addr = node.uri.host; + if (node.uri.port != "") { + addr += ":" + node.uri.port.toString(); + } + } + if (addr != persistantPeer) { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + persistantPeer = addr; + libdcrwallet.closeWallet(walletInfo.name); + libdcrwallet.loadWalletSync({ + "name": walletInfo.name, + "dataDir": walletInfo.dirPath, + }); + } + await this._startSync(); + connecting = false; } @action @override Future<void> startSync() async { - // TODO: call libdcrwallet.spvSync() and update syncStatus. + if (connecting) { + throw "decred already connecting"; + } + connecting = true; + await this._startSync(); + connecting = false; + } + + Future<void> _startSync() async { + if (syncTimer != null) { + return; + } + try { + syncStatus = ConnectingSyncStatus(); + libdcrwallet.startSyncAsync( + name: walletInfo.name, + peers: persistantPeer, + ); + syncTimer = Timer.periodic( + Duration(seconds: 5), (Timer t) => performBackgroundTasks()); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } } @override @@ -134,6 +256,10 @@ abstract class DecredWalletBase extends WalletBase<DecredBalance, @override void close() { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } libdcrwallet.closeWallet(walletInfo.name); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 9e06d25da..8d8cb6079 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -45,6 +45,7 @@ const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const moneroWorldNodeUri = '.moneroworld.com'; +const decredDefaultUri = ":9108"; Future<void> defaultSettingsMigration( {required int version, @@ -95,6 +96,7 @@ Future<void> defaultSettingsMigration( PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); + await setDefaultDecredNodeKey(sharedPreferences, nodes); await changeMoneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); await changeBitcoinCurrentElectrumServerToDefault( @@ -647,6 +649,11 @@ Node? getNanoDefaultNode({required Box<Node> nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); } +Node? getDecredDefaultNode({required Box<Node> nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == decredDefaultUri) ?? + nodes.values.firstWhereOrNull((node) => (node.type == WalletType.decred)); +} + Node? getNanoDefaultPowNode({required Box<Node> nodes}) { return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); @@ -812,6 +819,18 @@ Future<void> rewriteSecureStoragePin({required SecureStorage secureStorage}) asy ); } +// If "node_list.resetToDefault" is called the old node.key will still be set in +// preferences. Set it to whatever it is now. +// +// TODO: There really isn't any reason to have a default node for decred, find +// a different way to handle this. +Future<void> setDefaultDecredNodeKey( + SharedPreferences sharedPreferences, Box<Node> nodeSource) async { + final node = nodeSource.values.firstWhere((node) => node.type == WalletType.decred); + await sharedPreferences.setInt( + PreferencesKey.currentDecredNodeIdKey, node.key as int); +} + Future<void> changeBitcoinCurrentElectrumServerToDefault( {required SharedPreferences sharedPreferences, required Box<Node> nodes, @@ -1149,6 +1168,7 @@ Future<void> checkCurrentNodes( final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1168,6 +1188,8 @@ Future<void> checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + final currentDecredNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentDecredNodeId); final currentNanoPowNodeServer = powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); final currentBitcoinCashNodeServer = @@ -1261,6 +1283,14 @@ Future<void> checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); } + + if (currentDecredNodeServer == null) { + final decredMainnetPort = ":9108"; + final node = Node(uri: decredDefaultUri, type: WalletType.decred); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentDecredNodeIdKey, node.key as int); + } } Future<void> resetBitcoinElectrumServer( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 85e37a7bc..48a54f65b 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import "package:yaml/yaml.dart"; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; @@ -200,6 +201,12 @@ Future<List<Node>> loadDefaultWowneroNodes() async { return nodes; } +Future<List<Node>> loadDefaultDecredNodes() async { + final decredMainnetPort = ":9108"; + final node = Node(uri: decredMainnetPort, type: WalletType.decred); + return <Node>[node]; +} + Future<void> resetToDefault(Box<Node> nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -211,6 +218,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async { final polygonNodes = await loadDefaultPolygonNodes(); final solanaNodes = await loadDefaultSolanaNodes(); final tronNodes = await loadDefaultTronNodes(); + final decredNodes = await loadDefaultDecredNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -220,7 +228,9 @@ Future<void> resetToDefault(Box<Node> nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes + tronNodes; + solanaNodes + + tronNodes + + decredNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 99f0154da..94adb4237 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -816,11 +816,6 @@ abstract class SettingsStoreBase with Store { Node getCurrentNode(WalletType walletType) { final node = nodes[walletType]; - // TODO: Implement connecting to a user's preferred node. - if (walletType == WalletType.decred) { - return Node(); - } - if (node == null) { throw Exception('No node found for wallet type: ${walletType.toString()}'); } @@ -1007,6 +1002,7 @@ abstract class SettingsStoreBase with Store { final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1015,6 +1011,7 @@ abstract class SettingsStoreBase with Store { final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); + final decredNode = nodeSource.get(decredNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); @@ -1100,6 +1097,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.wownero] = wowneroNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1334,6 +1335,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.bitcoinCash] = bitcoinCash!.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); } + if (decred != null && + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1442,6 +1448,7 @@ abstract class SettingsStoreBase with Store { final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1453,6 +1460,7 @@ abstract class SettingsStoreBase with Store { final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); + final decredNode = nodeSource.get(decredNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1497,6 +1505,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.wownero] = wowneroNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1633,6 +1645,9 @@ abstract class SettingsStoreBase with Store { case WalletType.wownero: await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); break; + case WalletType.decred: + await _sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 2721fd7b3..34e8bdf0d 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -8,6 +8,8 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/node.dart'; import 'package:cake_wallet/entities/node_list.dart'; import 'package:cake_wallet/entities/default_settings_migration.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; part 'node_list_view_model.g.dart'; @@ -47,6 +49,9 @@ abstract class NodeListViewModelBase with Store { Future<void> reset() async { await resetToDefault(_nodeSource); + final decredNode = getDecredDefaultNode(nodes: _nodeSource)!; + final sharedPrefs = getIt.get<SharedPreferences>(); + await setDefaultDecredNodeKey(sharedPrefs, _nodeSource); Node node; @@ -88,6 +93,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.wownero: node = getWowneroDefaultNode(nodes: _nodeSource); break; + case WalletType.decred: + node = decredNode; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); }