Add support for view only wallets to most coins

This commit is contained in:
julian 2024-11-12 18:45:17 -06:00 committed by julian-CStack
parent 3da57bc150
commit 6ff539e71b
38 changed files with 1665 additions and 556 deletions

View file

@ -0,0 +1,164 @@
import 'dart:convert';
import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import 'key_data_interface.dart';
// do not remove or change the order of these enum values
enum ViewOnlyWalletType {
cryptonote,
addressOnly,
xPub;
}
sealed class ViewOnlyWalletData with KeyDataInterface {
@override
final String walletId;
ViewOnlyWalletType get type;
ViewOnlyWalletData({
required this.walletId,
});
static ViewOnlyWalletData fromJsonEncodedString(
String jsonEncodedString, {
required String walletId,
}) {
final map = jsonDecode(jsonEncodedString) as Map;
final json = Map<String, dynamic>.from(map);
final type = ViewOnlyWalletType.values[json["type"] as int];
switch (type) {
case ViewOnlyWalletType.cryptonote:
return CryptonoteViewOnlyWalletData.fromJsonEncodedString(
jsonEncodedString,
walletId: walletId,
);
case ViewOnlyWalletType.addressOnly:
return AddressViewOnlyWalletData.fromJsonEncodedString(
jsonEncodedString,
walletId: walletId,
);
case ViewOnlyWalletType.xPub:
return ExtendedKeysViewOnlyWalletData.fromJsonEncodedString(
jsonEncodedString,
walletId: walletId,
);
}
}
String toJsonEncodedString();
}
class CryptonoteViewOnlyWalletData extends ViewOnlyWalletData {
@override
final type = ViewOnlyWalletType.cryptonote;
final String address;
final String privateViewKey;
CryptonoteViewOnlyWalletData({
required super.walletId,
required this.address,
required this.privateViewKey,
});
static CryptonoteViewOnlyWalletData fromJsonEncodedString(
String jsonEncodedString, {
required String walletId,
}) {
final map = jsonDecode(jsonEncodedString) as Map;
final json = Map<String, dynamic>.from(map);
return CryptonoteViewOnlyWalletData(
walletId: walletId,
address: json["address"] as String,
privateViewKey: json["privateViewKey"] as String,
);
}
@override
String toJsonEncodedString() => jsonEncode({
"type": type.index,
"address": address,
"privateViewKey": privateViewKey,
});
}
class AddressViewOnlyWalletData extends ViewOnlyWalletData {
@override
final type = ViewOnlyWalletType.addressOnly;
final String address;
AddressViewOnlyWalletData({
required super.walletId,
required this.address,
});
static AddressViewOnlyWalletData fromJsonEncodedString(
String jsonEncodedString, {
required String walletId,
}) {
final map = jsonDecode(jsonEncodedString) as Map;
final json = Map<String, dynamic>.from(map);
return AddressViewOnlyWalletData(
walletId: walletId,
address: json["address"] as String,
);
}
@override
String toJsonEncodedString() => jsonEncode({
"type": type.index,
"address": address,
});
}
class ExtendedKeysViewOnlyWalletData extends ViewOnlyWalletData {
@override
final type = ViewOnlyWalletType.xPub;
final List<XPub> xPubs;
ExtendedKeysViewOnlyWalletData({
required super.walletId,
required List<XPub> xPubs,
}) : xPubs = List.unmodifiable(xPubs);
static ExtendedKeysViewOnlyWalletData fromJsonEncodedString(
String jsonEncodedString, {
required String walletId,
}) {
final map = jsonDecode(jsonEncodedString) as Map;
final json = Map<String, dynamic>.from(map);
return ExtendedKeysViewOnlyWalletData(
walletId: walletId,
xPubs: List<Map<String, dynamic>>.from((json["xPubs"] as List))
.map(
(e) => XPub(
path: e["path"] as String,
encoded: e["encoded"] as String,
),
)
.toList(growable: false),
);
}
@override
String toJsonEncodedString() => jsonEncode({
"type": type.index,
"xPubs": [
...xPubs.map(
(e) => {
"path": e.path,
"encoded": e.encoded,
},
),
],
});
}

View file

