From 53eb6ac8d153f38e0a21445173ed3f4f84fdf2c3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 12 Nov 2024 09:28:44 -0600 Subject: [PATCH] monero (and wow) view only wallet functionality --- .../view_only_option_currency_interface.dart | 5 + .../intermediate/cryptonote_currency.dart | 4 +- lib/wallets/isar/models/wallet_info.dart | 5 + lib/wallets/wallet/impl/monero_wallet.dart | 16 +++ lib/wallets/wallet/impl/wownero_wallet.dart | 18 ++- .../intermediate/lib_monero_wallet.dart | 113 +++++++++++++++++- lib/wallets/wallet/wallet.dart | 20 +++- .../view_only_option_interface.dart | 34 ++++++ 8 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 lib/wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart create mode 100644 lib/wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart diff --git a/lib/wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart b/lib/wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart new file mode 100644 index 000000000..dec5c016b --- /dev/null +++ b/lib/wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart @@ -0,0 +1,5 @@ +import '../crypto_currency.dart'; + +mixin ViewOnlyOptionCurrencyInterface on CryptoCurrency { + // +} diff --git a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart index 496336235..319a501ac 100644 --- a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart @@ -1,7 +1,9 @@ import '../../../models/isar/models/blockchain_data/address.dart'; import '../crypto_currency.dart'; +import '../interfaces/view_only_option_currency_interface.dart'; -abstract class CryptonoteCurrency extends CryptoCurrency { +abstract class CryptonoteCurrency extends CryptoCurrency + with ViewOnlyOptionCurrencyInterface { CryptonoteCurrency(super.network); @override diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 6a7a9b54c..3e296a3a0 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -117,6 +117,10 @@ class WalletInfo implements IsarId { ? {} : Map.from(jsonDecode(otherDataJsonString!) as Map); + @ignore + bool get isViewOnly => + otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false; + Future isMnemonicVerified(Isar isar) async => (await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst()) ?.isMnemonicVerified == @@ -512,4 +516,5 @@ abstract class WalletInfoKeys { "firoSparkCacheSetTimestampCacheKey"; static const String enableOptInRbf = "enableOptInRbfKey"; static const String reuseAddress = "reuseAddressKey"; + static const String isViewOnlyKey = "isViewOnlyKey"; } diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index 0bc11c3e3..bcec63851 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -97,6 +97,22 @@ class MoneroWallet extends LibMoneroWallet { restoreHeight: height, ); + @override + Future getRestoredFromViewKeyWallet({ + required String path, + required String password, + required String address, + required String privateViewKey, + int height = 0, + }) async => + lib_monero.MoneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); + @override void invalidSeedLengthCheck(int length) { if (length != 25 && length != 16) { diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index afcfee319..a33bd2da7 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -134,9 +134,25 @@ class WowneroWallet extends LibMoneroWallet { restoreHeight: height, ); + @override + Future getRestoredFromViewKeyWallet({ + required String path, + required String password, + required String address, + required String privateViewKey, + int height = 0, + }) async => + lib_monero.WowneroWallet.createViewOnlyWallet( + path: path, + password: password, + address: address, + viewKey: privateViewKey, + restoreHeight: height, + ); + @override void invalidSeedLengthCheck(int length) { - if (!(length == 14 || length == 25)) { + if (!(length == 14 || length == 16 || length == 25)) { throw Exception("Invalid wownero mnemonic length found: $length"); } } diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index a8029baa4..5e7d2a8dd 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -36,10 +36,12 @@ import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; import '../wallet.dart'; import '../wallet_mixin_interfaces/multi_address_interface.dart'; +import '../wallet_mixin_interfaces/view_only_option_interface.dart'; import 'cryptonote_wallet.dart'; abstract class LibMoneroWallet - extends CryptonoteWallet implements MultiAddressInterface { + extends CryptonoteWallet + implements MultiAddressInterface, ViewOnlyOptionInterface { @override int get isarTransactionVersion => 2; @@ -139,6 +141,14 @@ abstract class LibMoneroWallet int height = 0, }); + Future getRestoredFromViewKeyWallet({ + required String path, + required String password, + required String address, + required String privateViewKey, + int height = 0, + }); + void invalidSeedLengthCheck(int length); bool walletExists(String path); @@ -333,6 +343,11 @@ abstract class LibMoneroWallet return; } + if (isViewOnly) { + await recoverViewOnly(); + return; + } + await refreshMutex.protect(() async { final mnemonic = await getMnemonic(); final seedLength = mnemonic.trim().split(" ").length; @@ -1284,6 +1299,102 @@ abstract class LibMoneroWallet } } + // ============== View only ================================================== + + @override + bool get isViewOnly => info.isViewOnly; + + @override + Future recoverViewOnly() async { + await refreshMutex.protect(() async { + final jsonEncodedString = await secureStorageInterface.read( + key: Wallet.getViewOnlyWalletDataSecStoreKey( + walletId: walletId, + ), + ); + + final data = ViewOnlyWalletData.fromJsonEncodedString(jsonEncodedString!); + + try { + final height = max(info.restoreHeight, 0); + + await info.updateRestoreHeight( + newRestoreHeight: height, + isar: mainDB.isar, + ); + + final String name = walletId; + + final path = await pathForWallet( + name: name, + type: compatType, + ); + + final password = generatePassword(); + await secureStorageInterface.write( + key: lib_monero_compat.libMoneroWalletPasswordKey(walletId), + value: password, + ); + final wallet = await getRestoredFromViewKeyWallet( + path: path, + password: password, + address: data.address!, + privateViewKey: data.privateViewKey!, + height: height, + ); + + if (libMoneroWallet != null) { + await exit(); + } + + libMoneroWallet = wallet; + + _setListener(); + + final newReceivingAddress = await getCurrentReceivingAddress() ?? + Address( + walletId: walletId, + derivationIndex: 0, + derivationPath: null, + value: wallet.getAddress().value, + publicKey: [], + type: AddressType.cryptonote, + subType: AddressSubType.receiving, + ); + + await mainDB.updateOrPutAddresses([newReceivingAddress]); + await info.updateReceivingAddress( + newAddress: newReceivingAddress.value, + isar: mainDB.isar, + ); + + await updateNode(); + _setListener(); + + unawaited(libMoneroWallet?.rescanBlockchain()); + libMoneroWallet?.startSyncing(); + + // await save(); + libMoneroWallet?.startListeners(); + libMoneroWallet?.startAutoSaving(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverViewOnly(): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + }); + } + + @override + Future getViewOnlyWalletData() async { + return ViewOnlyWalletData( + address: libMoneroWallet!.getAddress().value, + privateViewKey: libMoneroWallet!.getPrivateViewKey(), + ); + } + // ============== Private ==================================================== StreamSubscription? _torStatusListener; diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 7e7384267..4f69b6056 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -54,6 +54,7 @@ import 'wallet_mixin_interfaces/multi_address_interface.dart'; import 'wallet_mixin_interfaces/paynym_interface.dart'; import 'wallet_mixin_interfaces/private_key_interface.dart'; import 'wallet_mixin_interfaces/spark_interface.dart'; +import 'wallet_mixin_interfaces/view_only_option_interface.dart'; abstract class Wallet { // default to Transaction class. For TransactionV2 set to 2 @@ -145,7 +146,13 @@ abstract class Wallet { String? mnemonic, String? mnemonicPassphrase, String? privateKey, + ViewOnlyWalletData? viewOnlyData, }) async { + // TODO: rework soon? + if (walletInfo.isViewOnly && viewOnlyData == null) { + throw Exception("Missing view key while creating view only wallet!"); + } + final Wallet wallet = await _construct( walletInfo: walletInfo, mainDB: mainDB, @@ -154,7 +161,12 @@ abstract class Wallet { prefs: prefs, ); - if (wallet is MnemonicInterface) { + if (wallet is ViewOnlyOptionInterface) { + await secureStorageInterface.write( + key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId), + value: viewOnlyData!.toJsonEncodedString(), + ); + } else if (wallet is MnemonicInterface) { if (wallet is CryptonoteWallet) { // currently a special case due to the xmr/wow libraries handling their // own mnemonic generation on new wallet creation @@ -279,6 +291,12 @@ abstract class Wallet { }) => "${walletId}_privateKey"; + // secure storage key + static String getViewOnlyWalletDataSecStoreKey({ + required String walletId, + }) => + "${walletId}_viewOnlyWalletData"; + //============================================================================ // ========== Private ======================================================== diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart new file mode 100644 index 000000000..6e12f02cc --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import '../../crypto_currency/interfaces/view_only_option_currency_interface.dart'; +import '../wallet.dart'; + +class ViewOnlyWalletData { + final String? address; + final String? privateViewKey; + + ViewOnlyWalletData({required this.address, required this.privateViewKey}); + + factory ViewOnlyWalletData.fromJsonEncodedString(String jsonEncodedString) { + final map = jsonDecode(jsonEncodedString) as Map; + final json = Map.from(map); + return ViewOnlyWalletData( + address: json["address"] as String?, + privateViewKey: json["privateViewKey"] as String?, + ); + } + + String toJsonEncodedString() => jsonEncode({ + "address": address, + "privateViewKey": privateViewKey, + }); +} + +mixin ViewOnlyOptionInterface + on Wallet { + bool get isViewOnly; + + Future recoverViewOnly(); + + Future getViewOnlyWalletData(); +}