mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-22 19:39:22 +00:00
monero (and wow) view only wallet functionality
This commit is contained in:
parent
3d2d0e4e73
commit
53eb6ac8d1
8 changed files with 211 additions and 4 deletions
|
@ -0,0 +1,5 @@
|
|||
import '../crypto_currency.dart';
|
||||
|
||||
mixin ViewOnlyOptionCurrencyInterface on CryptoCurrency {
|
||||
//
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -117,6 +117,10 @@ class WalletInfo implements IsarId {
|
|||
? {}
|
||||
: Map<String, dynamic>.from(jsonDecode(otherDataJsonString!) as Map);
|
||||
|
||||
@ignore
|
||||
bool get isViewOnly =>
|
||||
otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false;
|
||||
|
||||
Future<bool> 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";
|
||||
}
|
||||
|
|
|
@ -97,6 +97,22 @@ class MoneroWallet extends LibMoneroWallet {
|
|||
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
|
||||
void invalidSeedLengthCheck(int length) {
|
||||
if (length != 25 && length != 16) {
|
||||
|
|
|
@ -134,9 +134,25 @@ class WowneroWallet extends LibMoneroWallet {
|
|||
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
|
||||
void invalidSeedLengthCheck(int length) {
|
||||
if (!(length == 14 || length == 25)) {
|
||||
if (!(length == 14 || length == 16 || length == 25)) {
|
||||
throw Exception("Invalid wownero mnemonic length found: $length");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T extends CryptonoteCurrency>
|
||||
extends CryptonoteWallet<T> implements MultiAddressInterface<T> {
|
||||
extends CryptonoteWallet<T>
|
||||
implements MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
|
||||
@override
|
||||
int get isarTransactionVersion => 2;
|
||||
|
||||
|
@ -139,6 +141,14 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
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);
|
||||
|
||||
bool walletExists(String path);
|
||||
|
@ -333,6 +343,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
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<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 ====================================================
|
||||
|
||||
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||
|
|
|
@ -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<T extends CryptoCurrency> {
|
||||
// default to Transaction class. For TransactionV2 set to 2
|
||||
|
@ -145,7 +146,13 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
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<T extends CryptoCurrency> {
|
|||
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<T extends CryptoCurrency> {
|
|||
}) =>
|
||||
"${walletId}_privateKey";
|
||||
|
||||
// secure storage key
|
||||
static String getViewOnlyWalletDataSecStoreKey({
|
||||
required String walletId,
|
||||
}) =>
|
||||
"${walletId}_viewOnlyWalletData";
|
||||
|
||||
//============================================================================
|
||||
// ========== Private ========================================================
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
Loading…
Reference in a new issue