@ -25,6 +25,7 @@ import '../../../utilities/name_generator.dart';
import '../../../utilities/text_styles.dart'; import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart'; import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
import '../../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/isar/models/wallet_info.dart';
import '../../../widgets/background.dart'; import '../../../widgets/background.dart';
@ -104,7 +105,9 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
case AddWalletType.New: case AddWalletType.New:
unawaited( unawaited(
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport coin.possibleMnemonicLengths.length > 1 ||
coin.hasMnemonicPassphraseSupport ||
coin is ViewOnlyOptionCurrencyInterface
? NewWalletOptionsView.routeName ? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView.routeName, : NewWalletRecoveryPhraseWarningView.routeName,
arguments: Tuple2( arguments: Tuple2(

View file

@ -12,9 +12,11 @@ import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart'; import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart'; import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
import '../../../widgets/background.dart'; import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart'; import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/custom_buttons/checkbox_text_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_app_bar.dart';
import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/desktop_scaffold.dart';
import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/primary_button.dart';
@ -25,8 +27,12 @@ import '../new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_wa
import '../restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart'; import '../restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart';
import '../restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart'; import '../restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart';
final pNewWalletOptions = final pNewWalletOptions = StateProvider<
StateProvider<({String mnemonicPassphrase, int mnemonicWordsCount})?>( ({
String mnemonicPassphrase,
int mnemonicWordsCount,
bool convertToViewOnly,
})?>(
(ref) => null, (ref) => null,
); );
@ -59,6 +65,8 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
bool hidePassword = true; bool hidePassword = true;
NewWalletOptions _selectedOptions = NewWalletOptions.Default; NewWalletOptions _selectedOptions = NewWalletOptions.Default;
bool _convertToViewOnly = false;
@override @override
void initState() { void initState() {
passwordController = TextEditingController(); passwordController = TextEditingController();
@ -210,7 +218,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
if (_selectedOptions == NewWalletOptions.Advanced) if (_selectedOptions == NewWalletOptions.Advanced)
Column( Column(
children: [ children: [
if (Util.isDesktop) if (Util.isDesktop && lengths.length > 1)
DropdownButtonHideUnderline( DropdownButtonHideUnderline(
child: DropdownButton2<int>( child: DropdownButton2<int>(
value: ref value: ref
@ -265,7 +273,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
), ),
), ),
), ),
if (!Util.isDesktop) if (!Util.isDesktop && lengths.length > 1)
MobileMnemonicLengthSelector( MobileMnemonicLengthSelector(
chooseMnemonicLength: () { chooseMnemonicLength: () {
showModalBottomSheet<dynamic>( showModalBottomSheet<dynamic>(
@ -284,91 +292,109 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
); );
}, },
), ),
const SizedBox( if (widget.coin.hasMnemonicPassphraseSupport)
height: 24, const SizedBox(
), height: 24,
RoundedWhiteContainer( ),
child: Center( if (widget.coin.hasMnemonicPassphraseSupport)
child: Text( RoundedWhiteContainer(
"You may add a BIP39 passphrase. This is optional. " child: Center(
"You will need BOTH your seed and your passphrase to recover the wallet.", child: Text(
style: Util.isDesktop "You may add a BIP39 passphrase. This is optional. "
? STextStyles.desktopTextExtraSmall(context) "You will need BOTH your seed and your passphrase to recover the wallet.",
.copyWith( style: Util.isDesktop
color: Theme.of(context) ? STextStyles.desktopTextExtraSmall(context)
.extension<StackColors>()! .copyWith(
.textSubtitle1, color: Theme.of(context)
) .extension<StackColors>()!
: STextStyles.itemSubtitle(context), .textSubtitle1,
)
: STextStyles.itemSubtitle(context),
),
), ),
), ),
), if (widget.coin.hasMnemonicPassphraseSupport)
const SizedBox( const SizedBox(
height: 8, height: 8,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
), ),
child: TextField( if (widget.coin.hasMnemonicPassphraseSupport)
key: const Key("mnemonicPassphraseFieldKey1"), ClipRRect(
focusNode: passwordFocusNode, borderRadius: BorderRadius.circular(
controller: passwordController, Constants.size.circularBorderRadius,
style: Util.isDesktop ),
? STextStyles.desktopTextMedium(context).copyWith( child: TextField(
height: 2, key: const Key("mnemonicPassphraseFieldKey1"),
) focusNode: passwordFocusNode,
: STextStyles.field(context), controller: passwordController,
obscureText: hidePassword, style: Util.isDesktop
enableSuggestions: false, ? STextStyles.desktopTextMedium(context).copyWith(
autocorrect: false, height: 2,
decoration: standardInputDecoration( )
"BIP39 passphrase", : STextStyles.field(context),
passwordFocusNode, obscureText: hidePassword,
context, enableSuggestions: false,
).copyWith( autocorrect: false,
suffixIcon: UnconstrainedBox( decoration: standardInputDecoration(
child: ConditionalParent( "BIP39 passphrase",
condition: Util.isDesktop, passwordFocusNode,
builder: (child) => SizedBox( context,
height: 70, ).copyWith(
child: child, suffixIcon: UnconstrainedBox(
), child: ConditionalParent(
child: Row( condition: Util.isDesktop,
children: [ builder: (child) => SizedBox(
SizedBox( height: 70,
width: Util.isDesktop ? 24 : 16, child: child,
), ),
GestureDetector( child: Row(
key: const Key( children: [
"mnemonicPassphraseFieldShowPasswordButtonKey", SizedBox(
),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: Util.isDesktop ? 24 : 16, width: Util.isDesktop ? 24 : 16,
height: Util.isDesktop ? 24 : 16,
), ),
), GestureDetector(
const SizedBox( key: const Key(
width: 12, "mnemonicPassphraseFieldShowPasswordButtonKey",
), ),
], onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: Util.isDesktop ? 24 : 16,
height: Util.isDesktop ? 24 : 16,
),
),
const SizedBox(
width: 12,
),
],
),
), ),
), ),
), ),
), ),
), ),
), if (widget.coin is ViewOnlyOptionCurrencyInterface)
const SizedBox(
height: 24,
),
if (widget.coin is ViewOnlyOptionCurrencyInterface)
CheckboxTextButton(
label: "Convert to view only wallet. "
"You will only be shown the seed phrase once. "
"Save it somewhere. "
"If you lose it you will lose access to any funds in this wallet.",
onChanged: (value) {
_convertToViewOnly = value;
},
),
], ],
), ),
if (!Util.isDesktop) const Spacer(), if (!Util.isDesktop) const Spacer(),
@ -383,6 +409,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
mnemonicWordsCount: mnemonicWordsCount:
ref.read(mnemonicWordCountStateProvider.state).state, ref.read(mnemonicWordCountStateProvider.state).state,
mnemonicPassphrase: passwordController.text, mnemonicPassphrase: passwordController.text,
convertToViewOnly: _convertToViewOnly,
); );
} else { } else {
ref.read(pNewWalletOptions.notifier).state = null; ref.read(pNewWalletOptions.notifier).state = null;

View file

@ -582,7 +582,12 @@ class _NewWalletRecoveryPhraseWarningViewState
) )
.state! .state!
.mnemonicPassphrase; .mnemonicPassphrase;
} else {} } else {
// this may not be epiccash specific?
if (coin is Epiccash) {
mnemonicPassphrase = "";
}
}
wordCount = ref wordCount = ref
.read( .read(

View file

@ -24,6 +24,7 @@ import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart'; import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart'; import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/checkbox_text_button.dart'; import '../../../../widgets/custom_buttons/checkbox_text_button.dart';
@ -656,7 +657,7 @@ class ViewOnlyRestoreOption extends StatefulWidget {
class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> { class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final showDateOption = widget.coin is ViewOnlyOptionCurrencyInterface; final showDateOption = widget.coin is CryptonoteCurrency;
return Column( return Column(
children: [ children: [
if (showDateOption) if (showDateOption)

View file

@ -4,32 +4,41 @@ import 'dart:io';
import 'package:cs_monero/src/deprecated/get_height_by_date.dart' import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
as cs_monero_deprecated; as cs_monero_deprecated;
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
import '../../../providers/db/main_db_provider.dart'; import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/secure_store_provider.dart'; import '../../../providers/global/secure_store_provider.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/barcode_scanner_interface.dart';
import '../../../utilities/clipboard_interface.dart'; import '../../../utilities/clipboard_interface.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/text_styles.dart'; import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart'; import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import '../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
import '../../../wallets/isar/models/wallet_info.dart'; import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart'; import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart'; import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_app_bar.dart';
import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/desktop_scaffold.dart';
import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/stack_text_field.dart'; import '../../../widgets/stack_text_field.dart';
import '../../../widgets/toggle.dart';
import '../../home_view/home_view.dart'; import '../../home_view/home_view.dart';
import 'confirm_recovery_dialog.dart'; import 'confirm_recovery_dialog.dart';
import 'sub_widgets/restore_failed_dialog.dart'; import 'sub_widgets/restore_failed_dialog.dart';
@ -66,7 +75,10 @@ class _RestoreViewOnlyWalletViewState
late final TextEditingController addressController; late final TextEditingController addressController;
late final TextEditingController viewKeyController; late final TextEditingController viewKeyController;
late String _currentDropDownValue;
bool _enableRestoreButton = false; bool _enableRestoreButton = false;
bool _addressOnly = false;
bool _buttonLock = false; bool _buttonLock = false;
@ -106,30 +118,43 @@ class _RestoreViewOnlyWalletViewState
WalletInfoKeys.isViewOnlyKey: true, WalletInfoKeys.isViewOnlyKey: true,
}; };
if (widget.restoreFromDate != null) { final ViewOnlyWalletType viewOnlyWalletType;
if (widget.coin is Monero) { if (widget.coin is Bip39HDCurrency) {
height = cs_monero_deprecated.getMoneroHeightByDate( if (widget.coin is Firo) {
date: widget.restoreFromDate!, otherDataJson.addAll(
{
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
WalletInfoKeys.enableLelantusScanning:
widget.enableLelantusScanning,
},
); );
} }
if (widget.coin is Wownero) { viewOnlyWalletType = _addressOnly
height = cs_monero_deprecated.getWowneroHeightByDate( ? ViewOnlyWalletType.addressOnly
date: widget.restoreFromDate!, : ViewOnlyWalletType.xPub;
); } else if (widget.coin is CryptonoteCurrency) {
if (widget.restoreFromDate != null) {
if (widget.coin is Monero) {
height = cs_monero_deprecated.getMoneroHeightByDate(
date: widget.restoreFromDate!,
);
}
if (widget.coin is Wownero) {
height = cs_monero_deprecated.getWowneroHeightByDate(
date: widget.restoreFromDate!,
);
}
if (height < 0) height = 0;
} }
if (height < 0) {
height = 0;
}
}
if (widget.coin is Firo) { viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
otherDataJson.addAll( } else {
{ throw Exception(
WalletInfoKeys.lelantusCoinIsarRescanRequired: false, "Unsupported view only wallet currency type found: ${widget.coin.runtimeType}",
WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning,
},
); );
} }
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] =
viewOnlyWalletType.index;
if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable(); if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable();
@ -166,6 +191,43 @@ class _RestoreViewOnlyWalletViewState
); );
} }
final ViewOnlyWalletData viewOnlyData;
switch (viewOnlyWalletType) {
case ViewOnlyWalletType.cryptonote:
if (addressController.text.isEmpty ||
viewKeyController.text.isEmpty) {
throw Exception("Missing address and/or private view key fields");
}
viewOnlyData = CryptonoteViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
privateViewKey: viewKeyController.text,
);
break;
case ViewOnlyWalletType.addressOnly:
if (addressController.text.isEmpty) {
throw Exception("Address is empty");
}
viewOnlyData = AddressViewOnlyWalletData(
walletId: info.walletId,
address: addressController.text,
);
break;
case ViewOnlyWalletType.xPub:
viewOnlyData = ExtendedKeysViewOnlyWalletData(
walletId: info.walletId,
xPubs: [
XPub(
path: _currentDropDownValue,
encoded: viewKeyController.text,
),
],
);
break;
}
var node = ref var node = ref
.read(nodeServiceChangeNotifierProvider) .read(nodeServiceChangeNotifierProvider)
.getPrimaryNodeFor(currency: widget.coin); .getPrimaryNodeFor(currency: widget.coin);
@ -185,10 +247,7 @@ class _RestoreViewOnlyWalletViewState
secureStorageInterface: ref.read(secureStoreProvider), secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider), nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider), prefs: ref.read(prefsChangeNotifierProvider),
viewOnlyData: ViewOnlyWalletData( viewOnlyData: viewOnlyData,
address: addressController.text,
privateViewKey: viewKeyController.text,
),
); );
// TODO: extract interface with isRestore param // TODO: extract interface with isRestore param
@ -278,11 +337,27 @@ class _RestoreViewOnlyWalletViewState
super.initState(); super.initState();
addressController = TextEditingController(); addressController = TextEditingController();
viewKeyController = TextEditingController(); viewKeyController = TextEditingController();
if (widget.coin is Bip39HDCurrency) {
_currentDropDownValue = (widget.coin as Bip39HDCurrency)
.supportedHardenedDerivationPaths
.last;
}
}
@override
void dispose() {
addressController.dispose();
viewKeyController.dispose();
super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDesktop = Util.isDesktop; final isDesktop = Util.isDesktop;
final isElectrumX = widget.coin is ElectrumXCurrencyInterface;
return MasterScaffold( return MasterScaffold(
isDesktop: isDesktop, isDesktop: isDesktop,
appBar: isDesktop appBar: isDesktop
@ -339,32 +414,156 @@ class _RestoreViewOnlyWalletViewState
? STextStyles.desktopH2(context) ? STextStyles.desktopH2(context)
: STextStyles.pageTitleH1(context), : STextStyles.pageTitleH1(context),
), ),
if (isElectrumX)
SizedBox(
height: isDesktop ? 24 : 16,
),
if (isElectrumX)
SizedBox(
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
key: UniqueKey(),
onText: "Extended pub key",
offText: "Single address",
onColor: Theme.of(context)
.extension<StackColors>()!
.popupBG,
offColor: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
isOn: _addressOnly,
onValueChanged: (value) {
setState(() {
_addressOnly = value;
});
},
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
),
SizedBox( SizedBox(
height: isDesktop ? 24 : 16, height: isDesktop ? 24 : 16,
), ),
FullTextField( if (!isElectrumX || _addressOnly)
label: "Address", FullTextField(
controller: addressController, key: const Key("viewOnlyAddressRestoreFieldKey"),
onChanged: (newValue) { label: "Address",
setState(() { controller: addressController,
_enableRestoreButton = newValue.isNotEmpty && onChanged: (newValue) {
viewKeyController.text.isNotEmpty; if (isElectrumX) {
}); viewKeyController.text = "";
}, setState(() {
), _enableRestoreButton = newValue.isNotEmpty;
SizedBox( });
height: isDesktop ? 16 : 12, } else {
), setState(() {
FullTextField( _enableRestoreButton = newValue.isNotEmpty &&
label: "View Key", viewKeyController.text.isNotEmpty;
controller: viewKeyController, });
onChanged: (value) { }
setState(() { },
_enableRestoreButton = value.isNotEmpty && ),
addressController.text.isNotEmpty; if (!isElectrumX)
}); SizedBox(
}, height: isDesktop ? 16 : 12,
), ),
if (isElectrumX && !_addressOnly)
DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: _currentDropDownValue,
items: [
...(widget.coin as Bip39HDCurrency)
.supportedHardenedDerivationPaths
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e,
style: STextStyles.w500_14(context),
),
),
),
],
onChanged: (value) {
if (value is String) {
setState(() {
_currentDropDownValue = value;
});
}
},
isExpanded: true,
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
iconStyleData: IconStyleData(
icon: Padding(
padding: const EdgeInsets.only(right: 10),
child: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
if (isElectrumX && !_addressOnly)
SizedBox(
height: isDesktop ? 16 : 12,
),
if (!isElectrumX || !_addressOnly)
FullTextField(
key: const Key("viewOnlyKeyRestoreFieldKey"),
label:
"${isElectrumX ? "Extended" : "Private View"} Key",
controller: viewKeyController,
onChanged: (value) {
if (isElectrumX) {
addressController.text = "";
setState(() {
_enableRestoreButton = value.isNotEmpty;
});
} else {
setState(() {
_enableRestoreButton = value.isNotEmpty &&
addressController.text.isNotEmpty;
});
}
},
),
if (!isDesktop) const Spacer(), if (!isDesktop) const Spacer(),
SizedBox( SizedBox(
height: isDesktop ? 24 : 16, height: isDesktop ? 24 : 16,

View file

@ -9,13 +9,17 @@
*/ */
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
as cs_monero_deprecated;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../notifications/show_flush_bar.dart'; import '../../../notifications/show_flush_bar.dart';
import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart';
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@ -25,14 +29,25 @@ import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart'; import '../../../utilities/assets.dart';
import '../../../utilities/constants.dart'; import '../../../utilities/constants.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart'; import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart'; import '../../../utilities/util.dart';
import '../../../wallets/crypto_currency/coins/ethereum.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
import '../../../wallets/isar/models/wallet_info.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../wallets/wallet/wallet.dart'; import '../../../wallets/wallet/wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_app_bar.dart';
import '../../../widgets/desktop/desktop_scaffold.dart'; import '../../../widgets/desktop/desktop_scaffold.dart';
import '../../../widgets/stack_dialog.dart';
import '../../home_view/home_view.dart'; import '../../home_view/home_view.dart';
import '../add_token_view/edit_wallet_tokens_view.dart'; import '../add_token_view/edit_wallet_tokens_view.dart';
import '../new_wallet_options/new_wallet_options_view.dart'; import '../new_wallet_options/new_wallet_options_view.dart';
@ -64,46 +79,25 @@ class _VerifyRecoveryPhraseViewState
extends ConsumerState<VerifyRecoveryPhraseView> extends ConsumerState<VerifyRecoveryPhraseView>
// with WidgetsBindingObserver // with WidgetsBindingObserver
{ {
late Wallet _wallet; late String _walletId;
late CryptoCurrency _coin;
late List<String> _mnemonic; late List<String> _mnemonic;
late final bool isDesktop; late final bool isDesktop;
@override @override
void initState() { void initState() {
_wallet = widget.wallet; _walletId = widget.wallet.walletId;
_coin = widget.wallet.cryptoCurrency;
_mnemonic = widget.mnemonic; _mnemonic = widget.mnemonic;
isDesktop = Util.isDesktop; isDesktop = Util.isDesktop;
// WidgetsBinding.instance?.addObserver(this);
super.initState(); super.initState();
} }
@override @override
dispose() { dispose() {
// WidgetsBinding.instance?.removeObserver(this);
super.dispose(); super.dispose();
} }
// @override
// void didChangeAppLifecycleState(AppLifecycleState state) {
// switch (state) {
// case AppLifecycleState.inactive:
// debugPrint(
// "VerifyRecoveryPhraseView ========================= Inactive");
// break;
// case AppLifecycleState.paused:
// debugPrint("VerifyRecoveryPhraseView ========================= Paused");
// break;
// case AppLifecycleState.resumed:
// debugPrint(
// "VerifyRecoveryPhraseView ========================= Resumed");
// break;
// case AppLifecycleState.detached:
// debugPrint(
// "VerifyRecoveryPhraseView ========================= Detached");
// break;
// }
// }
Future<bool> _verifyMnemonicPassphrase() async { Future<bool> _verifyMnemonicPassphrase() async {
final result = await showDialog<String?>( final result = await showDialog<String?>(
context: context, context: context,
@ -113,6 +107,153 @@ class _VerifyRecoveryPhraseViewState
return result == "verified"; return result == "verified";
} }
Future<void> _convertToViewOnly() async {
int height = 0;
final Map<String, dynamic> otherDataJson = {
WalletInfoKeys.isViewOnlyKey: true,
};
final ViewOnlyWalletType viewOnlyWalletType;
if (widget.wallet is ExtendedKeysInterface) {
if (widget.wallet.cryptoCurrency is Firo) {
otherDataJson.addAll(
{
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
WalletInfoKeys.enableLelantusScanning: false,
},
);
}
viewOnlyWalletType = ViewOnlyWalletType.xPub;
} else if (widget.wallet is LibMoneroWallet) {
if (widget.wallet.cryptoCurrency is Monero) {
height = cs_monero_deprecated.getMoneroHeightByDate(
date: DateTime.now().subtract(const Duration(days: 7)),
);
}
if (widget.wallet.cryptoCurrency is Wownero) {
height = cs_monero_deprecated.getWowneroHeightByDate(
date: DateTime.now().subtract(const Duration(days: 7)),
);
}
if (height < 0) height = 0;
viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
} else {
throw Exception(
"Unsupported view only wallet type found: ${widget.wallet.runtimeType}",
);
}
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] =
viewOnlyWalletType.index;
final voInfo = WalletInfo.createNew(
coin: _coin,
name: widget.wallet.info.name,
restoreHeight: height,
otherDataJsonString: jsonEncode(otherDataJson),
);
final ViewOnlyWalletData viewOnlyData;
if (widget.wallet is ExtendedKeysInterface) {
final extendedKeyInfo =
await (widget.wallet as ExtendedKeysInterface).getXPubs();
final testPath = (_coin as Bip39HDCurrency).constructDerivePath(
derivePathType: (_coin as Bip39HDCurrency).defaultDerivePathType,
chain: 0,
index: 0,
);
XPub? xPub;
for (final pub in extendedKeyInfo.xpubs) {
if (testPath.startsWith(pub.path)) {
xPub = pub;
break;
}
}
if (xPub == null) {
throw Exception("Default derivation path not matched in xPubs");
}
viewOnlyData = ExtendedKeysViewOnlyWalletData(
walletId: voInfo.walletId,
xPubs: [xPub],
);
} else if (widget.wallet is LibMoneroWallet) {
final w = widget.wallet as LibMoneroWallet;
final info = await w
.hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing();
final address = info.$1;
final privateViewKey = info.$2;
viewOnlyData = CryptonoteViewOnlyWalletData(
walletId: voInfo.walletId,
address: address,
privateViewKey: privateViewKey,
);
} else {
throw Exception(
"Unsupported view only wallet type found: ${widget.wallet.runtimeType}",
);
}
final voWallet = await Wallet.create(
walletInfo: voInfo,
mainDB: ref.read(mainDBProvider),
secureStorageInterface: ref.read(secureStoreProvider),
nodeService: ref.read(nodeServiceChangeNotifierProvider),
prefs: ref.read(prefsChangeNotifierProvider),
viewOnlyData: viewOnlyData,
);
try {
// TODO: extract interface with isRestore param
switch (voWallet.runtimeType) {
case const (EpiccashWallet):
await (voWallet as EpiccashWallet).init(isRestore: true);
break;
case const (MoneroWallet):
await (voWallet as MoneroWallet).init(isRestore: true);
break;
case const (WowneroWallet):
await (voWallet as WowneroWallet).init(isRestore: true);
break;
default:
await voWallet.init();
}
await voWallet.recover(isRescan: false);
// don't remove this setMnemonicVerified thing
await voWallet.info.setMnemonicVerified(
isar: ref.read(mainDBProvider).isar,
);
ref.read(pWallets).addWallet(voWallet);
await ref.read(pWallets).deleteWallet(
widget.wallet.info,
ref.read(secureStoreProvider),
);
} catch (e) {
await ref.read(pWallets).deleteWallet(
widget.wallet.info,
ref.read(secureStoreProvider),
);
await ref.read(pWallets).deleteWallet(
voWallet.info,
ref.read(secureStoreProvider),
);
rethrow;
}
}
Future<void> _continue(bool isMatch) async { Future<void> _continue(bool isMatch) async {
if (isMatch) { if (isMatch) {
if (ref.read(pNewWalletOptions) != null && if (ref.read(pNewWalletOptions) != null &&
@ -124,11 +265,11 @@ class _VerifyRecoveryPhraseViewState
} }
} }
await ref.read(pWalletInfo(_wallet.walletId)).setMnemonicVerified( await ref.read(pWalletInfo(widget.wallet.walletId)).setMnemonicVerified(
isar: ref.read(mainDBProvider).isar, isar: ref.read(mainDBProvider).isar,
); );
ref.read(pWallets).addWallet(_wallet); ref.read(pWallets).addWallet(widget.wallet);
final isCreateSpecialEthWallet = final isCreateSpecialEthWallet =
ref.read(createSpecialEthWalletRoutingFlag); ref.read(createSpecialEthWalletRoutingFlag);
@ -142,6 +283,51 @@ class _VerifyRecoveryPhraseViewState
.state; .state;
} }
if (mounted &&
ref.read(pNewWalletOptions)?.convertToViewOnly == true &&
widget.wallet is ViewOnlyOptionInterface) {
try {
Exception? ex;
await showLoading(
whileFuture: _convertToViewOnly(),
context: context,
message: "Converting to view only wallet",
rootNavigator: Util.isDesktop,
onException: (e) {
ex = e;
},
);
if (ex != null) {
throw ex!;
}
} catch (e, s) {
Logging.instance.log(
"$e\n$s",
level: LogLevel.Fatal,
);
if (mounted) {
await showDialog<void>(
context: context,
builder: (_) => StackOkDialog(
title: e.toString(),
desktopPopRootNavigator: Util.isDesktop,
),
);
}
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(
NewWalletRecoveryPhraseView.routeName,
),
);
}
return;
}
}
if (mounted) { if (mounted) {
if (isDesktop) { if (isDesktop) {
if (isCreateSpecialEthWallet) { if (isCreateSpecialEthWallet) {
@ -156,7 +342,7 @@ class _VerifyRecoveryPhraseViewState
DesktopHomeView.routeName, DesktopHomeView.routeName,
), ),
); );
if (widget.wallet.info.coin is Ethereum) { if (_coin is Ethereum) {
unawaited( unawaited(
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
EditWalletTokensView.routeName, EditWalletTokensView.routeName,
@ -179,7 +365,7 @@ class _VerifyRecoveryPhraseViewState
(route) => false, (route) => false,
), ),
); );
if (widget.wallet.info.coin is Ethereum) { if (_coin is Ethereum) {
unawaited( unawaited(
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
EditWalletTokensView.routeName, EditWalletTokensView.routeName,
@ -269,7 +455,7 @@ class _VerifyRecoveryPhraseViewState
Future<void> delete() async { Future<void> delete() async {
await ref.read(pWallets).deleteWallet( await ref.read(pWallets).deleteWallet(
_wallet.info, widget.wallet.info,
ref.read(secureStoreProvider), ref.read(secureStoreProvider),
); );
} }
@ -299,7 +485,7 @@ class _VerifyRecoveryPhraseViewState
trailing: ExitToMyStackButton( trailing: ExitToMyStackButton(
onPressed: () async { onPressed: () async {
await delete(); await delete();
if (mounted) { if (context.mounted) {
Navigator.of(context).popUntil( Navigator.of(context).popUntil(
ModalRoute.withName(DesktopHomeView.routeName), ModalRoute.withName(DesktopHomeView.routeName),
); );

View file

@ -148,6 +148,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final coin = ref.watch(pWalletCoin(widget.walletId)); final coin = ref.watch(pWalletCoin(widget.walletId));
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
return ConditionalParent( return ConditionalParent(
condition: !isDesktop, condition: !isDesktop,
builder: (child) => Background( builder: (child) => Background(
@ -383,13 +384,11 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
detail: address.zSafeFrost.toString(), detail: address.zSafeFrost.toString(),
button: Container(), button: Container(),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is Bip39HDWallet && !wallet.isViewOnly)
is Bip39HDWallet)
const _Div( const _Div(
height: 12, height: 12,
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is Bip39HDWallet && !wallet.isViewOnly)
is Bip39HDWallet)
AddressPrivateKey( AddressPrivateKey(
walletId: widget.walletId, walletId: widget.walletId,
address: address, address: address,

View file

@ -18,6 +18,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../../models/isar/models/isar_models.dart'; import '../../models/isar/models/isar_models.dart';
import '../../models/keys/view_only_wallet_data.dart';
import '../../notifications/show_flush_bar.dart'; import '../../notifications/show_flush_bar.dart';
import '../../providers/db/main_db_provider.dart'; import '../../providers/db/main_db_provider.dart';
import '../../providers/providers.dart'; import '../../providers/providers.dart';
@ -36,6 +37,7 @@ import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../widgets/background.dart'; import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart'; import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -189,6 +191,16 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
clipboard = widget.clipboard; clipboard = widget.clipboard;
final wallet = ref.read(pWallets).getWallet(walletId); final wallet = ref.read(pWallets).getWallet(walletId);
_supportsSpark = wallet is SparkInterface; _supportsSpark = wallet is SparkInterface;
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
_showMultiType = false;
} else {
_showMultiType = _supportsSpark ||
(wallet is! BCashInterface &&
wallet is Bip39HDWallet &&
wallet.supportedAddressTypes.length > 1);
}
_showMultiType = _supportsSpark || _showMultiType = _supportsSpark ||
(wallet is! BCashInterface && (wallet is! BCashInterface &&
wallet is Bip39HDWallet && wallet is Bip39HDWallet &&
@ -265,6 +277,18 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
address = ref.watch(pWalletReceivingAddress(walletId)); address = ref.watch(pWalletReceivingAddress(walletId));
} }
final wallet =
ref.watch(pWallets.select((value) => value.getWallet(walletId)));
final bool canGen;
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
canGen = false;
} else {
canGen = (wallet is MultiAddressInterface || _supportsSpark);
}
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -553,17 +577,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
); );
}, },
), ),
if (ref.watch( if (canGen)
pWallets.select((value) => value.getWallet(walletId)),
) is MultiAddressInterface ||
_supportsSpark)
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
if (ref.watch( if (canGen)
pWallets.select((value) => value.getWallet(walletId)),
) is MultiAddressInterface ||
_supportsSpark)
SecondaryButton( SecondaryButton(
label: "Generate new address", label: "Generate new address",
onPressed: _supportsSpark && onPressed: _supportsSpark &&

View file

@ -27,6 +27,7 @@ import '../../../../../models/exchange/change_now/exchange_transaction.dart';
import '../../../../../models/exchange/response_objects/trade.dart'; import '../../../../../models/exchange/response_objects/trade.dart';
import '../../../../../models/isar/models/contact_entry.dart'; import '../../../../../models/isar/models/contact_entry.dart';
import '../../../../../models/isar/models/transaction_note.dart'; import '../../../../../models/isar/models/transaction_note.dart';
import '../../../../../models/keys/view_only_wallet_data.dart';
import '../../../../../models/node_model.dart'; import '../../../../../models/node_model.dart';
import '../../../../../models/stack_restoring_ui_state.dart'; import '../../../../../models/stack_restoring_ui_state.dart';
import '../../../../../models/trade_wallet_lookup.dart'; import '../../../../../models/trade_wallet_lookup.dart';
@ -58,6 +59,7 @@ import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
import '../../../../../wallets/wallet/wallet.dart'; import '../../../../../wallets/wallet/wallet.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
class PreRestoreState { class PreRestoreState {
final Set<String> walletIds; final Set<String> walletIds;
@ -312,7 +314,10 @@ abstract class SWB {
backupWallet['isFavorite'] = wallet.info.isFavourite; backupWallet['isFavorite'] = wallet.info.isFavourite;
backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString; backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;
if (wallet is MnemonicInterface) { if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
backupWallet['viewOnlyWalletDataKey'] =
(await wallet.getViewOnlyWalletData()).toJsonEncodedString();
} else if (wallet is MnemonicInterface) {
backupWallet['mnemonic'] = await wallet.getMnemonic(); backupWallet['mnemonic'] = await wallet.getMnemonic();
backupWallet['mnemonicPassphrase'] = backupWallet['mnemonicPassphrase'] =
await wallet.getMnemonicPassphrase(); await wallet.getMnemonicPassphrase();
@ -419,7 +424,16 @@ abstract class SWB {
String? mnemonic, mnemonicPassphrase, privateKey; String? mnemonic, mnemonicPassphrase, privateKey;
if (walletbackup['mnemonic'] == null) { ViewOnlyWalletData? viewOnlyData;
if (info.isViewOnly) {
final viewOnlyDataEncoded =
walletbackup['viewOnlyWalletDataKey'] as String;
viewOnlyData = ViewOnlyWalletData.fromJsonEncodedString(
viewOnlyDataEncoded,
walletId: info.walletId,
);
} else if (walletbackup['mnemonic'] == null) {
// probably private key based // probably private key based
if (walletbackup['privateKey'] != null) { if (walletbackup['privateKey'] != null) {
privateKey = walletbackup['privateKey'] as String; privateKey = walletbackup['privateKey'] as String;
@ -486,6 +500,7 @@ abstract class SWB {
mnemonic: mnemonic, mnemonic: mnemonic,
mnemonicPassphrase: mnemonicPassphrase, mnemonicPassphrase: mnemonicPassphrase,
privateKey: privateKey, privateKey: privateKey,
viewOnlyData: viewOnlyData,
); );
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) { if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {

View file

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../utilities/util.dart';
import '../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../widgets/detail_item.dart';
import '../../wallet_view/transaction_views/transaction_details_view.dart';
class ViewOnlyWalletDataWidget extends StatelessWidget {
const ViewOnlyWalletDataWidget({
super.key,
required this.data,
});
final ViewOnlyWalletData data;
@override
Widget build(BuildContext context) {
return switch (data) {
final CryptonoteViewOnlyWalletData e => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DetailItem(
title: "Address",
detail: e.address,
button: Util.isDesktop
? IconCopyButton(
data: e.address,
)
: SimpleCopyButton(
data: e.address,
),
),
const SizedBox(
height: 16,
),
DetailItem(
title: "Private view key",
detail: e.privateViewKey,
button: Util.isDesktop
? IconCopyButton(
data: e.privateViewKey,
)
: SimpleCopyButton(
data: e.privateViewKey,
),
),
],
),
final AddressViewOnlyWalletData e => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DetailItem(
title: "Address",
detail: e.address,
button: Util.isDesktop
? IconCopyButton(
data: e.address,
)
: SimpleCopyButton(
data: e.address,
),
),
],
),
final ExtendedKeysViewOnlyWalletData e => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...e.xPubs.map(
(xPub) => DetailItem(
title: xPub.path,
detail: xPub.encoded,
button: Util.isDesktop
? IconCopyButton(
data: xPub.encoded,
)
: SimpleCopyButton(
data: xPub.encoded,
),
),
),
],
),
};
}
}

View file

@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.dart'; import '../../../../app_config.dart';
import '../../../../models/keys/cw_key_data.dart'; import '../../../../models/keys/cw_key_data.dart';
import '../../../../models/keys/key_data_interface.dart'; import '../../../../models/keys/key_data_interface.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../models/keys/xpriv_data.dart'; import '../../../../models/keys/xpriv_data.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
@ -39,6 +40,7 @@ import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_dialog.dart';
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../wallet_view/transaction_views/transaction_details_view.dart'; import '../../../wallet_view/transaction_views/transaction_details_view.dart';
import '../../sub_widgets/view_only_wallet_data_widget.dart';
import 'cn_wallet_keys.dart'; import 'cn_wallet_keys.dart';
import 'wallet_xprivs.dart'; import 'wallet_xprivs.dart';
@ -426,7 +428,7 @@ class _FrostKeys extends StatelessWidget {
} }
} }
class MobileKeyDataView extends StatelessWidget { class MobileKeyDataView extends ConsumerWidget {
const MobileKeyDataView({ const MobileKeyDataView({
super.key, super.key,
required this.walletId, required this.walletId,
@ -441,7 +443,7 @@ class MobileKeyDataView extends StatelessWidget {
final KeyDataInterface keyData; final KeyDataInterface keyData;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -455,6 +457,7 @@ class MobileKeyDataView extends StatelessWidget {
"Wallet ${switch (keyData.runtimeType) { "Wallet ${switch (keyData.runtimeType) {
const (XPrivData) => "xpriv(s)", const (XPrivData) => "xpriv(s)",
const (CWKeyData) => "keys", const (CWKeyData) => "keys",
const (ViewOnlyWalletData) => "keys",
_ => throw UnimplementedError( _ => throw UnimplementedError(
"Don't forget to add your KeyDataInterface here!", "Don't forget to add your KeyDataInterface here!",
), ),
@ -483,6 +486,10 @@ class MobileKeyDataView extends StatelessWidget {
walletId: walletId, walletId: walletId,
cwKeyData: keyData as CWKeyData, cwKeyData: keyData as CWKeyData,
), ),
const (ViewOnlyWalletData) =>
ViewOnlyWalletDataWidget(
data: keyData as ViewOnlyWalletData,
),
_ => throw UnimplementedError( _ => throw UnimplementedError(
"Don't forget to add your KeyDataInterface here!", "Don't forget to add your KeyDataInterface here!",
), ),

View file

@ -49,7 +49,7 @@ class WalletXPrivsState extends ConsumerState<WalletXPrivs> {
late String _currentDropDownValue; late String _currentDropDownValue;
String _current(String key) => String _current(String key) =>
widget.xprivData.xprivs.firstWhere((e) => e.path == key).xpriv; widget.xprivData.xprivs.firstWhere((e) => e.path == key).encoded;
Future<void> _copy() async { Future<void> _copy() async {
await widget.clipboardInterface.setData( await widget.clipboardInterface.setData(

View file

@ -19,6 +19,7 @@ import '../../../db/hive/db.dart';
import '../../../db/sqlite/firo_cache.dart'; import '../../../db/sqlite/firo_cache.dart';
import '../../../models/epicbox_config_model.dart'; import '../../../models/epicbox_config_model.dart';
import '../../../models/keys/key_data_interface.dart'; import '../../../models/keys/key_data_interface.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../notifications/show_flush_bar.dart'; import '../../../notifications/show_flush_bar.dart';
import '../../../providers/global/wallets_provider.dart'; import '../../../providers/global/wallets_provider.dart';
import '../../../providers/ui/transaction_filter_provider.dart'; import '../../../providers/ui/transaction_filter_provider.dart';
@ -99,8 +100,13 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
void initState() { void initState() {
walletId = widget.walletId; walletId = widget.walletId;
coin = widget.coin; coin = widget.coin;
xPubEnabled =
ref.read(pWallets).getWallet(walletId) is ExtendedKeysInterface; final wallet = ref.read(pWallets).getWallet(walletId);
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
xPubEnabled = false;
} else {
xPubEnabled = wallet is ExtendedKeysInterface;
}
xpub = ""; xpub = "";
@ -166,6 +172,15 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
final wallet = ref.read(pWallets).getWallet(widget.walletId);
bool canBackup = true;
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
canBackup = false;
}
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -248,146 +263,154 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
); );
}, },
), ),
const SizedBox( if (canBackup)
height: 8, const SizedBox(
), height: 8,
Consumer( ),
builder: (_, ref, __) { if (canBackup)
return SettingsListButton( Consumer(
iconAssetName: Assets.svg.lock, builder: (_, ref, __) {
iconSize: 16, return SettingsListButton(
title: "Wallet backup", iconAssetName: Assets.svg.lock,
onPressed: () async { iconSize: 16,
final wallet = ref title: "Wallet backup",
.read(pWallets) onPressed: () async {
.getWallet(widget.walletId); // TODO: [prio=med] take wallets that don't have a mnemonic into account
// TODO: [prio=med] take wallets that don't have a mnemonic into account List<String>? mnemonic;
List<String>? mnemonic;
({
String myName,
String config,
String keys,
({ ({
String myName,
String config, String config,
String keys String keys,
})? prevGen, ({
})? frostWalletData; String config,
ViewOnlyWalletData? voData; String keys
if (wallet is BitcoinFrostWallet) { })? prevGen,
final futures = [ })? frostWalletData;
wallet.getSerializedKeys(), if (wallet is BitcoinFrostWallet) {
wallet.getMultisigConfig(), final futures = [
wallet.getSerializedKeysPrevGen(), wallet.getSerializedKeys(),
wallet.getMultisigConfigPrevGen(), wallet.getMultisigConfig(),
]; wallet.getSerializedKeysPrevGen(),
wallet.getMultisigConfigPrevGen(),
];
final results = final results =
await Future.wait(futures); await Future.wait(futures);
if (results.length == 4) { if (results.length == 4) {
frostWalletData = ( frostWalletData = (
myName: wallet.frostInfo.myName, myName: wallet.frostInfo.myName,
config: results[1]!, config: results[1]!,
keys: results[0]!, keys: results[0]!,
prevGen: results[2] == null || prevGen: results[2] == null ||
results[3] == null results[3] == null
? null ? null
: ( : (
config: results[3]!, config: results[3]!,
keys: results[2]!, keys: results[2]!,
), ),
); );
}
} else {
if (wallet is MnemonicInterface) {
if (wallet
is ViewOnlyOptionInterface &&
!(wallet
as ViewOnlyOptionInterface)
.isViewOnly) {
mnemonic = await wallet
.getMnemonicAsWords();
}
}
} }
} else {
KeyDataInterface? keyData;
if (wallet if (wallet
is ViewOnlyOptionInterface && is ViewOnlyOptionInterface &&
wallet.isViewOnly) { wallet.isViewOnly) {
voData = await wallet keyData = await wallet
.getViewOnlyWalletData(); .getViewOnlyWalletData();
} else if (wallet } else if (wallet
is MnemonicInterface) { is ExtendedKeysInterface) {
mnemonic = await wallet keyData = await wallet.getXPrivs();
.getMnemonicAsWords(); } else if (wallet
is LibMoneroWallet) {
keyData = await wallet.getKeys();
} }
}
KeyDataInterface? keyData; if (context.mounted) {
if (wallet is ExtendedKeysInterface) { if (keyData != null &&
keyData = await wallet.getXPrivs(); wallet
} else if (wallet is LibMoneroWallet) { is ViewOnlyOptionInterface) {
keyData = await wallet.getKeys(); await Navigator.push(
} context,
RouteGenerator.getRoute(
if (context.mounted) { shouldUseMaterialRoute:
if (voData != null) { RouteGenerator
await Navigator.push( .useMaterialPageRoute,
context, builder: (_) =>
RouteGenerator.getRoute( LockscreenView(
shouldUseMaterialRoute: routeOnSuccessArguments: (
RouteGenerator walletId: walletId,
.useMaterialPageRoute, keyData: keyData,
builder: (_) => LockscreenView( ),
routeOnSuccessArguments: ( showBackButton: true,
walletId: walletId, routeOnSuccess:
keyData: keyData, MobileKeyDataView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery data",
biometricsAuthenticationTitle:
"View recovery data",
), ),
showBackButton: true, settings: const RouteSettings(
routeOnSuccess: name:
MobileKeyDataView "/viewRecoveryDataLockscreen",
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery data",
biometricsAuthenticationTitle:
"View recovery data",
),
settings: const RouteSettings(
name:
"/viewRecoveryDataLockscreen",
),
),
);
} else {
await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) => LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
keyData: keyData,
), ),
showBackButton: true,
routeOnSuccess:
WalletBackupView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
), ),
settings: const RouteSettings( );
name: } else {
"/viewRecoverPhraseLockscreen", await Navigator.push(
context,
RouteGenerator.getRoute(
shouldUseMaterialRoute:
RouteGenerator
.useMaterialPageRoute,
builder: (_) =>
LockscreenView(
routeOnSuccessArguments: (
walletId: walletId,
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
keyData: keyData,
),
showBackButton: true,
routeOnSuccess:
WalletBackupView
.routeName,
biometricsCancelButtonString:
"CANCEL",
biometricsLocalizedReason:
"Authenticate to view recovery phrase",
biometricsAuthenticationTitle:
"View recovery phrase",
),
settings: const RouteSettings(
name:
"/viewRecoverPhraseLockscreen",
),
), ),
), );
); }
} }
} },
}, );
); },
}, ),
),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.dart'; import '../../../../app_config.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import '../../../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart';
import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/global/secure_store_provider.dart';
import '../../../../providers/global/wallets_provider.dart'; import '../../../../providers/global/wallets_provider.dart';
@ -10,16 +11,13 @@ import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart'; import '../../../../utilities/util.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_dialog.dart';
import '../../../home_view/home_view.dart'; import '../../../home_view/home_view.dart';
import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart'; import '../../sub_widgets/view_only_wallet_data_widget.dart';
class DeleteViewOnlyWalletKeysView extends ConsumerStatefulWidget { class DeleteViewOnlyWalletKeysView extends ConsumerStatefulWidget {
const DeleteViewOnlyWalletKeysView({ const DeleteViewOnlyWalletKeysView({
@ -161,34 +159,9 @@ class _DeleteViewOnlyWalletKeysViewState
const SizedBox( const SizedBox(
height: 24, height: 24,
), ),
if (widget.data.address != null) ViewOnlyWalletDataWidget(
DetailItem( data: widget.data,
title: "Address", ),
detail: widget.data.address!,
button: Util.isDesktop
? IconCopyButton(
data: widget.data.address!,
)
: SimpleCopyButton(
data: widget.data.address!,
),
),
if (widget.data.address != null)
const SizedBox(
height: 16,
),
if (widget.data.privateViewKey != null)
DetailItem(
title: "Private view key",
detail: widget.data.privateViewKey!,
button: Util.isDesktop
? IconCopyButton(
data: widget.data.privateViewKey!,
)
: SimpleCopyButton(
data: widget.data.privateViewKey!,
),
),
if (!Util.isDesktop) const Spacer(), if (!Util.isDesktop) const Spacer(),
const SizedBox( const SizedBox(
height: 16, height: 16,

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.dart'; import '../../../../app_config.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../providers/providers.dart'; import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';

View file

@ -11,6 +11,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/db/main_db_provider.dart';
import '../../../../providers/providers.dart'; import '../../../../providers/providers.dart';
import '../../../../route_generator.dart'; import '../../../../route_generator.dart';
@ -23,6 +24,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.da
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/background.dart'; import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
@ -133,6 +135,12 @@ class _WalletSettingsWalletSettingsViewState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly;
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -189,13 +197,11 @@ class _WalletSettingsWalletSettingsViewState
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is RbfInterface)
is RbfInterface)
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is RbfInterface)
is RbfInterface)
RoundedWhiteContainer( RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RawMaterialButton( child: RawMaterialButton(
@ -227,13 +233,11 @@ class _WalletSettingsWalletSettingsViewState
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
is MultiAddressInterface)
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
is MultiAddressInterface)
RoundedWhiteContainer( RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RawMaterialButton( child: RawMaterialButton(
@ -278,13 +282,11 @@ class _WalletSettingsWalletSettingsViewState
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is LelantusInterface)
is LelantusInterface)
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is LelantusInterface)
is LelantusInterface)
RoundedWhiteContainer( RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RawMaterialButton( child: RawMaterialButton(
@ -316,13 +318,11 @@ class _WalletSettingsWalletSettingsViewState
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is SparkInterface)
is SparkInterface)
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
if (ref.watch(pWallets).getWallet(widget.walletId) if (wallet is SparkInterface)
is SparkInterface)
RoundedWhiteContainer( RoundedWhiteContainer(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: RawMaterialButton( child: RawMaterialButton(

View file

@ -57,7 +57,7 @@ class XPubViewState extends ConsumerState<XPubView> {
late String _currentDropDownValue; late String _currentDropDownValue;
String _current(String key) => String _current(String key) =>
widget.xpubData.xpubs.firstWhere((e) => e.path == key).xpub; widget.xpubData.xpubs.firstWhere((e) => e.path == key).encoded;
Future<void> _copy() async { Future<void> _copy() async {
await widget.clipboardInterface.setData( await widget.clipboardInterface.setData(

View file

@ -186,6 +186,21 @@ class WalletSummaryInfo extends ConsumerWidget {
), ),
), ),
const Spacer(), const Spacer(),
if (ref.watch(pWalletInfo(walletId)).isViewOnly)
FittedBox(
fit: BoxFit.scaleDown,
child: SelectableText(
"(View only)",
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 18,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard
.withOpacity(0.7),
),
),
),
const Spacer(),
FittedBox( FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: SelectableText( child: SelectableText(

View file

@ -21,6 +21,7 @@ import 'package:isar/isar.dart';
import '../../../db/sqlite/firo_cache.dart'; import '../../../db/sqlite/firo_cache.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/isar/models/isar_models.dart'; import '../../../models/isar/models/isar_models.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import '../../../pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
import '../../../pages/token_view/my_tokens_view.dart'; import '../../../pages/token_view/my_tokens_view.dart';
import '../../../pages/wallet_view/sub_widgets/transactions_list.dart'; import '../../../pages/wallet_view/sub_widgets/transactions_list.dart';
@ -44,6 +45,7 @@ import '../../../utilities/wallet_tools.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/impl/banano_wallet.dart'; import '../../../wallets/wallet/impl/banano_wallet.dart';
import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_app_bar.dart';
@ -168,6 +170,11 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null; final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
// if the view only wallet watches a single address there are no keys of any kind
final showKeysButton = !(wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly);
return DesktopScaffold( return DesktopScaffold(
appBar: DesktopAppBar( appBar: DesktopAppBar(
background: Theme.of(context).extension<StackColors>()!.popupBG, background: Theme.of(context).extension<StackColors>()!.popupBG,
@ -216,6 +223,19 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
), ),
), ),
), ),
if (ref.watch(pWalletInfo(widget.walletId)).isViewOnly)
const SizedBox(
width: 20,
),
if (ref.watch(pWalletInfo(widget.walletId)).isViewOnly)
Text(
"(View only)",
style: STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconLeft,
),
),
if (kDebugMode) const Spacer(), if (kDebugMode) const Spacer(),
if (kDebugMode) if (kDebugMode)
Column( Column(
@ -312,12 +332,14 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
walletId: widget.walletId, walletId: widget.walletId,
eventBus: eventBus, eventBus: eventBus,
), ),
const SizedBox( if (showKeysButton)
width: 2, const SizedBox(
), width: 2,
WalletKeysButton( ),
walletId: widget.walletId, if (showKeysButton)
), WalletKeysButton(
walletId: widget.walletId,
),
const SizedBox( const SizedBox(
width: 2, width: 2,
), ),

View file

@ -19,6 +19,7 @@ import 'package:isar/isar.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/isar/models/isar_models.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart'; import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart';
import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/db/main_db_provider.dart';
@ -40,6 +41,7 @@ import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_loading_overlay.dart'; import '../../../../widgets/custom_loading_overlay.dart';
@ -185,10 +187,15 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
clipboard = widget.clipboard; clipboard = widget.clipboard;
final wallet = ref.read(pWallets).getWallet(walletId); final wallet = ref.read(pWallets).getWallet(walletId);
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
showMultiType = supportsSpark ||
(wallet is! BCashInterface && if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
wallet is Bip39HDWallet && showMultiType = false;
wallet.supportedAddressTypes.length > 1); } else {
showMultiType = supportsSpark ||
(wallet is! BCashInterface &&
wallet is Bip39HDWallet &&
wallet.supportedAddressTypes.length > 1);
}
_walletAddressTypes.add(wallet.info.mainAddressType); _walletAddressTypes.add(wallet.info.mainAddressType);
@ -259,6 +266,18 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
address = ref.watch(pWalletReceivingAddress(walletId)); address = ref.watch(pWalletReceivingAddress(walletId));
} }
final wallet =
ref.watch(pWallets.select((value) => value.getWallet(walletId)));
final bool canGen;
if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
canGen = false;
} else {
canGen = (wallet is MultiAddressInterface || supportsSpark);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@ -430,16 +449,12 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
), ),
), ),
if (ref.watch(pWallets.select((value) => value.getWallet(walletId))) if (canGen)
is MultiAddressInterface ||
supportsSpark)
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
if (ref.watch(pWallets.select((value) => value.getWallet(walletId))) if (canGen)
is MultiAddressInterface ||
supportsSpark)
SecondaryButton( SecondaryButton(
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
onPressed: supportsSpark && onPressed: supportsSpark &&

View file

@ -14,6 +14,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../app_config.dart'; import '../../../../../app_config.dart';
import '../../../../../db/sqlite/firo_cache.dart'; import '../../../../../db/sqlite/firo_cache.dart';
import '../../../../../models/keys/view_only_wallet_data.dart';
import '../../../../../providers/db/main_db_provider.dart'; import '../../../../../providers/db/main_db_provider.dart';
import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/prefs_provider.dart';
import '../../../../../providers/global/wallets_provider.dart'; import '../../../../../providers/global/wallets_provider.dart';
@ -240,6 +241,9 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
); );
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly &&
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly;
return DesktopDialog( return DesktopDialog(
child: Column( child: Column(
@ -386,40 +390,41 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
), ),
), ),
// reuseAddress preference. // reuseAddress preference.
_MoreFeaturesItemBase( if (!isViewOnlyNoAddressGen)
onPressed: _switchReuseAddressToggled, _MoreFeaturesItemBase(
child: Row( onPressed: _switchReuseAddressToggled,
children: [ child: Row(
const SizedBox(width: 3), children: [
SizedBox( const SizedBox(width: 3),
height: 20, SizedBox(
width: 40, height: 20,
child: IgnorePointer( width: 40,
child: DraggableSwitchButton( child: IgnorePointer(
isOn: ref.watch( child: DraggableSwitchButton(
pWalletInfo(widget.walletId) isOn: ref.watch(
.select((value) => value.otherData), pWalletInfo(widget.walletId)
)[WalletInfoKeys.reuseAddress] as bool? ?? .select((value) => value.otherData),
false, )[WalletInfoKeys.reuseAddress] as bool? ??
controller: _switchController, false,
controller: _switchController,
),
), ),
), ),
), const SizedBox(
const SizedBox( width: 16,
width: 16, ),
), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( "Reuse receiving address",
"Reuse receiving address", style: STextStyles.w600_20(context),
style: STextStyles.w600_20(context), ),
), ],
], ),
), ],
], ),
), ),
),
const SizedBox( const SizedBox(
height: 28, height: 28,
), ),

View file

@ -83,7 +83,9 @@ class _UnlockWalletKeysDesktopState
.verifyPassphrase(passwordController.text); .verifyPassphrase(passwordController.text);
if (verified) { if (verified) {
Navigator.of(context, rootNavigator: true).pop(); if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
final wallet = ref.read(pWallets).getWallet(widget.walletId); final wallet = ref.read(pWallets).getWallet(widget.walletId);
({String keys, String config})? frostData; ({String keys, String config})? frostData;
@ -101,7 +103,8 @@ class _UnlockWalletKeysDesktopState
throw Exception("FIXME ~= see todo in code"); throw Exception("FIXME ~= see todo in code");
} }
} else { } else {
if (wallet is ViewOnlyOptionInterface) { if (wallet is ViewOnlyOptionInterface &&
(wallet as ViewOnlyOptionInterface).isViewOnly) {
// TODO: is something needed here? // TODO: is something needed here?
} else { } else {
words = await wallet.getMnemonicAsWords(); words = await wallet.getMnemonicAsWords();
@ -109,7 +112,9 @@ class _UnlockWalletKeysDesktopState
} }
KeyDataInterface? keyData; KeyDataInterface? keyData;
if (wallet is ExtendedKeysInterface) { if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
keyData = await wallet.getViewOnlyWalletData();
} else if (wallet is ExtendedKeysInterface) {
keyData = await wallet.getXPrivs(); keyData = await wallet.getXPrivs();
} else if (wallet is LibMoneroWallet) { } else if (wallet is LibMoneroWallet) {
keyData = await wallet.getKeys(); keyData = await wallet.getKeys();
@ -127,17 +132,20 @@ class _UnlockWalletKeysDesktopState
); );
} }
} else { } else {
Navigator.of(context, rootNavigator: true).pop(); if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
}
await Future<void>.delayed(const Duration(milliseconds: 300)); await Future<void>.delayed(const Duration(milliseconds: 300));
if (mounted) {
unawaited( unawaited(
showFloatingFlushBar( showFloatingFlushBar(
type: FlushBarType.warning, type: FlushBarType.warning,
message: "Invalid passphrase!", message: "Invalid passphrase!",
context: context, context: context,
), ),
); );
}
} }
} }
@ -332,7 +340,10 @@ class _UnlockWalletKeysDesktopState
.verifyPassphrase(passwordController.text); .verifyPassphrase(passwordController.text);
if (verified) { if (verified) {
Navigator.of(context, rootNavigator: true).pop(); if (context.mounted) {
Navigator.of(context, rootNavigator: true)
.pop();
}
({String keys, String config})? frostData; ({String keys, String config})? frostData;
List<String>? words; List<String>? words;
@ -352,7 +363,9 @@ class _UnlockWalletKeysDesktopState
throw Exception("FIXME ~= see todo in code"); throw Exception("FIXME ~= see todo in code");
} }
} else { } else {
if (wallet is ViewOnlyOptionInterface) { if (wallet is ViewOnlyOptionInterface &&
(wallet as ViewOnlyOptionInterface)
.isViewOnly) {
// TODO: is something needed here? // TODO: is something needed here?
} else { } else {
words = await wallet.getMnemonicAsWords(); words = await wallet.getMnemonicAsWords();
@ -360,7 +373,10 @@ class _UnlockWalletKeysDesktopState
} }
KeyDataInterface? keyData; KeyDataInterface? keyData;
if (wallet is ExtendedKeysInterface) { if (wallet is ViewOnlyOptionInterface &&
wallet.isViewOnly) {
keyData = await wallet.getViewOnlyWalletData();
} else if (wallet is ExtendedKeysInterface) {
keyData = await wallet.getXPrivs(); keyData = await wallet.getXPrivs();
} else if (wallet is LibMoneroWallet) { } else if (wallet is LibMoneroWallet) {
keyData = await wallet.getKeys(); keyData = await wallet.getKeys();
@ -379,19 +395,23 @@ class _UnlockWalletKeysDesktopState
); );
} }
} else { } else {
Navigator.of(context, rootNavigator: true).pop(); if (context.mounted) {
Navigator.of(context, rootNavigator: true)
.pop();
}
await Future<void>.delayed( await Future<void>.delayed(
const Duration(milliseconds: 300), const Duration(milliseconds: 300),
); );
if (context.mounted) {
unawaited( unawaited(
showFloatingFlushBar( showFloatingFlushBar(
type: FlushBarType.warning, type: FlushBarType.warning,
message: "Invalid passphrase!", message: "Invalid passphrase!",
context: context, context: context,
), ),
); );
}
} }
} }
: null, : null,

View file

@ -16,9 +16,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../models/keys/cw_key_data.dart'; import '../../../../models/keys/cw_key_data.dart';
import '../../../../models/keys/key_data_interface.dart'; import '../../../../models/keys/key_data_interface.dart';
import '../../../../models/keys/view_only_wallet_data.dart';
import '../../../../models/keys/xpriv_data.dart'; import '../../../../models/keys/xpriv_data.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../../pages/settings_views/sub_widgets/view_only_wallet_data_widget.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart';
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart'; import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
@ -180,32 +182,39 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
], ],
) )
: keyData != null : keyData != null
? CustomTabView( ? keyData is ViewOnlyWalletData
titles: [ ? Padding(
if (words.isNotEmpty) "Mnemonic", padding: const EdgeInsets.symmetric(horizontal: 16),
if (keyData is XPrivData) "XPriv(s)", child: ViewOnlyWalletDataWidget(
if (keyData is CWKeyData) "Keys", data: keyData as ViewOnlyWalletData,
],
children: [
if (words.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 16),
child: _Mnemonic(
words: words,
),
), ),
if (keyData is XPrivData) )
WalletXPrivs( : CustomTabView(
xprivData: keyData as XPrivData, titles: [
walletId: walletId, if (words.isNotEmpty) "Mnemonic",
), if (keyData is XPrivData) "XPriv(s)",
if (keyData is CWKeyData) if (keyData is CWKeyData) "Keys",
CNWalletKeys( ],
cwKeyData: keyData as CWKeyData, children: [
walletId: walletId, if (words.isNotEmpty)
), Padding(
], padding: const EdgeInsets.only(top: 16),
) child: _Mnemonic(
words: words,
),
),
if (keyData is XPrivData)
WalletXPrivs(
xprivData: keyData as XPrivData,
walletId: walletId,
),
if (keyData is CWKeyData)
CNWalletKeys(
cwKeyData: keyData as CWKeyData,
walletId: walletId,
),
],
)
: _Mnemonic( : _Mnemonic(
words: words, words: words,
), ),

