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';