From 3293aade1cc5938f38a72fafaf8f85666faec61a Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Fri, 16 May 2025 21:14:55 +0300 Subject: [PATCH 1/9] feat: paper wallet import --- .../svg/stack_wallet/paperwallet.svg | 8 ++ .../add_wallet_view/add_wallet_view.dart | 62 ++++++++++++++ lib/utilities/address_utils.dart | 82 +++++++++++++++++++ lib/utilities/assets.dart | 1 + lib/wallets/crypto_currency/coins/monero.dart | 63 ++++++++++++++ .../crypto_currency/crypto_currency.dart | 9 ++ 6 files changed, 225 insertions(+) create mode 100644 asset_sources/svg/stack_wallet/paperwallet.svg diff --git a/asset_sources/svg/stack_wallet/paperwallet.svg b/asset_sources/svg/stack_wallet/paperwallet.svg new file mode 100644 index 000000000..efb36b790 --- /dev/null +++ b/asset_sources/svg/stack_wallet/paperwallet.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 748f5074e..e5b254692 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -10,7 +10,9 @@ import 'dart:async'; +import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; @@ -24,9 +26,12 @@ import '../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; +import '../../../utilities/address_utils.dart'; import '../../../utilities/assets.dart'; +import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/default_eth_tokens.dart'; +import '../../../utilities/logger.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; @@ -40,6 +45,7 @@ import '../../../widgets/icon_widgets/x_icon.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_text_field.dart'; import '../../../widgets/textfield_icon_button.dart'; +import '../../wallet_view/wallet_view.dart'; import '../add_token_view/add_custom_token_view.dart'; import '../add_token_view/sub_widgets/add_custom_token_selector.dart'; import 'sub_widgets/add_wallet_text.dart'; @@ -126,6 +132,31 @@ class _AddWalletViewState extends ConsumerState { } } + Future scanPaperWalletQr() async { + try { + final qrResult = await const BarcodeScannerWrapper().scan(); + + final results = AddressUtils.parseWalletUri(qrResult.rawContent, logging: Logging.instance); + + if (results != null) { + final wallet = await results.coin.importPaperWallet(results, ref); + if (mounted) { + await Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: wallet.walletId, + ); + } + } + } on PlatformException catch (e, s) { + // likely failed to get camera permissions + Logging.instance.e( + "Restore wallet qr scan failed: $e", + error: e, + stackTrace: s, + ); + } + } + @override void initState() { _searchFieldController = TextEditingController(); @@ -343,6 +374,37 @@ class _AddWalletViewState extends ConsumerState { Navigator.of(context).pop(); }, ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + semanticsLabel: + "Paper Wallet Import Button. Imports your paper wallet to Stack Wallet.", + key: const Key("restoreWalletImportPaperWalletButton"), + size: 36, + shadows: const [], + color: Theme.of(context) + .extension()! + .background, + icon: SvgPicture.asset( + Assets.svg.paperWallet, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + onPressed: scanPaperWalletQr, + ), + ), + ), + ], ), body: Container( color: Theme.of(context).extension()!.background, diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 171dc09b2..267ada0b9 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -10,6 +10,8 @@ import 'dart:convert'; +import 'package:logger/logger.dart'; + import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'logger.dart'; @@ -160,6 +162,41 @@ class AddressUtils { } } + /// Parses a wallet URI and returns a WalletUriData object. + /// + /// Returns null on failure to parse. + static WalletUriData? parseWalletUri(String uri, {Logging? logging}) { + String scheme = ""; + Map parsedData = {}; + if (uri.split(":")[0].contains("_")) { // We need to check if the uri is compatible because RFC 3986 does not allow underscores in the scheme + final String compatibleUri = uri.replaceFirst("_", ""); + scheme = uri.split(":")[0]; + parsedData = _parseUri(compatibleUri); + parsedData.remove("scheme"); + } else { + parsedData = _parseUri(uri); + scheme = parsedData['scheme'] ?? ''; + parsedData.remove('scheme'); + } + + final CryptoCurrency? coin = AppConfig.coins.map((e) => "${e.uriScheme}_wallet").toSet().contains(scheme) ? + AppConfig.coins.firstWhere((e) => "${e.uriScheme}_wallet".contains(scheme)) : null; + + if (coin == null) { + return null; + } + + return WalletUriData( + coin: coin, + address: parsedData['address']?.trim(), + seed: parsedData['seed'] ?? parsedData['mnemonic'], + spendKey: parsedData['spend_key'], + viewKey: parsedData['view_key'], + height: int.tryParse(parsedData['height'] ?? ''), + txids: parsedData['txids']?.split(',') ?? parsedData['txid']?.split(','), + ); + } + /// Builds a uri string with the given address and query parameters (if any) static String buildUriString( String scheme, @@ -284,3 +321,48 @@ class PaymentUriData { "additionalParams: $additionalParams" " }"; } + +class WalletUriData { + final CryptoCurrency coin; + final String? address; + final String? seed; + final String? spendKey; + final String? viewKey; + final int? height; + final List? txids; + + WalletUriData({ + required this.coin, + this.address, + this.seed, + this.spendKey, + this.viewKey, + this.height, + this.txids, + }); + + @override + String toString() { + return "WalletUriData { " + "coin: $coin, " + "address: $address, " + "seed: $seed, " + "spendKey: $spendKey, " + "viewKey: $viewKey, " + "height: $height, " + "txids: $txids" + " }"; + } + + String toJson() { + return jsonEncode({ + "coin": coin.prettyName, + "address": address, + "seed": seed, + "spendKey": spendKey, + "viewKey": viewKey, + "height": height, + "txids": txids, + }); + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 73c520410..21c6c8218 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -144,6 +144,7 @@ class _SVG { String get checkCircle => "assets/svg/circle-check.svg"; String get clipboard => "assets/svg/clipboard.svg"; String get qrcode => "assets/svg/qrcode1.svg"; + String get paperWallet => "assets/svg/paperwallet.svg"; String get ellipsis => "assets/svg/gear-3.svg"; String get chevronDown => "assets/svg/chevron-down.svg"; String get chevronUp => "assets/svg/chevron-up.svg"; diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 4cbeeeb68..508f825b0 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -1,9 +1,21 @@ import 'package:cs_monero/src/ffi_bindings/monero_wallet_bindings.dart' as xmr_wallet_ffi; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/node_model.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/node_service_provider.dart'; +import '../../../providers/global/prefs_provider.dart'; +import '../../../providers/global/secure_store_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../utilities/address_utils.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; +import '../../wallet/impl/monero_wallet.dart'; +import '../../wallet/wallet.dart'; import '../crypto_currency.dart'; import '../intermediate/cryptonote_currency.dart'; @@ -122,6 +134,57 @@ class Monero extends CryptonoteCurrency { } } + @override + Future> importPaperWallet(WalletUriData walletData, WidgetRef ref) async { + try { + if (walletData.txids != null) { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + + } else { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref.read(mainDBProvider).isar); + ref.read(pWallets).addWallet(wallet); + return wallet; + } + } catch (e) { + throw Exception("Failed to import paper wallet: $e"); + } + } + static const sixteenWordsWordList = { "abandon", "ability", diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index d5553ceca..23cd25b26 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -1,6 +1,10 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/node_model.dart'; +import '../../utilities/address_utils.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../wallet/wallet.dart'; export 'coins/banano.dart'; export 'coins/bitcoin.dart'; @@ -91,4 +95,9 @@ abstract class CryptoCurrency { @override int get hashCode => Object.hash(runtimeType, network); + + Future importPaperWallet(WalletUriData walletData, WidgetRef ref) async { + throw UnimplementedError( + "Paper wallet import not implemented for $identifier",); + } } From a1d5d7013696960c1bb7e2458d27e38c0fcee802 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Sun, 25 May 2025 20:38:17 +0300 Subject: [PATCH 2/9] feat: monero gift wallet --- .../add_wallet_view/add_wallet_view.dart | 51 ++++++- lib/wallets/crypto_currency/coins/monero.dart | 132 +++++++++++++----- .../crypto_currency/crypto_currency.dart | 32 ++++- .../intermediate/lib_monero_wallet.dart | 12 ++ 4 files changed, 183 insertions(+), 44 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index e5b254692..869e7da41 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -139,12 +139,53 @@ class _AddWalletViewState extends ConsumerState { final results = AddressUtils.parseWalletUri(qrResult.rawContent, logging: Logging.instance); if (results != null) { - final wallet = await results.coin.importPaperWallet(results, ref); if (mounted) { - await Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: wallet.walletId, - ); + unawaited(showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (_) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox( + height: 16, + ), + Text( + "Importing ${results.coin.prettyName} Paper Wallet", + style: STextStyles.field(context).copyWith( + fontSize: 16, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ], + ) + ), + ); + }, + )); + final wallet = await results.coin.importPaperWallet(results, ref); + if (mounted) { + Navigator.pop(context); + await Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: wallet.walletId, + ); + } } } } on PlatformException catch (e, s) { diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 508f825b0..804f0efd8 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -1,20 +1,30 @@ +import 'dart:io'; + +import 'package:compat/old_cw_core/wallet_type.dart'; +import 'package:cs_monero/cs_monero.dart' as cs_monero; import 'package:cs_monero/src/ffi_bindings/monero_wallet_bindings.dart' as xmr_wallet_ffi; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/io_client.dart'; +import 'package:monero_rpc/monero_rpc.dart'; +import '../../../models/isar/models/isar_models.dart'; import '../../../models/node_model.dart'; import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/node_service_provider.dart'; import '../../../providers/global/prefs_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/global/wallets_provider.dart'; +import '../../../services/node_service.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../isar/models/wallet_info.dart'; +import '../../isar/providers/wallet_info_provider.dart'; import '../../models/tx_data.dart'; import '../../wallet/impl/monero_wallet.dart'; +import '../../wallet/intermediate/lib_monero_wallet.dart'; import '../../wallet/wallet.dart'; import '../crypto_currency.dart'; import '../intermediate/cryptonote_currency.dart'; @@ -136,52 +146,100 @@ class Monero extends CryptonoteCurrency { @override Future> importPaperWallet(WalletUriData walletData, WidgetRef ref) async { - try { - if (walletData.txids != null) { - final info = WalletInfo.createNew( + if (walletData.txids != null) { + final wallet = await Wallet.create( + walletInfo: WalletInfo.createNew( coin: walletData.coin, name: "${walletData.coin.prettyName} Paper Wallet", restoreHeight: walletData.height ?? 0, otherDataJsonString: walletData.toJson(), - ); + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); + // Scan the blocks with given txids + final primaryNode = defaultNode; + final daemonRpc = DaemonRpc( + IOClient(HttpClient()), "${primaryNode.host}:${primaryNode.port}"); + final txs = await daemonRpc.postToEndpoint("/get_transactions", { + "txs_hashes": walletData.txids, + "decode_as_json": true, + }); + for (final tx in txs["txs"] as List) { + wallet.onSyncingUpdate(syncHeight: tx["block_height"] as int, + nodeHeight: wallet.currentKnownChainHeight); + await wallet.waitForBalanceChange(); + } - - } else { - final info = WalletInfo.createNew( + // Set the sync height to the current known chain height - make the wallet synced + wallet.onSyncingUpdate(syncHeight: wallet.currentKnownChainHeight, + nodeHeight: wallet.currentKnownChainHeight); + + // Create the new wallet info + final newWallet = await Wallet.create( + walletInfo: WalletInfo.createNew( coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); + name: "${walletData.coin.prettyName} Gift Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonic: null, + mnemonicPassphrase: null, + privateKey: null, + ); + await (newWallet as MoneroWallet).init(wordCount: 16); + await newWallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(newWallet); - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); + await newWallet.open(); + await newWallet.generateNewReceivingAddress(); - await (wallet as MoneroWallet).init(isRestore: true); - await wallet.recover(isRescan: false); - await wallet.info.setMnemonicVerified(isar: ref.read(mainDBProvider).isar); - ref.read(pWallets).addWallet(wallet); - return wallet; - } - } catch (e) { - throw Exception("Failed to import paper wallet: $e"); + // Send the balance to the new wallet + final txData = await wallet.prepareSend(txData: TxData( + recipients: [ + (address: (await newWallet.getCurrentReceivingAddress())!.value, amount: (await wallet.totalBalance), isChange: false) + ], + feeRateType: FeeRateType.average, + )); + await wallet.confirmSend(txData: txData); + + return newWallet; + } else { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(wallet); + return wallet; } } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 23cd25b26..c01e6b68e 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -2,8 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/node_model.dart'; +import '../../providers/db/main_db_provider.dart'; +import '../../providers/global/node_service_provider.dart'; +import '../../providers/global/prefs_provider.dart'; +import '../../providers/global/secure_store_provider.dart'; +import '../../providers/global/wallets_provider.dart'; import '../../utilities/address_utils.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../isar/models/wallet_info.dart'; import '../wallet/wallet.dart'; export 'coins/banano.dart'; @@ -97,7 +103,29 @@ abstract class CryptoCurrency { int get hashCode => Object.hash(runtimeType, network); Future importPaperWallet(WalletUriData walletData, WidgetRef ref) async { - throw UnimplementedError( - "Paper wallet import not implemented for $identifier",); + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + await wallet.init(); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(wallet); + return wallet; } } diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index b81a10d97..a2fd9a18d 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -798,6 +798,15 @@ abstract class LibMoneroWallet } } + Completer _balanceChangeCompleter = Completer(); + + Future waitForBalanceChange() async { + if (_balanceChangeCompleter.isCompleted) { + _balanceChangeCompleter = Completer(); + } + return _balanceChangeCompleter.future; + } + void onBalancesChanged({ required BigInt newBalance, required BigInt newUnlockedBalance, @@ -805,6 +814,9 @@ abstract class LibMoneroWallet try { await updateBalance(); await updateTransactions(); + if (newBalance != BigInt.zero && !_balanceChangeCompleter.isCompleted) { + _balanceChangeCompleter.complete(); + } } catch (e, s) { Logging.instance.w("onBalancesChanged(): ", error: e, stackTrace: s); } From 9859e529e5dd23f6eecf0b32fa949e04aadf4ce2 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Sat, 31 May 2025 18:05:59 +0300 Subject: [PATCH 3/9] fix: use tor, display showLoading and use primary node --- .../add_wallet_view/add_wallet_view.dart | 51 +++++-------------- lib/wallets/crypto_currency/coins/monero.dart | 38 +++++++++++--- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index c859f9703..f1a3a392a 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -32,9 +32,11 @@ import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/default_eth_tokens.dart'; import '../../../utilities/logger.dart'; +import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/background.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; @@ -139,50 +141,21 @@ class _AddWalletViewState extends ConsumerState { if (results != null) { if (mounted) { - unawaited(showModalBottomSheet( - backgroundColor: Colors.transparent, + final Wallet? wallet = await showLoading( + whileFuture: results.coin.importPaperWallet(results, ref), context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (_) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox( - height: 16, - ), - Text( - "Importing ${results.coin.prettyName} Paper Wallet", - style: STextStyles.field(context).copyWith( - fontSize: 16, - height: 1.5, - ), - textAlign: TextAlign.center, - ), - ], - ) - ), - ); - }, - )); - final wallet = await results.coin.importPaperWallet(results, ref); + message: "Importing paper wallet...", + ); + if (wallet == null) { + throw Exception( + "Failed to import paper wallet because wallet from importPaperWallet is null: ${results.coin.prettyName}", + ); + } if (mounted) { Navigator.pop(context); await Navigator.of(context).pushNamed( WalletView.routeName, - arguments: wallet.walletId, + arguments: wallet!.walletId, ); } } diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 804f0efd8..72c469bcd 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -1,30 +1,30 @@ import 'dart:io'; -import 'package:compat/old_cw_core/wallet_type.dart'; -import 'package:cs_monero/cs_monero.dart' as cs_monero; import 'package:cs_monero/src/ffi_bindings/monero_wallet_bindings.dart' as xmr_wallet_ffi; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/io_client.dart'; import 'package:monero_rpc/monero_rpc.dart'; +import 'package:socks5_proxy/socks.dart'; -import '../../../models/isar/models/isar_models.dart'; import '../../../models/node_model.dart'; import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/node_service_provider.dart'; import '../../../providers/global/prefs_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/global/wallets_provider.dart'; +import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/node_service.dart'; +import '../../../services/tor_service.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; import '../../isar/models/wallet_info.dart'; -import '../../isar/providers/wallet_info_provider.dart'; import '../../models/tx_data.dart'; import '../../wallet/impl/monero_wallet.dart'; -import '../../wallet/intermediate/lib_monero_wallet.dart'; import '../../wallet/wallet.dart'; import '../crypto_currency.dart'; import '../intermediate/cryptonote_currency.dart'; @@ -147,6 +147,7 @@ class Monero extends CryptonoteCurrency { @override Future> importPaperWallet(WalletUriData walletData, WidgetRef ref) async { if (walletData.txids != null) { + // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet final wallet = await Wallet.create( walletInfo: WalletInfo.createNew( coin: walletData.coin, @@ -164,10 +165,31 @@ class Monero extends CryptonoteCurrency { await (wallet as MoneroWallet).init(isRestore: true); await wallet.recover(isRescan: false); - // Scan the blocks with given txids - final primaryNode = defaultNode; + final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) + .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode; + + // Create an HTTP client with Tor support if enabled + final torService = TorService.sharedInstance; + final prefs = Prefs.instance; + final httpClient = HttpClient(); + if (prefs.useTor) { + if (torService.status != TorConnectionStatus.connected) { + if (prefs.torKillSwitch) { + throw Exception("Tor is not connected, and the kill switch is enabled. Can't sweep gift wallet"); + } else { + // If Tor is not connected, we can still proceed with the request + Logging.instance.w("Tor is not connected, proceeding without Tor."); + } + } else { + SocksTCPClient.assignToHttpClient(httpClient, [ProxySettings(torService.getProxyInfo().host, torService.getProxyInfo().port)]); + } + } + + // Create a DaemonRpc instance to interact with the Monero node final daemonRpc = DaemonRpc( - IOClient(HttpClient()), "${primaryNode.host}:${primaryNode.port}"); + IOClient(httpClient), "${primaryNode.host}:${primaryNode.port}"); + + // Scan the blocks with given txids final txs = await daemonRpc.postToEndpoint("/get_transactions", { "txs_hashes": walletData.txids, "decode_as_json": true, From 2a6349c5c5bda13e8c7176ae599cd5334d8aefac Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:39:16 +0300 Subject: [PATCH 4/9] feat: gift mnemonic confirmation --- .../add_wallet_view/add_wallet_view.dart | 325 +++++++++++++++++- lib/wallets/crypto_currency/coins/monero.dart | 197 +++++------ .../crypto_currency/crypto_currency.dart | 60 ++-- .../intermediate/lib_monero_wallet.dart | 1 + 4 files changed, 442 insertions(+), 141 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index f1a3a392a..124eaa3b8 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -9,13 +9,16 @@ */ import 'dart:async'; +import 'dart:math'; import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; +import 'package:tuple/tuple.dart'; import '../../../app_config.dart'; import '../../../db/isar/main_db.dart'; @@ -24,6 +27,7 @@ import '../../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import '../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; @@ -36,12 +40,16 @@ import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../wallets/isar/models/wallet_info.dart'; +import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/background.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; +import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/expandable.dart'; import '../../../widgets/icon_widgets/x_icon.dart'; import '../../../widgets/rounded_white_container.dart'; @@ -50,6 +58,8 @@ import '../../../widgets/textfield_icon_button.dart'; import '../../wallet_view/wallet_view.dart'; import '../add_token_view/add_custom_token_view.dart'; import '../add_token_view/sub_widgets/add_custom_token_selector.dart'; +import '../new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import '../verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'sub_widgets/add_wallet_text.dart'; import 'sub_widgets/expanding_sub_list_item.dart'; import 'sub_widgets/next_button.dart'; @@ -133,6 +143,38 @@ class _AddWalletViewState extends ConsumerState { } } + Tuple2, String> randomize( + List mnemonic, + int chosenIndex, + int wordsToShow, + ) { + final List remaining = []; + final String chosenWord = mnemonic[chosenIndex]; + + for (int i = 0; i < mnemonic.length; i++) { + if (chosenWord != mnemonic[i]) { + remaining.add(mnemonic[i]); + } + } + + final random = Random(); + + final List result = []; + + for (int i = 0; i < wordsToShow - 1; i++) { + final randomIndex = random.nextInt(remaining.length); + result.add(remaining.removeAt(randomIndex)); + } + + result.insert(random.nextInt(wordsToShow), chosenWord); + + if (kDebugMode) { + print("Mnemonic game correct word: $chosenWord"); + } + + return Tuple2(result, chosenWord); + } + Future scanPaperWalletQr() async { try { final qrResult = await const BarcodeScannerWrapper().scan(); @@ -140,23 +182,280 @@ class _AddWalletViewState extends ConsumerState { final results = AddressUtils.parseWalletUri(qrResult.rawContent, logging: Logging.instance); if (results != null) { - if (mounted) { - final Wallet? wallet = await showLoading( - whileFuture: results.coin.importPaperWallet(results, ref), - context: context, - message: "Importing paper wallet...", + if (results.coin == Monero(CryptoCurrencyNetwork.main) && results.txids != null) { + // Mnemonic for the wallet to sweep into is shown and gets confirmed + // Create the new wallet info + final newWallet = await Wallet.create( + walletInfo: WalletInfo.createNew( + coin: results.coin, + name: "${results.coin.prettyName} Gift Wallet ${results.address != null ? '(${results.address!.substring(results.address!.length - 4)})' : ''}", + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonic: null, + mnemonicPassphrase: null, + privateKey: null, ); - if (wallet == null) { - throw Exception( - "Failed to import paper wallet because wallet from importPaperWallet is null: ${results.coin.prettyName}", - ); + await (newWallet as MoneroWallet).init(wordCount: 16); + final mnemonic = (await newWallet.getMnemonic()).split(" "); + if (mounted) { + final hasWroteDown = await showDialog(context: context, barrierDismissible: false, builder: (context) { + return Dialog( + insetPadding: const EdgeInsets.all(16), // This may seem too much, but its needed for the dialog to show the mnemonic table properly + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.background, + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Monero Gift Wallet Redeem", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + "You are about to redeem the gift into a wallet with the following mnemonic phrase. Please write down this words. You will be asked to verify the mnemonic phrase after you have written it down.", + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.label(context).copyWith(fontSize: 12), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + MnemonicTable(words: mnemonic, isDesktop: isDesktop), + const SizedBox(height: 16), + PrimaryButton( + label: "I have written down the mnemonic", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ), + ), + ); + }) ?? false; + if (hasWroteDown) { + // Verify if checked + final chosenIndex = Random().nextInt(mnemonic.length); + final words = randomize(mnemonic, chosenIndex, 3); + if (mounted) { + final hasVerified = await showDialog(context: context, builder: (context) { + return Dialog( + insetPadding: const EdgeInsets.all(16), // This may seem too much, but its needed for the dialog to show the mnemonic table properly + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.background, + borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Monero Gift Wallet Redeem", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + "Verify recovery phrase", + textAlign: TextAlign.center, + style: + isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.label(context).copyWith(fontSize: 12), + ), + const SizedBox(height: 16), + Text( + isDesktop ? "Select word number" : "Tap word number ", + textAlign: TextAlign.center, + style: + isDesktop + ? STextStyles.desktopSubtitleH1(context) + : STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 16), + Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + child: Text( + "${chosenIndex + 1}", + textAlign: TextAlign.center, + style: STextStyles.subtitle600( + context, + ).copyWith(fontSize: 32, letterSpacing: 0.25), + ), + ), + ), + const SizedBox(height: 16), + Column( + children: [ + for (int i = 0; i < words.item1.length; i++) + Padding( + padding: EdgeInsets.symmetric( + vertical: isDesktop ? 8 : 5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isDesktop) ...[ + const SizedBox(width: 10), + ], + Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: MaterialButton( + splashColor: Theme.of(context).extension()!.highlight, + padding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 18, + horizontal: 12, + ) + : const EdgeInsets.all(12), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + onPressed: () { + final word = words.item1[i]; + final wordIndex = mnemonic.indexOf(word); + if (wordIndex == chosenIndex) { + Navigator.of(context).pop(true); + } else { + Navigator.of(context).pop(false); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + words.item1[i], + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.baseXS(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + ) + ], + ), + ), + ], + ) + ], + ), + ), + ); + }) ?? false; + if (hasVerified) { + if (mounted) { + final wallet = await showLoading( + whileFuture: (() async { + await newWallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(newWallet); + await newWallet.open(); + await newWallet.generateNewReceivingAddress(); + return results.coin.importPaperWallet(results, ref, newWallet: newWallet); + })(), + context: context, + message: "Importing paper wallet...", + ); + if (wallet == null) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Failed to import paper wallet for ${results.coin.prettyName}. Please try again.", + ), + ), + ); + } + return; + } + if (mounted) { + Navigator.pop(context); + await Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: wallet.walletId, + ); + } + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Mnemonic verification failed. Please try again.", + ), + ), + ); + } + } + } + } } + } else { if (mounted) { - Navigator.pop(context); - await Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: wallet!.walletId, + final wallet = await showLoading( + whileFuture: results.coin.importPaperWallet(results, ref), + context: context, + message: "Importing paper wallet...", ); + if (wallet == null) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Failed to import paper wallet for ${results.coin.prettyName}. Please try again.", + ), + ), + ); + } + return; + } + if (mounted) { + Navigator.pop(context); + await Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: wallet.walletId, + ); + } } } } diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 72c469bcd..36d61baa1 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -145,123 +145,114 @@ class Monero extends CryptonoteCurrency { } @override - Future> importPaperWallet(WalletUriData walletData, WidgetRef ref) async { - if (walletData.txids != null) { - // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet - final wallet = await Wallet.create( - walletInfo: WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ), - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); - await (wallet as MoneroWallet).init(isRestore: true); - await wallet.recover(isRescan: false); + Future?> importPaperWallet(WalletUriData walletData, WidgetRef ref, {Wallet? newWallet}) async { + try { + if (walletData.txids != null) { + // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet + final wallet = await Wallet.create( + walletInfo: WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); - final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) - .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode; + final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) + .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode; - // Create an HTTP client with Tor support if enabled - final torService = TorService.sharedInstance; - final prefs = Prefs.instance; - final httpClient = HttpClient(); - if (prefs.useTor) { - if (torService.status != TorConnectionStatus.connected) { - if (prefs.torKillSwitch) { - throw Exception("Tor is not connected, and the kill switch is enabled. Can't sweep gift wallet"); + // Create an HTTP client with Tor support if enabled + final torService = TorService.sharedInstance; + final prefs = Prefs.instance; + final httpClient = HttpClient(); + if (prefs.useTor) { + if (torService.status != TorConnectionStatus.connected) { + if (prefs.torKillSwitch) { + throw Exception("Tor is not connected, and the kill switch is enabled. Can't sweep gift wallet"); + } else { + // If Tor is not connected, we can still proceed with the request + Logging.instance.w("Tor is not connected, proceeding without Tor."); + } } else { - // If Tor is not connected, we can still proceed with the request - Logging.instance.w("Tor is not connected, proceeding without Tor."); + SocksTCPClient.assignToHttpClient(httpClient, [ProxySettings(torService.getProxyInfo().host, torService.getProxyInfo().port)]); } - } else { - SocksTCPClient.assignToHttpClient(httpClient, [ProxySettings(torService.getProxyInfo().host, torService.getProxyInfo().port)]); } - } - // Create a DaemonRpc instance to interact with the Monero node - final daemonRpc = DaemonRpc( - IOClient(httpClient), "${primaryNode.host}:${primaryNode.port}"); + // Create a DaemonRpc instance to interact with the Monero node + final daemonRpc = DaemonRpc( + IOClient(httpClient), "${primaryNode.host}:${primaryNode.port}"); - // Scan the blocks with given txids - final txs = await daemonRpc.postToEndpoint("/get_transactions", { - "txs_hashes": walletData.txids, - "decode_as_json": true, - }); - for (final tx in txs["txs"] as List) { - wallet.onSyncingUpdate(syncHeight: tx["block_height"] as int, - nodeHeight: wallet.currentKnownChainHeight); - await wallet.waitForBalanceChange(); - } + // Scan the blocks with given txids + final txs = await daemonRpc.postToEndpoint("/get_transactions", { + "txs_hashes": walletData.txids, + "decode_as_json": true, + }); - // Set the sync height to the current known chain height - make the wallet synced - wallet.onSyncingUpdate(syncHeight: wallet.currentKnownChainHeight, - nodeHeight: wallet.currentKnownChainHeight); + for (final tx in txs["txs"] as List) { + wallet.onSyncingUpdate(syncHeight: tx["block_height"] as int, + nodeHeight: wallet.currentKnownChainHeight); + await wallet.waitForBalanceChange(); + } - // Create the new wallet info - final newWallet = await Wallet.create( - walletInfo: WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Gift Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - ), - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonic: null, - mnemonicPassphrase: null, - privateKey: null, - ); - await (newWallet as MoneroWallet).init(wordCount: 16); - await newWallet.info.setMnemonicVerified(isar: ref - .read(mainDBProvider) - .isar); - ref.read(pWallets).addWallet(newWallet); + // Set the sync height to the current known chain height - make the wallet synced + wallet.onSyncingUpdate(syncHeight: wallet.currentKnownChainHeight, + nodeHeight: wallet.currentKnownChainHeight); - await newWallet.open(); - await newWallet.generateNewReceivingAddress(); + if (newWallet == null) { + throw Exception("newWallet must be provided when importing a paper wallet with txids"); + } - // Send the balance to the new wallet - final txData = await wallet.prepareSend(txData: TxData( - recipients: [ - (address: (await newWallet.getCurrentReceivingAddress())!.value, amount: (await wallet.totalBalance), isChange: false) - ], - feeRateType: FeeRateType.average, - )); - await wallet.confirmSend(txData: txData); + // Send the balance to the new wallet + final txData = await wallet.prepareSend(txData: TxData( + recipients: [ + (address: (await newWallet.getCurrentReceivingAddress())!.value, amount: (await wallet.totalBalance), isChange: false) + ], + feeRateType: FeeRateType.average, + )); + await wallet.confirmSend(txData: txData); - return newWallet; - } else { - final info = WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); + return newWallet; + } else { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); - await (wallet as MoneroWallet).init(isRestore: true); - await wallet.recover(isRescan: false); - await wallet.info.setMnemonicVerified(isar: ref - .read(mainDBProvider) - .isar); - ref.read(pWallets).addWallet(wallet); - return wallet; + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(wallet); + return wallet; + } + } catch (e, stackTrace) { + Logging.instance.e( + "Error importing paper wallet: $e", + error: e, + stackTrace: stackTrace, + ); + return null; } } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 2c12748c0..210c28842 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -9,6 +9,7 @@ import '../../providers/global/secure_store_provider.dart'; import '../../providers/global/wallets_provider.dart'; import '../../utilities/address_utils.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../../utilities/logger.dart'; import '../isar/models/wallet_info.dart'; import '../wallet/wallet.dart'; @@ -104,30 +105,39 @@ abstract class CryptoCurrency { @override int get hashCode => Object.hash(runtimeType, network); - Future importPaperWallet(WalletUriData walletData, WidgetRef ref) async { - final info = WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); - - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); - - await wallet.init(); - await wallet.recover(isRescan: false); - await wallet.info.setMnemonicVerified(isar: ref - .read(mainDBProvider) - .isar); - ref.read(pWallets).addWallet(wallet); - return wallet; + Future importPaperWallet(WalletUriData walletData, WidgetRef ref, {Wallet? newWallet}) async { + try { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + await wallet.init(); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(wallet); + return wallet; + } catch (e, stackTrace) { + Logging.instance.e( + "Error importing paper wallet: $e", + error: e, + stackTrace: stackTrace, + ); + return null; + } } } diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index a2fd9a18d..a2c17b0b9 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -814,6 +814,7 @@ abstract class LibMoneroWallet try { await updateBalance(); await updateTransactions(); + // TODO: Implement a better balance change detection, not by onBalancesChanged if (newBalance != BigInt.zero && !_balanceChangeCompleter.isCompleted) { _balanceChangeCompleter.complete(); } From b33d2755bb2f3581c3722fc12323d407e099c198 Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:47:44 +0300 Subject: [PATCH 5/9] refactor: exit and delete wallets after use --- .../add_wallet_views/add_wallet_view/add_wallet_view.dart | 6 ++++++ lib/wallets/crypto_currency/coins/monero.dart | 2 ++ 2 files changed, 8 insertions(+) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 124eaa3b8..5d890ff6f 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -35,6 +35,7 @@ import '../../../utilities/assets.dart'; import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/default_eth_tokens.dart'; +import '../../../utilities/flutter_secure_storage_interface.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; @@ -397,6 +398,8 @@ class _AddWalletViewState extends ConsumerState { message: "Importing paper wallet...", ); if (wallet == null) { + await newWallet.exit(); + await ref.read(pWallets).deleteWallet(newWallet.info, ref.read(secureStoreProvider)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -417,6 +420,7 @@ class _AddWalletViewState extends ConsumerState { } } } else { + await newWallet.exit(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -428,6 +432,8 @@ class _AddWalletViewState extends ConsumerState { } } } + } else { + await newWallet.exit(); } } } else { diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 36d61baa1..deb71cdd2 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -219,6 +219,8 @@ class Monero extends CryptonoteCurrency { )); await wallet.confirmSend(txData: txData); + await wallet.exit(); + return newWallet; } else { final info = WalletInfo.createNew( From 1f528e015a83760015ee8bf946e9419344e2ed2a Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:18:24 +0300 Subject: [PATCH 6/9] fix: flushbar, scanner and logging uses app-wide flushbar refactors scanner as the latest removes unused logging --- .../add_wallet_view/add_wallet_view.dart | 45 +++++++++++-------- lib/utilities/address_utils.dart | 4 +- lib/wallets/crypto_currency/coins/monero.dart | 2 +- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 5d890ff6f..1d3199a2b 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -26,6 +26,7 @@ import '../../../models/add_wallet_list_entity/add_wallet_list_entity.dart'; import '../../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import '../../../models/isar/models/ethereum/eth_contract.dart'; +import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; @@ -178,9 +179,9 @@ class _AddWalletViewState extends ConsumerState { Future scanPaperWalletQr() async { try { - final qrResult = await const BarcodeScannerWrapper().scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(); - final results = AddressUtils.parseWalletUri(qrResult.rawContent, logging: Logging.instance); + final results = AddressUtils.parseWalletUri(qrResult.rawContent); if (results != null) { if (results.coin == Monero(CryptoCurrencyNetwork.main) && results.txids != null) { @@ -401,12 +402,11 @@ class _AddWalletViewState extends ConsumerState { await newWallet.exit(); await ref.read(pWallets).deleteWallet(newWallet.info, ref.read(secureStoreProvider)); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Failed to import paper wallet for ${results.coin.prettyName}. Please try again.", - ), - ), + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to import gift wallet for ${results.coin.prettyName}. Please try again.", + context: context) ); } return; @@ -422,11 +422,11 @@ class _AddWalletViewState extends ConsumerState { } else { await newWallet.exit(); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Mnemonic verification failed. Please try again.", - ), + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to verify mnemonic phrase for the new wallet. Please try again.", + context: context, ), ); } @@ -445,11 +445,11 @@ class _AddWalletViewState extends ConsumerState { ); if (wallet == null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Failed to import paper wallet for ${results.coin.prettyName}. Please try again.", - ), + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to import paper wallet for ${results.coin.prettyName}. Please try again.", + context: context, ), ); } @@ -472,6 +472,15 @@ class _AddWalletViewState extends ConsumerState { error: e, stackTrace: s, ); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to import paper wallet. Please try again.", + context: context, + ), + ); + } } } diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 3eafa1691..99a58f286 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -10,8 +10,6 @@ import 'dart:convert'; -import 'package:logger/logger.dart'; - import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; import 'logger.dart'; @@ -179,7 +177,7 @@ class AddressUtils { /// Parses a wallet URI and returns a WalletUriData object. /// /// Returns null on failure to parse. - static WalletUriData? parseWalletUri(String uri, {Logging? logging}) { + static WalletUriData? parseWalletUri(String uri) { String scheme = ""; Map parsedData = {}; if (uri.split(":")[0].contains("_")) { // We need to check if the uri is compatible because RFC 3986 does not allow underscores in the scheme diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 46b12d410..af4bc949b 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -169,7 +169,7 @@ class Monero extends CryptonoteCurrency { await wallet.recover(isRescan: false); final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) - .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode; + .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode(isPrimary: true); // Create an HTTP client with Tor support if enabled final torService = TorService.sharedInstance; From 810aa3f1a71a1f2b5e4cbd88656b7d23e76068af Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:33:46 +0300 Subject: [PATCH 7/9] refactor: use simple functions instead of a cryptocurrency function --- .../add_wallet_view/add_wallet_view.dart | 144 +++++++++++++++++- lib/wallets/crypto_currency/coins/monero.dart | 114 -------------- .../crypto_currency/crypto_currency.dart | 36 ----- 3 files changed, 137 insertions(+), 157 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 1d3199a2b..0ff8a7152 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -9,15 +9,18 @@ */ import 'dart:async'; +import 'dart:io'; import 'dart:math'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:http/io_client.dart'; import 'package:isar/isar.dart'; +import 'package:monero_rpc/monero_rpc.dart'; +import 'package:socks5_proxy/socks.dart'; import 'package:tuple/tuple.dart'; import '../../../app_config.dart'; @@ -30,24 +33,27 @@ import '../../../notifications/show_flush_bar.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/providers.dart'; +import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; +import '../../../services/node_service.dart'; +import '../../../services/tor_service.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/address_utils.dart'; import '../../../utilities/assets.dart'; -import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/default_eth_tokens.dart'; -import '../../../utilities/flutter_secure_storage_interface.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/models/wallet_info.dart'; +import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/background.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; @@ -61,7 +67,6 @@ import '../../wallet_view/wallet_view.dart'; import '../add_token_view/add_custom_token_view.dart'; import '../add_token_view/sub_widgets/add_custom_token_selector.dart'; import '../new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; -import '../verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'sub_widgets/add_wallet_text.dart'; import 'sub_widgets/expanding_sub_list_item.dart'; import 'sub_widgets/next_button.dart'; @@ -177,6 +182,131 @@ class _AddWalletViewState extends ConsumerState { return Tuple2(result, chosenWord); } + // Imports the given walletData, returns the walletId of the imported wallet + // TODO: Implement wallet creation service and move this logic there + Future?> importPaperWallet(WalletUriData walletData, {Wallet? newWallet}) async { + if (walletData.coin == Monero(CryptoCurrencyNetwork.main) && walletData.txids != null) { + // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet + try { + final wallet = await Wallet.create( + walletInfo: WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + await (wallet as MoneroWallet).init(isRestore: true); + await wallet.recover(isRescan: false); + + final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) + .getPrimaryNodeFor(currency: walletData.coin) ?? walletData.coin.defaultNode(isPrimary: true); + + // Create an HTTP client with Tor support if enabled + final torService = TorService.sharedInstance; + final prefs = Prefs.instance; + final httpClient = HttpClient(); + if (prefs.useTor) { + if (torService.status != TorConnectionStatus.connected) { + if (prefs.torKillSwitch) { + throw Exception("Tor is not connected, and the kill switch is enabled. Can't sweep gift wallet"); + } else { + // If Tor is not connected, we can still proceed with the request + Logging.instance.w("Tor is not connected, proceeding without Tor."); + } + } else { + SocksTCPClient.assignToHttpClient(httpClient, [ProxySettings(torService.getProxyInfo().host, torService.getProxyInfo().port)]); + } + } + + // Create a DaemonRpc instance to interact with the Monero node + final daemonRpc = DaemonRpc( + IOClient(httpClient), "${primaryNode.host}:${primaryNode.port}"); + + // Scan the blocks with given txids + final txs = await daemonRpc.postToEndpoint("/get_transactions", { + "txs_hashes": walletData.txids, + "decode_as_json": true, + }); + + for (final tx in txs["txs"] as List) { + wallet.onSyncingUpdate(syncHeight: tx["block_height"] as int, + nodeHeight: wallet.currentKnownChainHeight); + await wallet.waitForBalanceChange(); + } + + // Set the sync height to the current known chain height - make the wallet synced + wallet.onSyncingUpdate(syncHeight: wallet.currentKnownChainHeight, + nodeHeight: wallet.currentKnownChainHeight); + + if (newWallet == null) { + throw Exception("newWallet must be provided when importing a paper wallet with txids"); + } + + // Send the balance to the new wallet + final txData = await wallet.prepareSend(txData: TxData( + recipients: [ + (address: (await newWallet.getCurrentReceivingAddress())!.value, amount: (await wallet.totalBalance), isChange: false) + ], + feeRateType: FeeRateType.average, + )); + await wallet.confirmSend(txData: txData); + + await wallet.exit(); + + return newWallet; + } catch (e, stackTrace) { + Logging.instance.e( + "Error importing Monero gift wallet: $e", + error: e, + stackTrace: stackTrace, + ); + return null; + } + } else { + // Normal import + try { + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + + await wallet.init(); + await wallet.recover(isRescan: false); + await wallet.info.setMnemonicVerified(isar: ref + .read(mainDBProvider) + .isar); + ref.read(pWallets).addWallet(wallet); + return wallet; + } catch (e, stackTrace) { + Logging.instance.e( + "Error importing paper wallet for ${walletData.coin.prettyName}: $e", + error: e, + stackTrace: stackTrace, + ); + return null; + } + } + } + Future scanPaperWalletQr() async { try { final qrResult = await ref.read(pBarcodeScanner).scan(); @@ -393,7 +523,7 @@ class _AddWalletViewState extends ConsumerState { ref.read(pWallets).addWallet(newWallet); await newWallet.open(); await newWallet.generateNewReceivingAddress(); - return results.coin.importPaperWallet(results, ref, newWallet: newWallet); + return importPaperWallet(results, newWallet: newWallet); })(), context: context, message: "Importing paper wallet...", @@ -439,7 +569,7 @@ class _AddWalletViewState extends ConsumerState { } else { if (mounted) { final wallet = await showLoading( - whileFuture: results.coin.importPaperWallet(results, ref), + whileFuture: importPaperWallet(results), context: context, message: "Importing paper wallet...", ); diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index af4bc949b..52a969c3d 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -146,120 +146,6 @@ class Monero extends CryptonoteCurrency { } } - @override - Future?> importPaperWallet(WalletUriData walletData, WidgetRef ref, {Wallet? newWallet}) async { - try { - if (walletData.txids != null) { - // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet - final wallet = await Wallet.create( - walletInfo: WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ), - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); - await (wallet as MoneroWallet).init(isRestore: true); - await wallet.recover(isRescan: false); - - final primaryNode = NodeService(secureStorageInterface: ref.read(secureStoreProvider)) - .getPrimaryNodeFor(currency: walletData.coin) ?? defaultNode(isPrimary: true); - - // Create an HTTP client with Tor support if enabled - final torService = TorService.sharedInstance; - final prefs = Prefs.instance; - final httpClient = HttpClient(); - if (prefs.useTor) { - if (torService.status != TorConnectionStatus.connected) { - if (prefs.torKillSwitch) { - throw Exception("Tor is not connected, and the kill switch is enabled. Can't sweep gift wallet"); - } else { - // If Tor is not connected, we can still proceed with the request - Logging.instance.w("Tor is not connected, proceeding without Tor."); - } - } else { - SocksTCPClient.assignToHttpClient(httpClient, [ProxySettings(torService.getProxyInfo().host, torService.getProxyInfo().port)]); - } - } - - // Create a DaemonRpc instance to interact with the Monero node - final daemonRpc = DaemonRpc( - IOClient(httpClient), "${primaryNode.host}:${primaryNode.port}"); - - // Scan the blocks with given txids - final txs = await daemonRpc.postToEndpoint("/get_transactions", { - "txs_hashes": walletData.txids, - "decode_as_json": true, - }); - - for (final tx in txs["txs"] as List) { - wallet.onSyncingUpdate(syncHeight: tx["block_height"] as int, - nodeHeight: wallet.currentKnownChainHeight); - await wallet.waitForBalanceChange(); - } - - // Set the sync height to the current known chain height - make the wallet synced - wallet.onSyncingUpdate(syncHeight: wallet.currentKnownChainHeight, - nodeHeight: wallet.currentKnownChainHeight); - - if (newWallet == null) { - throw Exception("newWallet must be provided when importing a paper wallet with txids"); - } - - // Send the balance to the new wallet - final txData = await wallet.prepareSend(txData: TxData( - recipients: [ - (address: (await newWallet.getCurrentReceivingAddress())!.value, amount: (await wallet.totalBalance), isChange: false) - ], - feeRateType: FeeRateType.average, - )); - await wallet.confirmSend(txData: txData); - - await wallet.exit(); - - return newWallet; - } else { - final info = WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); - - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); - - await (wallet as MoneroWallet).init(isRestore: true); - await wallet.recover(isRescan: false); - await wallet.info.setMnemonicVerified(isar: ref - .read(mainDBProvider) - .isar); - ref.read(pWallets).addWallet(wallet); - return wallet; - } - } catch (e, stackTrace) { - Logging.instance.e( - "Error importing paper wallet: $e", - error: e, - stackTrace: stackTrace, - ); - return null; - } - } - static const sixteenWordsWordList = { "abandon", "ability", diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 28af4b95d..c9c147123 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -104,40 +104,4 @@ abstract class CryptoCurrency { @override int get hashCode => Object.hash(runtimeType, network); - - Future importPaperWallet(WalletUriData walletData, WidgetRef ref, {Wallet? newWallet}) async { - try { - final info = WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); - - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); - - await wallet.init(); - await wallet.recover(isRescan: false); - await wallet.info.setMnemonicVerified(isar: ref - .read(mainDBProvider) - .isar); - ref.read(pWallets).addWallet(wallet); - return wallet; - } catch (e, stackTrace) { - Logging.instance.e( - "Error importing paper wallet: $e", - error: e, - stackTrace: stackTrace, - ); - return null; - } - } } From bdf9421fce129d5228b9816596e5b4113e74a2ea Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:36:50 +0300 Subject: [PATCH 8/9] refactor: wallet exit if error occured --- .../add_wallet_view/add_wallet_view.dart | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 0ff8a7152..8a26667b7 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -187,21 +187,21 @@ class _AddWalletViewState extends ConsumerState { Future?> importPaperWallet(WalletUriData walletData, {Wallet? newWallet}) async { if (walletData.coin == Monero(CryptoCurrencyNetwork.main) && walletData.txids != null) { // If the walletData contains txids, we need to create a temporary wallet to sweep the gift wallet + final wallet = await Wallet.create( + walletInfo: WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ), + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); try { - final wallet = await Wallet.create( - walletInfo: WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ), - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); await (wallet as MoneroWallet).init(isRestore: true); await wallet.recover(isRescan: false); @@ -262,6 +262,7 @@ class _AddWalletViewState extends ConsumerState { return newWallet; } catch (e, stackTrace) { + await wallet.exit(); Logging.instance.e( "Error importing Monero gift wallet: $e", error: e, @@ -271,24 +272,23 @@ class _AddWalletViewState extends ConsumerState { } } else { // Normal import - try { - final info = WalletInfo.createNew( - coin: walletData.coin, - name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", - restoreHeight: walletData.height ?? 0, - otherDataJsonString: walletData.toJson(), - ); - - final wallet = await Wallet.create( - walletInfo: info, - mainDB: ref.read(mainDBProvider), - secureStorageInterface: ref.read(secureStoreProvider), - nodeService: ref.read(nodeServiceChangeNotifierProvider), - prefs: ref.read(prefsChangeNotifierProvider), - mnemonicPassphrase: null, - mnemonic: walletData.seed, - ); + final info = WalletInfo.createNew( + coin: walletData.coin, + name: "${walletData.coin.prettyName} Paper Wallet ${walletData.address != null ? '(${walletData.address!.substring(walletData.address!.length - 4)})' : ''}", + restoreHeight: walletData.height ?? 0, + otherDataJsonString: walletData.toJson(), + ); + final wallet = await Wallet.create( + walletInfo: info, + mainDB: ref.read(mainDBProvider), + secureStorageInterface: ref.read(secureStoreProvider), + nodeService: ref.read(nodeServiceChangeNotifierProvider), + prefs: ref.read(prefsChangeNotifierProvider), + mnemonicPassphrase: null, + mnemonic: walletData.seed, + ); + try { await wallet.init(); await wallet.recover(isRescan: false); await wallet.info.setMnemonicVerified(isar: ref @@ -297,6 +297,7 @@ class _AddWalletViewState extends ConsumerState { ref.read(pWallets).addWallet(wallet); return wallet; } catch (e, stackTrace) { + await wallet.exit(); Logging.instance.e( "Error importing paper wallet for ${walletData.coin.prettyName}: $e", error: e, From fe15e5f995853c4e407e99f4cf67784922a812cb Mon Sep 17 00:00:00 2001 From: dethe <76167420+detherminal@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:40:39 +0300 Subject: [PATCH 9/9] refactor: remove more unused imports --- lib/wallets/crypto_currency/coins/monero.dart | 22 ------------------- .../crypto_currency/crypto_currency.dart | 11 ---------- 2 files changed, 33 deletions(-) diff --git a/lib/wallets/crypto_currency/coins/monero.dart b/lib/wallets/crypto_currency/coins/monero.dart index 52a969c3d..4d1535179 100644 --- a/lib/wallets/crypto_currency/coins/monero.dart +++ b/lib/wallets/crypto_currency/coins/monero.dart @@ -1,31 +1,9 @@ -import 'dart:io'; - import 'package:cs_monero/src/ffi_bindings/monero_wallet_bindings.dart' as xmr_wallet_ffi; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:http/io_client.dart'; -import 'package:monero_rpc/monero_rpc.dart'; -import 'package:socks5_proxy/socks.dart'; import '../../../models/node_model.dart'; -import '../../../providers/db/main_db_provider.dart'; -import '../../../providers/global/node_service_provider.dart'; -import '../../../providers/global/prefs_provider.dart'; -import '../../../providers/global/secure_store_provider.dart'; -import '../../../providers/global/wallets_provider.dart'; -import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; -import '../../../services/node_service.dart'; -import '../../../services/tor_service.dart'; -import '../../../utilities/address_utils.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; -import '../../../utilities/enums/fee_rate_type_enum.dart'; -import '../../../utilities/logger.dart'; -import '../../../utilities/prefs.dart'; -import '../../isar/models/wallet_info.dart'; -import '../../models/tx_data.dart'; -import '../../wallet/impl/monero_wallet.dart'; -import '../../wallet/wallet.dart'; import '../crypto_currency.dart'; import '../intermediate/cryptonote_currency.dart'; diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index c9c147123..4f6c232e6 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -1,17 +1,6 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; - import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/node_model.dart'; -import '../../providers/db/main_db_provider.dart'; -import '../../providers/global/node_service_provider.dart'; -import '../../providers/global/prefs_provider.dart'; -import '../../providers/global/secure_store_provider.dart'; -import '../../providers/global/wallets_provider.dart'; -import '../../utilities/address_utils.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; -import '../../utilities/logger.dart'; -import '../isar/models/wallet_info.dart'; -import '../wallet/wallet.dart'; export 'coins/banano.dart'; export 'coins/bitcoin.dart';