View file

@ -31,6 +31,7 @@ import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import '../../../addresses/desktop_wallet_addresses_view.dart'; import '../../../addresses/desktop_wallet_addresses_view.dart';
import '../../../lelantus_coins/lelantus_coins_view.dart'; import '../../../lelantus_coins/lelantus_coins_view.dart';
import '../../../spark_coins/spark_coins_view.dart'; import '../../../spark_coins/spark_coins_view.dart';
@ -295,8 +296,12 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final firoDebug = kDebugMode && (coin is Firo); final firoDebug = kDebugMode && (coin is Firo);
final bool xpubEnabled = final wallet = ref.watch(pWallets).getWallet(walletId);
ref.watch(pWallets).getWallet(walletId) is ExtendedKeysInterface; bool xpubEnabled = wallet is ExtendedKeysInterface;
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
xpubEnabled = false;
}
final bool canChangeRep = coin is NanoCurrency; final bool canChangeRep = coin is NanoCurrency;

View file

@ -23,6 +23,7 @@ import 'models/isar/models/contact_entry.dart';
import 'models/isar/models/isar_models.dart'; import 'models/isar/models/isar_models.dart';
import 'models/isar/ordinal.dart'; import 'models/isar/ordinal.dart';
import 'models/keys/key_data_interface.dart'; import 'models/keys/key_data_interface.dart';
import 'models/keys/view_only_wallet_data.dart';
import 'models/paynym/paynym_account_lite.dart'; import 'models/paynym/paynym_account_lite.dart';
import 'models/send_view_auto_fill_data.dart'; import 'models/send_view_auto_fill_data.dart';
import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
@ -205,7 +206,6 @@ import 'wallets/crypto_currency/intermediate/frost_currency.dart';
import 'wallets/models/tx_data.dart'; import 'wallets/models/tx_data.dart';
import 'wallets/wallet/wallet.dart'; import 'wallets/wallet/wallet.dart';
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import 'wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
import 'widgets/choose_coin_view.dart'; import 'widgets/choose_coin_view.dart';
import 'widgets/frost_scaffold.dart'; import 'widgets/frost_scaffold.dart';

