mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 03:49: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 '../../../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
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 ========================================================
|
||||||
|
|
||||||
|
|
|
@ -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