monero (and wow) view only wallet functionality

This commit is contained in:
julian 2024-11-12 09:28:44 -06:00 committed by julian-CStack
parent 3d2d0e4e73
commit 53eb6ac8d1
8 changed files with 211 additions and 4 deletions

View file

@ -0,0 +1,5 @@
import '../crypto_currency.dart';
mixin ViewOnlyOptionCurrencyInterface on CryptoCurrency {
//
}

View file

@ -1,7 +1,9 @@
import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/address.dart';
import '../crypto_currency.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); CryptonoteCurrency(super.network);
@override @override

View file

@ -117,6 +117,10 @@ class WalletInfo implements IsarId {
? {} ? {}
: Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map); : Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map);
@ignore
bool get isViewOnly =>
otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false;
Future<bool> isMnemonicVerified(Isar isar) async => Future<bool> isMnemonicVerified(Isar isar) async =>
(await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst()) (await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst())
?.isMnemonicVerified == ?.isMnemonicVerified ==
@ -512,4 +516,5 @@ abstract class WalletInfoKeys {
"firoSparkCacheSetTimestampCacheKey"; "firoSparkCacheSetTimestampCacheKey";
static const String enableOptInRbf = "enableOptInRbfKey"; static const String enableOptInRbf = "enableOptInRbfKey";
static const String reuseAddress = "reuseAddressKey"; static const String reuseAddress = "reuseAddressKey";
static const String isViewOnlyKey = "isViewOnlyKey";
} }

View file

@ -97,6 +97,22 @@ class MoneroWallet extends LibMoneroWallet {
restoreHeight: height, restoreHeight: height,
); );
@override
Future<lib_monero.Wallet> 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 @override
void invalidSeedLengthCheck(int length) { void invalidSeedLengthCheck(int length) {
if (length != 25 && length != 16) { if (length != 25 && length != 16) {

View file

@ -134,9 +134,25 @@ class WowneroWallet extends LibMoneroWallet {
restoreHeight: height, restoreHeight: height,
); );
@override
Future<lib_monero.Wallet> 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 @override
void invalidSeedLengthCheck(int length) { void invalidSeedLengthCheck(int length) {
if (!(length == 14 || length == 25)) { if (!(length == 14 || length == 16 || length == 25)) {
throw Exception("Invalid wownero mnemonic length found: $length"); throw Exception("Invalid wownero mnemonic length found: $length");
} }
} }

View file

@ -36,10 +36,12 @@ import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart'; import '../../models/tx_data.dart';
import '../wallet.dart'; import '../wallet.dart';
import '../wallet_mixin_interfaces/multi_address_interface.dart'; import '../wallet_mixin_interfaces/multi_address_interface.dart';
import '../wallet_mixin_interfaces/view_only_option_interface.dart';
import 'cryptonote_wallet.dart'; import 'cryptonote_wallet.dart';
abstract class LibMoneroWallet<T extends CryptonoteCurrency> abstract class LibMoneroWallet<T extends CryptonoteCurrency>
extends CryptonoteWallet<T> implements MultiAddressInterface<T> { extends CryptonoteWallet<T>
implements MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
@override @override
int get isarTransactionVersion => 2; int get isarTransactionVersion => 2;
@ -139,6 +141,14 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
int height = 0, int height = 0,
}); });
Future<lib_monero.Wallet> getRestoredFromViewKeyWallet({
required String path,
required String password,
required String address,
required String privateViewKey,
int height = 0,
});
void invalidSeedLengthCheck(int length); void invalidSeedLengthCheck(int length);
bool walletExists(String path); bool walletExists(String path);
@ -333,6 +343,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
return; return;
} }
if (isViewOnly) {
await recoverViewOnly();
return;
}
await refreshMutex.protect(() async { await refreshMutex.protect(() async {
final mnemonic = await getMnemonic(); final mnemonic = await getMnemonic();
final seedLength = mnemonic.trim().split(" ").length; final seedLength = mnemonic.trim().split(" ").length;
@ -1284,6 +1299,102 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
} }
} }
// ============== View only ==================================================
@override
bool get isViewOnly => info.isViewOnly;
@override
Future<void> 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<ViewOnlyWalletData> getViewOnlyWalletData() async {
return ViewOnlyWalletData(
address: libMoneroWallet!.getAddress().value,
privateViewKey: libMoneroWallet!.getPrivateViewKey(),
);
}
// ============== Private ==================================================== // ============== Private ====================================================
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener; StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;

View file

@ -54,6 +54,7 @@ import 'wallet_mixin_interfaces/multi_address_interface.dart';
import 'wallet_mixin_interfaces/paynym_interface.dart'; import 'wallet_mixin_interfaces/paynym_interface.dart';
import 'wallet_mixin_interfaces/private_key_interface.dart'; import 'wallet_mixin_interfaces/private_key_interface.dart';
import 'wallet_mixin_interfaces/spark_interface.dart'; import 'wallet_mixin_interfaces/spark_interface.dart';
import 'wallet_mixin_interfaces/view_only_option_interface.dart';
abstract class Wallet<T extends CryptoCurrency> { abstract class Wallet<T extends CryptoCurrency> {
// default to Transaction class. For TransactionV2 set to 2 // default to Transaction class. For TransactionV2 set to 2
@ -145,7 +146,13 @@ abstract class Wallet<T extends CryptoCurrency> {
String? mnemonic, String? mnemonic,
String? mnemonicPassphrase, String? mnemonicPassphrase,
String? privateKey, String? privateKey,
ViewOnlyWalletData? viewOnlyData,
}) async { }) async {
// TODO: rework soon?
if (walletInfo.isViewOnly && viewOnlyData == null) {
throw Exception("Missing view key while creating view only wallet!");
}
final Wallet wallet = await _construct( final Wallet wallet = await _construct(
walletInfo: walletInfo, walletInfo: walletInfo,
mainDB: mainDB, mainDB: mainDB,
@ -154,7 +161,12 @@ abstract class Wallet<T extends CryptoCurrency> {
prefs: prefs, 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) { if (wallet is CryptonoteWallet) {
// currently a special case due to the xmr/wow libraries handling their // currently a special case due to the xmr/wow libraries handling their
// own mnemonic generation on new wallet creation // own mnemonic generation on new wallet creation
@ -279,6 +291,12 @@ abstract class Wallet<T extends CryptoCurrency> {
}) => }) =>
"${walletId}_privateKey"; "${walletId}_privateKey";
// secure storage key
static String getViewOnlyWalletDataSecStoreKey({
required String walletId,
}) =>
"${walletId}_viewOnlyWalletData";
//============================================================================ //============================================================================
// ========== Private ======================================================== // ========== Private ========================================================

View file

@ -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<String, dynamic>.from(map);
return ViewOnlyWalletData(
address: json["address"] as String?,
privateViewKey: json["privateViewKey"] as String?,
);
}
String toJsonEncodedString() => jsonEncode({
"address": address,
"privateViewKey": privateViewKey,
});
}
mixin ViewOnlyOptionInterface<T extends ViewOnlyOptionCurrencyInterface>
on Wallet<T> {
bool get isViewOnly;
Future<void> recoverViewOnly();
Future<ViewOnlyWalletData> getViewOnlyWalletData();
}