View file

@ -84,6 +84,9 @@ class Wallets {
key: Wallet.mnemonicPassphraseKey(walletId: walletId), key: Wallet.mnemonicPassphraseKey(walletId: walletId),
); );
await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId)); await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId));
await secureStorage.delete(
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
);
if (info.coin is Wownero) { if (info.coin is Wownero) {
await lib_monero_compat.deleteWalletFiles( await lib_monero_compat.deleteWalletFiles(

View file

@ -92,7 +92,7 @@ class Wownero extends CryptonoteCurrency {
bool get hasMnemonicPassphraseSupport => false; bool get hasMnemonicPassphraseSupport => false;
@override @override
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 25]; List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 16, 25];
@override @override
BigInt get satsPerCoin => BigInt.from(100000000000); BigInt get satsPerCoin => BigInt.from(100000000000);

View file

@ -5,9 +5,11 @@ import 'package:flutter/foundation.dart';
import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/derive_path_type_enum.dart';
import '../interfaces/view_only_option_currency_interface.dart';
import 'bip39_currency.dart'; import 'bip39_currency.dart';
abstract class Bip39HDCurrency extends Bip39Currency { abstract class Bip39HDCurrency extends Bip39Currency
implements ViewOnlyOptionCurrencyInterface {
Bip39HDCurrency(super.network); Bip39HDCurrency(super.network);
coinlib.Network get networkParams; coinlib.Network get networkParams;
@ -39,6 +41,25 @@ abstract class Bip39HDCurrency extends Bip39Currency {
} }
} }
List<String> get supportedHardenedDerivationPaths {
final paths = supportedDerivationPathTypes.map(
(e) => (
path: e,
addressType: e.getAddressType(),
),
);
return paths.map((e) {
final path = constructDerivePath(
derivePathType: e.path,
chain: 0,
index: 0,
);
// trim unhardened
return path.substring(0, path.lastIndexOf("'") + 1);
}).toList();
}
static String convertBytesToScriptHash(Uint8List bytes) { static String convertBytesToScriptHash(Uint8List bytes) {
final hash = sha256.convert(bytes.toList(growable: false)).toString(); final hash = sha256.convert(bytes.toList(growable: false)).toString();

View file

@ -6,6 +6,7 @@ import 'package:uuid/uuid.dart';
import '../../../app_config.dart'; import '../../../app_config.dart';
import '../../../models/balance.dart'; import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/crypto_currency.dart';
import '../isar_id_interface.dart'; import '../isar_id_interface.dart';
import 'wallet_info_meta.dart'; import 'wallet_info_meta.dart';
@ -121,6 +122,13 @@ class WalletInfo implements IsarId {
bool get isViewOnly => bool get isViewOnly =>
otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false; otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false;
@ignore
ViewOnlyWalletType? get viewOnlyWalletType {
final index = otherData[WalletInfoKeys.viewOnlyTypeIndexKey] as int?;
if (index == null) return null;
return ViewOnlyWalletType.values[index];
}
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 ==
@ -517,4 +525,5 @@ abstract class WalletInfoKeys {
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"; static const String isViewOnlyKey = "isViewOnlyKey";
static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey";
} }

View file

@ -1,4 +1,5 @@
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../isar_id_interface.dart'; import '../isar_id_interface.dart';
part 'wallet_info_meta.g.dart'; part 'wallet_info_meta.g.dart';

View file

@ -6,15 +6,17 @@ import 'package:isar/isar.dart';
import '../../../models/balance.dart'; import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/extensions/extensions.dart';
import '../../crypto_currency/intermediate/bip39_hd_currency.dart'; import '../../crypto_currency/intermediate/bip39_hd_currency.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 'bip39_wallet.dart'; import 'bip39_wallet.dart';
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T> abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
with MultiAddressInterface<T> { with MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
Bip39HDWallet(super.cryptoCurrency); Bip39HDWallet(super.cryptoCurrency);
Set<AddressType> get supportedAddressTypes => Set<AddressType> get supportedAddressTypes =>
@ -116,7 +118,9 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
@override @override
Future<void> checkSaveInitialReceivingAddress() async { Future<void> checkSaveInitialReceivingAddress() async {
final current = await getCurrentChangeAddress(); if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) return;
final current = await getCurrentReceivingAddress();
if (current == null) { if (current == null) {
final address = await _generateAddress( final address = await _generateAddress(
chain: 0, // receiving chain: 0, // receiving
@ -174,15 +178,29 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
required int index, required int index,
required DerivePathType derivePathType, required DerivePathType derivePathType,
}) async { }) async {
final root = await getRootHDNode();
final derivationPath = cryptoCurrency.constructDerivePath( final derivationPath = cryptoCurrency.constructDerivePath(
derivePathType: derivePathType, derivePathType: derivePathType,
chain: chain, chain: chain,
index: index, index: index,
); );
final keys = root.derivePath(derivationPath); final coinlib.HDKey keys;
if (isViewOnly) {
final idx = derivationPath.lastIndexOf("'/");
final path = derivationPath.substring(idx + 2);
final data =
await getViewOnlyWalletData() as ExtendedKeysViewOnlyWalletData;
final xPub = data.xPubs.firstWhere(
(e) => derivationPath.startsWith(e.path),
);
final node = coinlib.HDPublicKey.decode(xPub.encoded);
keys = node.derivePath(path);
} else {
final root = await getRootHDNode();
keys = root.derivePath(derivationPath);
}
final data = cryptoCurrency.getAddressForPublicKey( final data = cryptoCurrency.getAddressForPublicKey(
publicKey: keys.publicKey, publicKey: keys.publicKey,
@ -205,7 +223,8 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
value: convertAddressString(data.address.toString()), value: convertAddressString(data.address.toString()),
publicKey: keys.publicKey.data, publicKey: keys.publicKey.data,
derivationIndex: index, derivationIndex: index,
derivationPath: DerivationPath()..value = derivationPath, derivationPath:
isViewOnly ? null : (DerivationPath()..value = derivationPath),
type: data.addressType, type: data.addressType,
subType: subType, subType: subType,
); );

View file

@ -18,6 +18,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/keys/cw_key_data.dart'; import '../../../models/keys/cw_key_data.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../models/paymint/fee_object_model.dart'; import '../../../models/paymint/fee_object_model.dart';
import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart';
@ -41,7 +42,8 @@ import 'cryptonote_wallet.dart';
abstract class LibMoneroWallet<T extends CryptonoteCurrency> abstract class LibMoneroWallet<T extends CryptonoteCurrency>
extends CryptonoteWallet<T> extends CryptonoteWallet<T>
implements MultiAddressInterface<T>, ViewOnlyOptionInterface<T> { with ViewOnlyOptionInterface<T>
implements MultiAddressInterface<T> {
@override @override
int get isarTransactionVersion => 2; int get isarTransactionVersion => 2;
@ -91,8 +93,16 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
.walletIdEqualTo(walletId) .walletIdEqualTo(walletId)
.watch(fireImmediately: true) .watch(fireImmediately: true)
.listen((utxos) async { .listen((utxos) async {
await onUTXOsChanged(utxos); try {
await updateBalance(shouldUpdateUtxos: false); await onUTXOsChanged(utxos);
await updateBalance(shouldUpdateUtxos: false);
} catch (e, s) {
lib_monero.Logging.log?.i(
"_startInit",
error: e,
stackTrace: s,
);
}
}); });
}); });
} }
@ -286,6 +296,22 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
); );
} }
Future<(String, String)>
hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async {
final path = await pathForWallet(name: walletId, type: compatType);
final String password;
try {
password = (await secureStorageInterface.read(
key: lib_monero_compat.libMoneroWalletPasswordKey(walletId),
))!;
} catch (e, s) {
throw Exception("Password not found $e, $s");
}
loadWallet(path: path, password: password);
final wallet = libMoneroWallet!;
return (wallet.getAddress().value, wallet.getPrivateViewKey());
}
@override @override
Future<void> init({bool? isRestore}) async { Future<void> init({bool? isRestore}) async {
final path = await pathForWallet( final path = await pathForWallet(
@ -1301,19 +1327,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
// ============== View only ================================================== // ============== View only ==================================================
@override
bool get isViewOnly => info.isViewOnly;
@override @override
Future<void> recoverViewOnly() async { Future<void> recoverViewOnly() async {
await refreshMutex.protect(() async { await refreshMutex.protect(() async {
final jsonEncodedString = await secureStorageInterface.read( final data =
key: Wallet.getViewOnlyWalletDataSecStoreKey( await getViewOnlyWalletData() as CryptonoteViewOnlyWalletData;
walletId: walletId,
),
);
final data = ViewOnlyWalletData.fromJsonEncodedString(jsonEncodedString!);
try { try {
final height = max(info.restoreHeight, 0); final height = max(info.restoreHeight, 0);
@ -1338,8 +1356,8 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
final wallet = await getRestoredFromViewKeyWallet( final wallet = await getRestoredFromViewKeyWallet(
path: path, path: path,
password: password, password: password,
address: data.address!, address: data.address,
privateViewKey: data.privateViewKey!, privateViewKey: data.privateViewKey,
height: height, height: height,
); );
@ -1387,14 +1405,6 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
}); });
} }
@override
Future<ViewOnlyWalletData> getViewOnlyWalletData() async {
return ViewOnlyWalletData(
address: libMoneroWallet!.getAddress().value,
privateViewKey: libMoneroWallet!.getPrivateViewKey(),
);
}
// ============== Private ==================================================== // ============== Private ====================================================
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener; StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;

View file

@ -7,6 +7,7 @@ import 'package:mutex/mutex.dart';
import '../../db/isar/main_db.dart'; import '../../db/isar/main_db.dart';
import '../../models/isar/models/blockchain_data/address.dart'; import '../../models/isar/models/blockchain_data/address.dart';
import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../models/keys/view_only_wallet_data.dart';
import '../../models/node_model.dart'; import '../../models/node_model.dart';
import '../../models/paymint/fee_object_model.dart'; import '../../models/paymint/fee_object_model.dart';
import '../../services/event_bus/events/global/node_connection_status_changed_event.dart'; import '../../services/event_bus/events/global/node_connection_status_changed_event.dart';
@ -161,7 +162,7 @@ abstract class Wallet<T extends CryptoCurrency> {
prefs: prefs, prefs: prefs,
); );
if (wallet is ViewOnlyOptionInterface) { if (wallet is ViewOnlyOptionInterface && walletInfo.isViewOnly) {
await secureStorageInterface.write( await secureStorageInterface.write(
key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId), key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId),
value: viewOnlyData!.toJsonEncodedString(), value: viewOnlyData!.toJsonEncodedString(),
@ -583,6 +584,9 @@ abstract class Wallet<T extends CryptoCurrency> {
if (!tAlive) throw Exception("refresh alive ping failure"); if (!tAlive) throw Exception("refresh alive ping failure");
} }
final viewOnly = this is ViewOnlyOptionInterface &&
(this as ViewOnlyOptionInterface).isViewOnly;
try { try {
// this acquire should be almost instant due to above check. // this acquire should be almost instant due to above check.
// Slight possibility of race but should be irrelevant // Slight possibility of race but should be irrelevant
@ -607,7 +611,7 @@ abstract class Wallet<T extends CryptoCurrency> {
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
final Set<String> codesToCheck = {}; final Set<String> codesToCheck = {};
_checkAlive(); _checkAlive();
if (this is PaynymInterface) { if (this is PaynymInterface && !viewOnly) {
// isSegwit does not matter here at all // isSegwit does not matter here at all
final myCode = final myCode =
await (this as PaynymInterface).getPaymentCode(isSegwit: false); await (this as PaynymInterface).getPaymentCode(isSegwit: false);
@ -654,8 +658,10 @@ abstract class Wallet<T extends CryptoCurrency> {
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
if (this is MultiAddressInterface) { if (this is MultiAddressInterface) {
await (this as MultiAddressInterface) if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
.checkChangeAddressForTransactions(); await (this as MultiAddressInterface)
.checkChangeAddressForTransactions();
}
} }
_checkAlive(); _checkAlive();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
@ -681,7 +687,7 @@ abstract class Wallet<T extends CryptoCurrency> {
await fetchFuture; await fetchFuture;
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
if (this is PaynymInterface && codesToCheck.isNotEmpty) { if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) {
_checkAlive(); _checkAlive();
await (this as PaynymInterface) await (this as PaynymInterface)
.checkForNotificationTransactionsTo(codesToCheck); .checkForNotificationTransactionsTo(codesToCheck);

View file

@ -13,6 +13,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
import '../../../models/isar/models/isar_models.dart'; import '../../../models/isar/models/isar_models.dart';
import '../../../models/keys/view_only_wallet_data.dart';
import '../../../models/paymint/fee_object_model.dart'; import '../../../models/paymint/fee_object_model.dart';
import '../../../models/signing_data.dart'; import '../../../models/signing_data.dart';
import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount.dart';
@ -32,9 +33,10 @@ import '../intermediate/bip39_hd_wallet.dart';
import 'cpfp_interface.dart'; import 'cpfp_interface.dart';
import 'paynym_interface.dart'; import 'paynym_interface.dart';
import 'rbf_interface.dart'; import 'rbf_interface.dart';
import 'view_only_option_interface.dart';
mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
on Bip39HDWallet<T> { on Bip39HDWallet<T> implements ViewOnlyOptionInterface<T> {
late ElectrumXClient electrumXClient; late ElectrumXClient electrumXClient;
late CachedElectrumXClient electrumXCachedClient; late CachedElectrumXClient electrumXCachedClient;
@ -137,7 +139,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
(e.used != true) && (e.used != true) &&
(canCPFP || (canCPFP ||
e.isConfirmed( e.isConfirmed(
currentChainHeight, cryptoCurrency.minConfirms)), currentChainHeight,
cryptoCurrency.minConfirms,
)),
) )
.toList(); .toList();
final spendableSatoshiValue = final spendableSatoshiValue =
@ -944,7 +948,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
Future<({List<Address> addresses, int index})> checkGapsBatched( Future<({List<Address> addresses, int index})> checkGapsBatched(
int txCountBatchSize, int txCountBatchSize,
coinlib.HDPrivateKey root, coinlib.HDKey node,
DerivePathType type, DerivePathType type,
int chain, int chain,
) async { ) async {
@ -969,7 +973,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
index: index + j, index: index + j,
); );
final keys = root.derivePath(derivePath); final coinlib.HDKey keys;
if (isViewOnly) {
final idx = derivePath.lastIndexOf("'/");
final path = derivePath.substring(idx + 2);
keys = node.derivePath(path);
} else {
keys = node.derivePath(derivePath);
}
final addressData = cryptoCurrency.getAddressForPublicKey( final addressData = cryptoCurrency.getAddressForPublicKey(
publicKey: keys.publicKey, publicKey: keys.publicKey,
@ -986,7 +997,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
publicKey: keys.publicKey.data, publicKey: keys.publicKey.data,
type: addressData.addressType, type: addressData.addressType,
derivationIndex: index + j, derivationIndex: index + j,
derivationPath: DerivationPath()..value = derivePath, derivationPath:
isViewOnly ? null : (DerivationPath()..value = derivePath),
subType: subType:
chain == 0 ? AddressSubType.receiving : AddressSubType.change, chain == 0 ? AddressSubType.receiving : AddressSubType.change,
); );
@ -1025,13 +1037,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
} }
Future<({List<Address> addresses, int index})> checkGapsLinearly( Future<({List<Address> addresses, int index})> checkGapsLinearly(
coinlib.HDPrivateKey root, coinlib.HDKey node,
DerivePathType type, DerivePathType type,
int chain, int chain,
) async { ) async {
final List<Address> addressArray = []; final List<Address> addressArray = [];
int gapCounter = 0; int gapCounter = 0;
int index = 0; int index = 0;
for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) { for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) {
Logging.instance.log( Logging.instance.log(
"index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter", "index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter",
@ -1043,7 +1056,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
chain: chain, chain: chain,
index: index, index: index,
); );
final keys = root.derivePath(derivePath);
final coinlib.HDKey keys;
if (isViewOnly) {
final idx = derivePath.lastIndexOf("'/");
final path = derivePath.substring(idx + 2);
keys = node.derivePath(path);
} else {
keys = node.derivePath(derivePath);
}
final addressData = cryptoCurrency.getAddressForPublicKey( final addressData = cryptoCurrency.getAddressForPublicKey(
publicKey: keys.publicKey, publicKey: keys.publicKey,
derivePathType: type, derivePathType: type,
@ -1059,7 +1081,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
publicKey: keys.publicKey.data, publicKey: keys.publicKey.data,
type: addressData.addressType, type: addressData.addressType,
derivationIndex: index, derivationIndex: index,
derivationPath: DerivationPath()..value = derivePath, derivationPath:
isViewOnly ? null : (DerivationPath()..value = derivePath),
subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change, subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change,
); );
@ -1331,6 +1354,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
@override @override
Future<void> checkReceivingAddressForTransactions() async { Future<void> checkReceivingAddressForTransactions() async {
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) {
return;
}
if (info.otherData[WalletInfoKeys.reuseAddress] == true) { if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
try { try {
throw Exception(); throw Exception();
@ -1382,6 +1409,21 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
@override @override
Future<void> checkChangeAddressForTransactions() async { Future<void> checkChangeAddressForTransactions() async {
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) {
return;
}
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
try {
throw Exception();
} catch (_, s) {
Logging.instance.log(
"checkChangeAddressForTransactions called but reuse address flag set: $s",
level: LogLevel.Error,
);
}
}
try { try {
final currentChange = await getCurrentChangeAddress(); final currentChange = await getCurrentChangeAddress();
@ -1419,6 +1461,11 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
@override @override
Future<void> recover({required bool isRescan}) async { Future<void> recover({required bool isRescan}) async {
if (isViewOnly) {
await recoverViewOnly(isRescan: isRescan);
return;
}
final root = await getRootHDNode(); final root = await getRootHDNode();
final List<Future<({int index, List<Address> addresses})>> receiveFutures = final List<Future<({int index, List<Address> addresses})>> receiveFutures =
@ -1918,5 +1965,219 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
return address; return address;
} }
// ============== View only ==================================================
@override
Future<void> recoverViewOnly({bool isRescan = false}) async {
final data = await getViewOnlyWalletData();
final coinlib.HDKey? root;
if (data is AddressViewOnlyWalletData) {
root = null;
} else {
if ((data as ExtendedKeysViewOnlyWalletData).xPubs.length != 1) {
throw Exception(
"Only single xpub view only wallets are currently supported",
);
}
root = coinlib.HDPublicKey.decode(data.xPubs.first.encoded);
}
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
[];
final List<Future<({int index, List<Address> addresses})>> changeFutures =
[];
const receiveChain = 0;
const changeChain = 1;
const txCountBatchSize = 12;
try {
await refreshMutex.protect(() async {
if (isRescan) {
// clear cache
await electrumXCachedClient.clearSharedTransactionCache(
cryptoCurrency: info.coin,
);
// clear blockchain info
await mainDB.deleteWalletBlockchainData(walletId);
}
final List<Address> addressesToStore = [];
if (root != null) {
// receiving addresses
Logging.instance.log(
"checking receiving addresses...",
level: LogLevel.Info,
);
final canBatch = await serverCanBatch;
for (final type in cryptoCurrency.supportedDerivationPathTypes) {
final path = cryptoCurrency.constructDerivePath(
derivePathType: type,
chain: 0,
index: 0,
);
if (path.startsWith(
(data as ExtendedKeysViewOnlyWalletData).xPubs.first.path,
)) {
receiveFutures.add(
canBatch
? checkGapsBatched(
txCountBatchSize,
root,
type,
receiveChain,
)
: checkGapsLinearly(
root,
type,
receiveChain,
),
);
}
}
// change addresses
Logging.instance.log(
"checking change addresses...",
level: LogLevel.Info,
);
for (final type in cryptoCurrency.supportedDerivationPathTypes) {
final path = cryptoCurrency.constructDerivePath(
derivePathType: type,
chain: 0,
index: 0,
);
if (path.startsWith(
(data as ExtendedKeysViewOnlyWalletData).xPubs.first.path,
)) {
changeFutures.add(
canBatch
? checkGapsBatched(
txCountBatchSize,
root,
type,
changeChain,
)
: checkGapsLinearly(
root,
type,
changeChain,
),
);
}
}
// io limitations may require running these linearly instead
final futuresResult = await Future.wait([
Future.wait(receiveFutures),
Future.wait(changeFutures),
]);
final receiveResults = futuresResult[0];
final changeResults = futuresResult[1];
int highestReceivingIndexWithHistory = 0;
for (final tuple in receiveResults) {
if (tuple.addresses.isEmpty) {
await checkReceivingAddressForTransactions();
} else {
highestReceivingIndexWithHistory = max(
tuple.index,
highestReceivingIndexWithHistory,
);
addressesToStore.addAll(tuple.addresses);
}
}
int highestChangeIndexWithHistory = 0;
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
for (final tuple in changeResults) {
if (tuple.addresses.isEmpty) {
await checkChangeAddressForTransactions();
} else {
highestChangeIndexWithHistory = max(
tuple.index,
highestChangeIndexWithHistory,
);
addressesToStore.addAll(tuple.addresses);
}
}
// remove extra addresses to help minimize risk of creating a large gap
addressesToStore.removeWhere(
(e) =>
e.subType == AddressSubType.change &&
e.derivationIndex > highestChangeIndexWithHistory,
);
addressesToStore.removeWhere(
(e) =>
e.subType == AddressSubType.receiving &&
e.derivationIndex > highestReceivingIndexWithHistory,
);
} else {
final clAddress = coinlib.Address.fromString(
(data as AddressViewOnlyWalletData).address,
cryptoCurrency.networkParams,
);
final AddressType addressType;
switch (clAddress.runtimeType) {
case const (coinlib.P2PKHAddress):
addressType = AddressType.p2pkh;
break;
case const (coinlib.P2SHAddress):
addressType = AddressType.p2sh;
break;
case const (coinlib.P2WPKHAddress):
addressType = AddressType.p2wpkh;
break;
case const (coinlib.P2TRAddress):
addressType = AddressType.p2tr;
break;
default:
throw Exception(
"Unsupported address type: ${clAddress.runtimeType}",
);
}
addressesToStore.add(
Address(
walletId: walletId,
value: clAddress.toString(),
publicKey: [],
derivationIndex: -1,
derivationPath: null,
type: addressType,
subType: AddressSubType.receiving,
),
);
}
await mainDB.updateOrPutAddresses(addressesToStore);
});
unawaited(refresh());
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from electrumx_mixin recoverViewOnly(): $e\n$s",
level: LogLevel.Info,
);
rethrow;
}
}
// =========================================================================== // ===========================================================================
} }

View file

@ -2,35 +2,35 @@ import '../../../models/keys/xpriv_data.dart';
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import 'electrumx_interface.dart'; import 'electrumx_interface.dart';
typedef XPub = ({String path, String xpub}); abstract class XKey {
typedef XPriv = ({String path, String xpriv}); XKey({required this.path});
final String path;
}
class XPub extends XKey {
XPub({required super.path, required this.encoded});
final String encoded;
}
class XPriv extends XKey {
XPriv({required super.path, required this.encoded});
final String encoded;
}
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface> mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
on ElectrumXInterface<T> { on ElectrumXInterface<T> {
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async { Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
final paths = cryptoCurrency.supportedDerivationPathTypes.map( final paths = cryptoCurrency.supportedHardenedDerivationPaths;
(e) => (
path: e,
addressType: e.getAddressType(),
),
);
final master = await getRootHDNode(); final master = await getRootHDNode();
final fingerprint = master.fingerprint.toRadixString(16); final fingerprint = master.fingerprint.toRadixString(16);
final futures = paths.map((e) async { final futures = paths.map((path) async {
String path = cryptoCurrency.constructDerivePath(
derivePathType: e.path,
chain: 0,
index: 0,
);
// trim chain and address index
path = path.substring(0, path.lastIndexOf("'") + 1);
final node = master.derivePath(path); final node = master.derivePath(path);
return ( return XPub(
path: path, path: path,
xpub: node.hdPublicKey.encode( encoded: node.hdPublicKey.encode(
cryptoCurrency.networkParams.pubHDPrefix, cryptoCurrency.networkParams.pubHDPrefix,
// 0x04b24746, // 0x04b24746,
), ),
@ -44,29 +44,17 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
} }
Future<XPrivData> getXPrivs() async { Future<XPrivData> getXPrivs() async {
final paths = cryptoCurrency.supportedDerivationPathTypes.map( final paths = cryptoCurrency.supportedHardenedDerivationPaths;
(e) => (
path: e,
addressType: e.getAddressType(),
),
);
final master = await getRootHDNode(); final master = await getRootHDNode();
final fingerprint = master.fingerprint.toRadixString(16); final fingerprint = master.fingerprint.toRadixString(16);
final futures = paths.map((e) async { final futures = paths.map((path) async {
String path = cryptoCurrency.constructDerivePath(
derivePathType: e.path,
chain: 0,
index: 0,
);
// trim chain and address index
path = path.substring(0, path.lastIndexOf("'") + 1);
final node = master.derivePath(path); final node = master.derivePath(path);
return ( return XPriv(
path: path, path: path,
xpriv: node.encode( encoded: node.encode(
cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.privHDPrefix,
), ),
); );
@ -76,9 +64,9 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
walletId: walletId, walletId: walletId,
fingerprint: fingerprint, fingerprint: fingerprint,
xprivs: [ xprivs: [
( XPriv(
path: "Master", path: "Master",
xpriv: master.encode( encoded: master.encode(
cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.privHDPrefix,
), ),
), ),

View file

@ -1,34 +1,27 @@
import 'dart:convert'; import '../../../models/keys/view_only_wallet_data.dart';
import '../../crypto_currency/interfaces/view_only_option_currency_interface.dart'; import '../../crypto_currency/interfaces/view_only_option_currency_interface.dart';
import '../wallet.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> mixin ViewOnlyOptionInterface<T extends ViewOnlyOptionCurrencyInterface>
on Wallet<T> { on Wallet<T> {
bool get isViewOnly; ViewOnlyWalletType get viewOnlyType => info.viewOnlyWalletType!;
bool get isViewOnly => info.isViewOnly;
Future<void> recoverViewOnly(); Future<void> recoverViewOnly();
Future<ViewOnlyWalletData> getViewOnlyWalletData(); Future<ViewOnlyWalletData> getViewOnlyWalletData() async {
if (!isViewOnly) {
throw Exception("This is not a view only wallet");
}
final encoded = await secureStorageInterface.read(
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
);
return ViewOnlyWalletData.fromJsonEncodedString(
encoded!,
walletId: walletId,
);
}
} }

View file

@ -94,7 +94,7 @@ class StackDialog extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Flexible( Flexible(
child: Text( child: SelectableText(
title, title,
style: STextStyles.pageTitleH2(context), style: STextStyles.pageTitleH2(context),
), ),
@ -110,7 +110,7 @@ class StackDialog extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( SelectableText(
message!, message!,
style: STextStyles.smallMed14(context), style: STextStyles.smallMed14(context),
), ),