mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-22 11:29:23 +00:00
Add support for view only wallets to most coins
This commit is contained in:
parent
3da57bc150
commit
6ff539e71b
38 changed files with 1665 additions and 556 deletions
164
lib/models/keys/view_only_wallet_data.dart
Normal file
164
lib/models/keys/view_only_wallet_data.dart
Normal 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,
|
||||
},
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
|
@ -25,6 +25,7 @@ import '../../../utilities/name_generator.dart';
|
|||
import '../../../utilities/text_styles.dart';
|
||||
import '../../../utilities/util.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/isar/models/wallet_info.dart';
|
||||
import '../../../widgets/background.dart';
|
||||
|
@ -104,7 +105,9 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
|||
case AddWalletType.New:
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
coin.hasMnemonicPassphraseSupport
|
||||
coin.possibleMnemonicLengths.length > 1 ||
|
||||
coin.hasMnemonicPassphraseSupport ||
|
||||
coin is ViewOnlyOptionCurrencyInterface
|
||||
? NewWalletOptionsView.routeName
|
||||
: NewWalletRecoveryPhraseWarningView.routeName,
|
||||
arguments: Tuple2(
|
||||
|
|
|
@ -12,9 +12,11 @@ import '../../../utilities/constants.dart';
|
|||
import '../../../utilities/text_styles.dart';
|
||||
import '../../../utilities/util.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/conditional_parent.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_scaffold.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/sub_widgets/mnemonic_word_count_select_sheet.dart';
|
||||
|
||||
final pNewWalletOptions =
|
||||
StateProvider<({String mnemonicPassphrase, int mnemonicWordsCount})?>(
|
||||
final pNewWalletOptions = StateProvider<
|
||||
({
|
||||
String mnemonicPassphrase,
|
||||
int mnemonicWordsCount,
|
||||
bool convertToViewOnly,
|
||||
})?>(
|
||||
(ref) => null,
|
||||
);
|
||||
|
||||
|
@ -59,6 +65,8 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
|||
bool hidePassword = true;
|
||||
NewWalletOptions _selectedOptions = NewWalletOptions.Default;
|
||||
|
||||
bool _convertToViewOnly = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
passwordController = TextEditingController();
|
||||
|
@ -210,7 +218,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
|||
if (_selectedOptions == NewWalletOptions.Advanced)
|
||||
Column(
|
||||
children: [
|
||||
if (Util.isDesktop)
|
||||
if (Util.isDesktop && lengths.length > 1)
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
value: ref
|
||||
|
@ -265,7 +273,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
if (!Util.isDesktop && lengths.length > 1)
|
||||
MobileMnemonicLengthSelector(
|
||||
chooseMnemonicLength: () {
|
||||
showModalBottomSheet<dynamic>(
|
||||
|
@ -284,91 +292,109 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"You may add a BIP39 passphrase. This is optional. "
|
||||
"You will need BOTH your seed and your passphrase to recover the wallet.",
|
||||
style: Util.isDesktop
|
||||
? STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
)
|
||||
: STextStyles.itemSubtitle(context),
|
||||
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||
RoundedWhiteContainer(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"You may add a BIP39 passphrase. This is optional. "
|
||||
"You will need BOTH your seed and your passphrase to recover the wallet.",
|
||||
style: Util.isDesktop
|
||||
? STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
)
|
||||
: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("mnemonicPassphraseFieldKey1"),
|
||||
focusNode: passwordFocusNode,
|
||||
controller: passwordController,
|
||||
style: Util.isDesktop
|
||||
? STextStyles.desktopTextMedium(context).copyWith(
|
||||
height: 2,
|
||||
)
|
||||
: STextStyles.field(context),
|
||||
obscureText: hidePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: standardInputDecoration(
|
||||
"BIP39 passphrase",
|
||||
passwordFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
suffixIcon: UnconstrainedBox(
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => SizedBox(
|
||||
height: 70,
|
||||
child: child,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Util.isDesktop ? 24 : 16,
|
||||
),
|
||||
GestureDetector(
|
||||
key: const Key(
|
||||
"mnemonicPassphraseFieldShowPasswordButtonKey",
|
||||
),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
hidePassword = !hidePassword;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
hidePassword
|
||||
? Assets.svg.eye
|
||||
: Assets.svg.eyeSlash,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark3,
|
||||
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("mnemonicPassphraseFieldKey1"),
|
||||
focusNode: passwordFocusNode,
|
||||
controller: passwordController,
|
||||
style: Util.isDesktop
|
||||
? STextStyles.desktopTextMedium(context).copyWith(
|
||||
height: 2,
|
||||
)
|
||||
: STextStyles.field(context),
|
||||
obscureText: hidePassword,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: standardInputDecoration(
|
||||
"BIP39 passphrase",
|
||||
passwordFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
suffixIcon: UnconstrainedBox(
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => SizedBox(
|
||||
height: 70,
|
||||
child: child,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: Util.isDesktop ? 24 : 16,
|
||||
height: Util.isDesktop ? 24 : 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
],
|
||||
GestureDetector(
|
||||
key: const Key(
|
||||
"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(),
|
||||
|
@ -383,6 +409,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
|||
mnemonicWordsCount:
|
||||
ref.read(mnemonicWordCountStateProvider.state).state,
|
||||
mnemonicPassphrase: passwordController.text,
|
||||
convertToViewOnly: _convertToViewOnly,
|
||||
);
|
||||
} else {
|
||||
ref.read(pNewWalletOptions.notifier).state = null;
|
||||
|
|
|
@ -582,7 +582,12 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
)
|
||||
.state!
|
||||
.mnemonicPassphrase;
|
||||
} else {}
|
||||
} else {
|
||||
// this may not be epiccash specific?
|
||||
if (coin is Epiccash) {
|
||||
mnemonicPassphrase = "";
|
||||
}
|
||||
}
|
||||
|
||||
wordCount = ref
|
||||
.read(
|
||||
|
|
|
@ -24,6 +24,7 @@ import '../../../../utilities/text_styles.dart';
|
|||
import '../../../../utilities/util.dart';
|
||||
import '../../../../wallets/crypto_currency/crypto_currency.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/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/checkbox_text_button.dart';
|
||||
|
@ -656,7 +657,7 @@ class ViewOnlyRestoreOption extends StatefulWidget {
|
|||
class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final showDateOption = widget.coin is ViewOnlyOptionCurrencyInterface;
|
||||
final showDateOption = widget.coin is CryptonoteCurrency;
|
||||
return Column(
|
||||
children: [
|
||||
if (showDateOption)
|
||||
|
|
|
@ -4,32 +4,41 @@ import 'dart:io';
|
|||
|
||||
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
|
||||
as cs_monero_deprecated;
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.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/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import '../../../providers/db/main_db_provider.dart';
|
||||
import '../../../providers/global/secure_store_provider.dart';
|
||||
import '../../../providers/providers.dart';
|
||||
import '../../../themes/stack_colors.dart';
|
||||
import '../../../utilities/assets.dart';
|
||||
import '../../../utilities/barcode_scanner_interface.dart';
|
||||
import '../../../utilities/clipboard_interface.dart';
|
||||
import '../../../utilities/constants.dart';
|
||||
import '../../../utilities/text_styles.dart';
|
||||
import '../../../utilities/util.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/wallet/impl/epiccash_wallet.dart';
|
||||
import '../../../wallets/wallet/impl/monero_wallet.dart';
|
||||
import '../../../wallets/wallet/impl/wownero_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/desktop/desktop_app_bar.dart';
|
||||
import '../../../widgets/desktop/desktop_scaffold.dart';
|
||||
import '../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../widgets/stack_text_field.dart';
|
||||
import '../../../widgets/toggle.dart';
|
||||
import '../../home_view/home_view.dart';
|
||||
import 'confirm_recovery_dialog.dart';
|
||||
import 'sub_widgets/restore_failed_dialog.dart';
|
||||
|
@ -66,7 +75,10 @@ class _RestoreViewOnlyWalletViewState
|
|||
late final TextEditingController addressController;
|
||||
late final TextEditingController viewKeyController;
|
||||
|
||||
late String _currentDropDownValue;
|
||||
|
||||
bool _enableRestoreButton = false;
|
||||
bool _addressOnly = false;
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
|
@ -106,30 +118,43 @@ class _RestoreViewOnlyWalletViewState
|
|||
WalletInfoKeys.isViewOnlyKey: true,
|
||||
};
|
||||
|
||||
if (widget.restoreFromDate != null) {
|
||||
if (widget.coin is Monero) {
|
||||
height = cs_monero_deprecated.getMoneroHeightByDate(
|
||||
date: widget.restoreFromDate!,
|
||||
final ViewOnlyWalletType viewOnlyWalletType;
|
||||
if (widget.coin is Bip39HDCurrency) {
|
||||
if (widget.coin is Firo) {
|
||||
otherDataJson.addAll(
|
||||
{
|
||||
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
||||
WalletInfoKeys.enableLelantusScanning:
|
||||
widget.enableLelantusScanning,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (widget.coin is Wownero) {
|
||||
height = cs_monero_deprecated.getWowneroHeightByDate(
|
||||
date: widget.restoreFromDate!,
|
||||
);
|
||||
viewOnlyWalletType = _addressOnly
|
||||
? ViewOnlyWalletType.addressOnly
|
||||
: 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) {
|
||||
otherDataJson.addAll(
|
||||
{
|
||||
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
||||
WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning,
|
||||
},
|
||||
viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
|
||||
} else {
|
||||
throw Exception(
|
||||
"Unsupported view only wallet currency type found: ${widget.coin.runtimeType}",
|
||||
);
|
||||
}
|
||||
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] =
|
||||
viewOnlyWalletType.index;
|
||||
|
||||
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
|
||||
.read(nodeServiceChangeNotifierProvider)
|
||||
.getPrimaryNodeFor(currency: widget.coin);
|
||||
|
@ -185,10 +247,7 @@ class _RestoreViewOnlyWalletViewState
|
|||
secureStorageInterface: ref.read(secureStoreProvider),
|
||||
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
viewOnlyData: ViewOnlyWalletData(
|
||||
address: addressController.text,
|
||||
privateViewKey: viewKeyController.text,
|
||||
),
|
||||
viewOnlyData: viewOnlyData,
|
||||
);
|
||||
|
||||
// TODO: extract interface with isRestore param
|
||||
|
@ -278,11 +337,27 @@ class _RestoreViewOnlyWalletViewState
|
|||
super.initState();
|
||||
addressController = 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
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = Util.isDesktop;
|
||||
|
||||
final isElectrumX = widget.coin is ElectrumXCurrencyInterface;
|
||||
|
||||
return MasterScaffold(
|
||||
isDesktop: isDesktop,
|
||||
appBar: isDesktop
|
||||
|
@ -339,32 +414,156 @@ class _RestoreViewOnlyWalletViewState
|
|||
? STextStyles.desktopH2(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(
|
||||
height: isDesktop ? 24 : 16,
|
||||
),
|
||||
FullTextField(
|
||||
label: "Address",
|
||||
controller: addressController,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_enableRestoreButton = newValue.isNotEmpty &&
|
||||
viewKeyController.text.isNotEmpty;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 16 : 12,
|
||||
),
|
||||
FullTextField(
|
||||
label: "View Key",
|
||||
controller: viewKeyController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_enableRestoreButton = value.isNotEmpty &&
|
||||
addressController.text.isNotEmpty;
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!isElectrumX || _addressOnly)
|
||||
FullTextField(
|
||||
key: const Key("viewOnlyAddressRestoreFieldKey"),
|
||||
label: "Address",
|
||||
controller: addressController,
|
||||
onChanged: (newValue) {
|
||||
if (isElectrumX) {
|
||||
viewKeyController.text = "";
|
||||
setState(() {
|
||||
_enableRestoreButton = newValue.isNotEmpty;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_enableRestoreButton = newValue.isNotEmpty &&
|
||||
viewKeyController.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(),
|
||||
SizedBox(
|
||||
height: isDesktop ? 24 : 16,
|
||||
|
|
|
@ -9,13 +9,17 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
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/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../notifications/show_flush_bar.dart';
|
||||
import '../../../pages_desktop_specific/desktop_home_view.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 '../../../utilities/assets.dart';
|
||||
import '../../../utilities/constants.dart';
|
||||
import '../../../utilities/logger.dart';
|
||||
import '../../../utilities/show_loading.dart';
|
||||
import '../../../utilities/text_styles.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/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_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/desktop/desktop_app_bar.dart';
|
||||
import '../../../widgets/desktop/desktop_scaffold.dart';
|
||||
import '../../../widgets/stack_dialog.dart';
|
||||
import '../../home_view/home_view.dart';
|
||||
import '../add_token_view/edit_wallet_tokens_view.dart';
|
||||
import '../new_wallet_options/new_wallet_options_view.dart';
|
||||
|
@ -64,46 +79,25 @@ class _VerifyRecoveryPhraseViewState
|
|||
extends ConsumerState<VerifyRecoveryPhraseView>
|
||||
// with WidgetsBindingObserver
|
||||
{
|
||||
late Wallet _wallet;
|
||||
late String _walletId;
|
||||
late CryptoCurrency _coin;
|
||||
late List<String> _mnemonic;
|
||||
late final bool isDesktop;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_wallet = widget.wallet;
|
||||
_walletId = widget.wallet.walletId;
|
||||
_coin = widget.wallet.cryptoCurrency;
|
||||
_mnemonic = widget.mnemonic;
|
||||
isDesktop = Util.isDesktop;
|
||||
// WidgetsBinding.instance?.addObserver(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
// WidgetsBinding.instance?.removeObserver(this);
|
||||
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 {
|
||||
final result = await showDialog<String?>(
|
||||
context: context,
|
||||
|
@ -113,6 +107,153 @@ class _VerifyRecoveryPhraseViewState
|
|||
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 {
|
||||
if (isMatch) {
|
||||
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,
|
||||
);
|
||||
|
||||
ref.read(pWallets).addWallet(_wallet);
|
||||
ref.read(pWallets).addWallet(widget.wallet);
|
||||
|
||||
final isCreateSpecialEthWallet =
|
||||
ref.read(createSpecialEthWalletRoutingFlag);
|
||||
|
@ -142,6 +283,51 @@ class _VerifyRecoveryPhraseViewState
|
|||
.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 (isDesktop) {
|
||||
if (isCreateSpecialEthWallet) {
|
||||
|
@ -156,7 +342,7 @@ class _VerifyRecoveryPhraseViewState
|
|||
DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
if (widget.wallet.info.coin is Ethereum) {
|
||||
if (_coin is Ethereum) {
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
EditWalletTokensView.routeName,
|
||||
|
@ -179,7 +365,7 @@ class _VerifyRecoveryPhraseViewState
|
|||
(route) => false,
|
||||
),
|
||||
);
|
||||
if (widget.wallet.info.coin is Ethereum) {
|
||||
if (_coin is Ethereum) {
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
EditWalletTokensView.routeName,
|
||||
|
@ -269,7 +455,7 @@ class _VerifyRecoveryPhraseViewState
|
|||
|
||||
Future<void> delete() async {
|
||||
await ref.read(pWallets).deleteWallet(
|
||||
_wallet.info,
|
||||
widget.wallet.info,
|
||||
ref.read(secureStoreProvider),
|
||||
);
|
||||
}
|
||||
|
@ -299,7 +485,7 @@ class _VerifyRecoveryPhraseViewState
|
|||
trailing: ExitToMyStackButton(
|
||||
onPressed: () async {
|
||||
await delete();
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(DesktopHomeView.routeName),
|
||||
);
|
||||
|
|
|
@ -148,6 +148,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final coin = ref.watch(pWalletCoin(widget.walletId));
|
||||
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => Background(
|
||||
|
@ -383,13 +384,11 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
|||
detail: address.zSafeFrost.toString(),
|
||||
button: Container(),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is Bip39HDWallet)
|
||||
if (wallet is Bip39HDWallet && !wallet.isViewOnly)
|
||||
const _Div(
|
||||
height: 12,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is Bip39HDWallet)
|
||||
if (wallet is Bip39HDWallet && !wallet.isViewOnly)
|
||||
AddressPrivateKey(
|
||||
walletId: widget.walletId,
|
||||
address: address,
|
||||
|
|
|
@ -18,6 +18,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../models/isar/models/isar_models.dart';
|
||||
import '../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../notifications/show_flush_bar.dart';
|
||||
import '../../providers/db/main_db_provider.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/multi_address_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/conditional_parent.dart';
|
||||
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
|
@ -189,6 +191,16 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
clipboard = widget.clipboard;
|
||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||
_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 ||
|
||||
(wallet is! BCashInterface &&
|
||||
wallet is Bip39HDWallet &&
|
||||
|
@ -265,6 +277,18 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
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(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -553,17 +577,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
if (ref.watch(
|
||||
pWallets.select((value) => value.getWallet(walletId)),
|
||||
) is MultiAddressInterface ||
|
||||
_supportsSpark)
|
||||
if (canGen)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (ref.watch(
|
||||
pWallets.select((value) => value.getWallet(walletId)),
|
||||
) is MultiAddressInterface ||
|
||||
_supportsSpark)
|
||||
if (canGen)
|
||||
SecondaryButton(
|
||||
label: "Generate new address",
|
||||
onPressed: _supportsSpark &&
|
||||
|
|
|
@ -27,6 +27,7 @@ import '../../../../../models/exchange/change_now/exchange_transaction.dart';
|
|||
import '../../../../../models/exchange/response_objects/trade.dart';
|
||||
import '../../../../../models/isar/models/contact_entry.dart';
|
||||
import '../../../../../models/isar/models/transaction_note.dart';
|
||||
import '../../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../../models/node_model.dart';
|
||||
import '../../../../../models/stack_restoring_ui_state.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_mixin_interfaces/mnemonic_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 {
|
||||
final Set<String> walletIds;
|
||||
|
@ -312,7 +314,10 @@ abstract class SWB {
|
|||
backupWallet['isFavorite'] = wallet.info.isFavourite;
|
||||
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['mnemonicPassphrase'] =
|
||||
await wallet.getMnemonicPassphrase();
|
||||
|
@ -419,7 +424,16 @@ abstract class SWB {
|
|||
|
||||
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
|
||||
if (walletbackup['privateKey'] != null) {
|
||||
privateKey = walletbackup['privateKey'] as String;
|
||||
|
@ -486,6 +500,7 @@ abstract class SWB {
|
|||
mnemonic: mnemonic,
|
||||
mnemonicPassphrase: mnemonicPassphrase,
|
||||
privateKey: privateKey,
|
||||
viewOnlyData: viewOnlyData,
|
||||
);
|
||||
|
||||
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import '../../../../app_config.dart';
|
||||
import '../../../../models/keys/cw_key_data.dart';
|
||||
import '../../../../models/keys/key_data_interface.dart';
|
||||
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../models/keys/xpriv_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
|
@ -39,6 +40,7 @@ import '../../../../widgets/rounded_white_container.dart';
|
|||
import '../../../../widgets/stack_dialog.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 '../../sub_widgets/view_only_wallet_data_widget.dart';
|
||||
import 'cn_wallet_keys.dart';
|
||||
import 'wallet_xprivs.dart';
|
||||
|
||||
|
@ -426,7 +428,7 @@ class _FrostKeys extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class MobileKeyDataView extends StatelessWidget {
|
||||
class MobileKeyDataView extends ConsumerWidget {
|
||||
const MobileKeyDataView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
|
@ -441,7 +443,7 @@ class MobileKeyDataView extends StatelessWidget {
|
|||
final KeyDataInterface keyData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -455,6 +457,7 @@ class MobileKeyDataView extends StatelessWidget {
|
|||
"Wallet ${switch (keyData.runtimeType) {
|
||||
const (XPrivData) => "xpriv(s)",
|
||||
const (CWKeyData) => "keys",
|
||||
const (ViewOnlyWalletData) => "keys",
|
||||
_ => throw UnimplementedError(
|
||||
"Don't forget to add your KeyDataInterface here!",
|
||||
),
|
||||
|
@ -483,6 +486,10 @@ class MobileKeyDataView extends StatelessWidget {
|
|||
walletId: walletId,
|
||||
cwKeyData: keyData as CWKeyData,
|
||||
),
|
||||
const (ViewOnlyWalletData) =>
|
||||
ViewOnlyWalletDataWidget(
|
||||
data: keyData as ViewOnlyWalletData,
|
||||
),
|
||||
_ => throw UnimplementedError(
|
||||
"Don't forget to add your KeyDataInterface here!",
|
||||
),
|
||||
|
|
|
@ -49,7 +49,7 @@ class WalletXPrivsState extends ConsumerState<WalletXPrivs> {
|
|||
late String _currentDropDownValue;
|
||||
|
||||
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 {
|
||||
await widget.clipboardInterface.setData(
|
||||
|
|
|
@ -19,6 +19,7 @@ import '../../../db/hive/db.dart';
|
|||
import '../../../db/sqlite/firo_cache.dart';
|
||||
import '../../../models/epicbox_config_model.dart';
|
||||
import '../../../models/keys/key_data_interface.dart';
|
||||
import '../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../notifications/show_flush_bar.dart';
|
||||
import '../../../providers/global/wallets_provider.dart';
|
||||
import '../../../providers/ui/transaction_filter_provider.dart';
|
||||
|
@ -99,8 +100,13 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
void initState() {
|
||||
walletId = widget.walletId;
|
||||
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 = "";
|
||||
|
||||
|
@ -166,6 +172,15 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -248,146 +263,154 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return SettingsListButton(
|
||||
iconAssetName: Assets.svg.lock,
|
||||
iconSize: 16,
|
||||
title: "Wallet backup",
|
||||
onPressed: () async {
|
||||
final wallet = ref
|
||||
.read(pWallets)
|
||||
.getWallet(widget.walletId);
|
||||
if (canBackup)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (canBackup)
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return SettingsListButton(
|
||||
iconAssetName: Assets.svg.lock,
|
||||
iconSize: 16,
|
||||
title: "Wallet backup",
|
||||
onPressed: () async {
|
||||
// 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;
|
||||
({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
List<String>? mnemonic;
|
||||
({
|
||||
String myName,
|
||||
String config,
|
||||
String keys
|
||||
})? prevGen,
|
||||
})? frostWalletData;
|
||||
ViewOnlyWalletData? voData;
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
final futures = [
|
||||
wallet.getSerializedKeys(),
|
||||
wallet.getMultisigConfig(),
|
||||
wallet.getSerializedKeysPrevGen(),
|
||||
wallet.getMultisigConfigPrevGen(),
|
||||
];
|
||||
String keys,
|
||||
({
|
||||
String config,
|
||||
String keys
|
||||
})? prevGen,
|
||||
})? frostWalletData;
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
final futures = [
|
||||
wallet.getSerializedKeys(),
|
||||
wallet.getMultisigConfig(),
|
||||
wallet.getSerializedKeysPrevGen(),
|
||||
wallet.getMultisigConfigPrevGen(),
|
||||
];
|
||||
|
||||
final results =
|
||||
await Future.wait(futures);
|
||||
final results =
|
||||
await Future.wait(futures);
|
||||
|
||||
if (results.length == 4) {
|
||||
frostWalletData = (
|
||||
myName: wallet.frostInfo.myName,
|
||||
config: results[1]!,
|
||||
keys: results[0]!,
|
||||
prevGen: results[2] == null ||
|
||||
results[3] == null
|
||||
? null
|
||||
: (
|
||||
config: results[3]!,
|
||||
keys: results[2]!,
|
||||
),
|
||||
);
|
||||
if (results.length == 4) {
|
||||
frostWalletData = (
|
||||
myName: wallet.frostInfo.myName,
|
||||
config: results[1]!,
|
||||
keys: results[0]!,
|
||||
prevGen: results[2] == null ||
|
||||
results[3] == null
|
||||
? null
|
||||
: (
|
||||
config: results[3]!,
|
||||
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
|
||||
is ViewOnlyOptionInterface &&
|
||||
wallet.isViewOnly) {
|
||||
voData = await wallet
|
||||
keyData = await wallet
|
||||
.getViewOnlyWalletData();
|
||||
} else if (wallet
|
||||
is MnemonicInterface) {
|
||||
mnemonic = await wallet
|
||||
.getMnemonicAsWords();
|
||||
is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet
|
||||
is LibMoneroWallet) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
}
|
||||
|
||||
KeyDataInterface? keyData;
|
||||
if (wallet is ExtendedKeysInterface) {
|
||||
keyData = await wallet.getXPrivs();
|
||||
} else if (wallet is LibMoneroWallet) {
|
||||
keyData = await wallet.getKeys();
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
if (voData != null) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute:
|
||||
RouteGenerator
|
||||
.useMaterialPageRoute,
|
||||
builder: (_) => LockscreenView(
|
||||
routeOnSuccessArguments: (
|
||||
walletId: walletId,
|
||||
keyData: keyData,
|
||||
if (context.mounted) {
|
||||
if (keyData != null &&
|
||||
wallet
|
||||
is ViewOnlyOptionInterface) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute:
|
||||
RouteGenerator
|
||||
.useMaterialPageRoute,
|
||||
builder: (_) =>
|
||||
LockscreenView(
|
||||
routeOnSuccessArguments: (
|
||||
walletId: walletId,
|
||||
keyData: keyData,
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
MobileKeyDataView
|
||||
.routeName,
|
||||
biometricsCancelButtonString:
|
||||
"CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to view recovery data",
|
||||
biometricsAuthenticationTitle:
|
||||
"View recovery data",
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
MobileKeyDataView
|
||||
.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,
|
||||
settings: const RouteSettings(
|
||||
name:
|
||||
"/viewRecoveryDataLockscreen",
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
WalletBackupView
|
||||
.routeName,
|
||||
biometricsCancelButtonString:
|
||||
"CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to view recovery phrase",
|
||||
biometricsAuthenticationTitle:
|
||||
"View recovery phrase",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name:
|
||||
"/viewRecoverPhraseLockscreen",
|
||||
);
|
||||
} 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:
|
||||
"/viewRecoverPhraseLockscreen",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.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 '../../../../providers/global/secure_store_provider.dart';
|
||||
import '../../../../providers/global/wallets_provider.dart';
|
||||
|
@ -10,16 +11,13 @@ import '../../../../themes/stack_colors.dart';
|
|||
import '../../../../utilities/text_styles.dart';
|
||||
import '../../../../utilities/util.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/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
|
||||
import '../../../../widgets/desktop/primary_button.dart';
|
||||
import '../../../../widgets/detail_item.dart';
|
||||
import '../../../../widgets/rounded_white_container.dart';
|
||||
import '../../../../widgets/stack_dialog.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 {
|
||||
const DeleteViewOnlyWalletKeysView({
|
||||
|
@ -161,34 +159,9 @@ class _DeleteViewOnlyWalletKeysViewState
|
|||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (widget.data.address != null)
|
||||
DetailItem(
|
||||
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!,
|
||||
),
|
||||
),
|
||||
ViewOnlyWalletDataWidget(
|
||||
data: widget.data,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../../app_config.dart';
|
||||
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../providers/providers.dart';
|
||||
import '../../../../themes/stack_colors.dart';
|
||||
import '../../../../utilities/text_styles.dart';
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import 'package:flutter/material.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/providers.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/rbf_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/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
||||
|
@ -133,6 +135,12 @@ class _WalletSettingsWalletSettingsViewState
|
|||
|
||||
@override
|
||||
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(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -189,13 +197,11 @@ class _WalletSettingsWalletSettingsViewState
|
|||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is RbfInterface)
|
||||
if (wallet is RbfInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is RbfInterface)
|
||||
if (wallet is RbfInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
|
@ -227,13 +233,11 @@ class _WalletSettingsWalletSettingsViewState
|
|||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is MultiAddressInterface)
|
||||
if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is MultiAddressInterface)
|
||||
if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
|
@ -278,13 +282,11 @@ class _WalletSettingsWalletSettingsViewState
|
|||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is LelantusInterface)
|
||||
if (wallet is LelantusInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is LelantusInterface)
|
||||
if (wallet is LelantusInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
|
@ -316,13 +318,11 @@ class _WalletSettingsWalletSettingsViewState
|
|||
),
|
||||
),
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is SparkInterface)
|
||||
if (wallet is SparkInterface)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
||||
is SparkInterface)
|
||||
if (wallet is SparkInterface)
|
||||
RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
|
|
|
@ -57,7 +57,7 @@ class XPubViewState extends ConsumerState<XPubView> {
|
|||
late String _currentDropDownValue;
|
||||
|
||||
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 {
|
||||
await widget.clipboardInterface.setData(
|
||||
|
|
|
@ -186,6 +186,21 @@ class WalletSummaryInfo extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
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(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: SelectableText(
|
||||
|
|
|
@ -21,6 +21,7 @@ import 'package:isar/isar.dart';
|
|||
import '../../../db/sqlite/firo_cache.dart';
|
||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.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/token_view/my_tokens_view.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/wallet/impl/banano_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/blue_text_button.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;
|
||||
|
||||
// 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(
|
||||
appBar: DesktopAppBar(
|
||||
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)
|
||||
Column(
|
||||
|
@ -312,12 +332,14 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
walletId: widget.walletId,
|
||||
eventBus: eventBus,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
WalletKeysButton(
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
if (showKeysButton)
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
if (showKeysButton)
|
||||
WalletKeysButton(
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
|
|
|
@ -19,6 +19,7 @@ import 'package:isar/isar.dart';
|
|||
import 'package:tuple/tuple.dart';
|
||||
|
||||
import '../../../../models/isar/models/isar_models.dart';
|
||||
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.dart';
|
||||
import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.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/multi_address_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/custom_buttons/app_bar_icon_button.dart';
|
||||
import '../../../../widgets/custom_loading_overlay.dart';
|
||||
|
@ -185,10 +187,15 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
clipboard = widget.clipboard;
|
||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
|
||||
showMultiType = supportsSpark ||
|
||||
(wallet is! BCashInterface &&
|
||||
wallet is Bip39HDWallet &&
|
||||
wallet.supportedAddressTypes.length > 1);
|
||||
|
||||
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||
showMultiType = false;
|
||||
} else {
|
||||
showMultiType = supportsSpark ||
|
||||
(wallet is! BCashInterface &&
|
||||
wallet is Bip39HDWallet &&
|
||||
wallet.supportedAddressTypes.length > 1);
|
||||
}
|
||||
|
||||
_walletAddressTypes.add(wallet.info.mainAddressType);
|
||||
|
||||
|
@ -259,6 +266,18 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
@ -430,16 +449,12 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
),
|
||||
),
|
||||
|
||||
if (ref.watch(pWallets.select((value) => value.getWallet(walletId)))
|
||||
is MultiAddressInterface ||
|
||||
supportsSpark)
|
||||
if (canGen)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
|
||||
if (ref.watch(pWallets.select((value) => value.getWallet(walletId)))
|
||||
is MultiAddressInterface ||
|
||||
supportsSpark)
|
||||
if (canGen)
|
||||
SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: supportsSpark &&
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
|||
|
||||
import '../../../../../app_config.dart';
|
||||
import '../../../../../db/sqlite/firo_cache.dart';
|
||||
import '../../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../../providers/db/main_db_provider.dart';
|
||||
import '../../../../../providers/global/prefs_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 isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface &&
|
||||
wallet.isViewOnly &&
|
||||
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly;
|
||||
|
||||
return DesktopDialog(
|
||||
child: Column(
|
||||
|
@ -386,40 +390,41 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
|||
),
|
||||
),
|
||||
// reuseAddress preference.
|
||||
_MoreFeaturesItemBase(
|
||||
onPressed: _switchReuseAddressToggled,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 3),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: IgnorePointer(
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
pWalletInfo(widget.walletId)
|
||||
.select((value) => value.otherData),
|
||||
)[WalletInfoKeys.reuseAddress] as bool? ??
|
||||
false,
|
||||
controller: _switchController,
|
||||
if (!isViewOnlyNoAddressGen)
|
||||
_MoreFeaturesItemBase(
|
||||
onPressed: _switchReuseAddressToggled,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 3),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: IgnorePointer(
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
pWalletInfo(widget.walletId)
|
||||
.select((value) => value.otherData),
|
||||
)[WalletInfoKeys.reuseAddress] as bool? ??
|
||||
false,
|
||||
controller: _switchController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Reuse receiving address",
|
||||
style: STextStyles.w600_20(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Reuse receiving address",
|
||||
style: STextStyles.w600_20(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 28,
|
||||
),
|
||||
|
|
|
@ -83,7 +83,9 @@ class _UnlockWalletKeysDesktopState
|
|||
.verifyPassphrase(passwordController.text);
|
||||
|
||||
if (verified) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
({String keys, String config})? frostData;
|
||||
|
@ -101,7 +103,8 @@ class _UnlockWalletKeysDesktopState
|
|||
throw Exception("FIXME ~= see todo in code");
|
||||
}
|
||||
} else {
|
||||
if (wallet is ViewOnlyOptionInterface) {
|
||||
if (wallet is ViewOnlyOptionInterface &&
|
||||
(wallet as ViewOnlyOptionInterface).isViewOnly) {
|
||||
// TODO: is something needed here?
|
||||
} else {
|
||||
words = await wallet.getMnemonicAsWords();
|
||||
|
@ -109,7 +112,9 @@ class _UnlockWalletKeysDesktopState
|
|||
}
|
||||
|
||||
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();
|
||||
} else if (wallet is LibMoneroWallet) {
|
||||
keyData = await wallet.getKeys();
|
||||
|
@ -127,17 +132,20 @@ class _UnlockWalletKeysDesktopState
|
|||
);
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
}
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Invalid passphrase!",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Invalid passphrase!",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -332,7 +340,10 @@ class _UnlockWalletKeysDesktopState
|
|||
.verifyPassphrase(passwordController.text);
|
||||
|
||||
if (verified) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop();
|
||||
}
|
||||
|
||||
({String keys, String config})? frostData;
|
||||
List<String>? words;
|
||||
|
@ -352,7 +363,9 @@ class _UnlockWalletKeysDesktopState
|
|||
throw Exception("FIXME ~= see todo in code");
|
||||
}
|
||||
} else {
|
||||
if (wallet is ViewOnlyOptionInterface) {
|
||||
if (wallet is ViewOnlyOptionInterface &&
|
||||
(wallet as ViewOnlyOptionInterface)
|
||||
.isViewOnly) {
|
||||
// TODO: is something needed here?
|
||||
} else {
|
||||
words = await wallet.getMnemonicAsWords();
|
||||
|
@ -360,7 +373,10 @@ class _UnlockWalletKeysDesktopState
|
|||
}
|
||||
|
||||
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();
|
||||
} else if (wallet is LibMoneroWallet) {
|
||||
keyData = await wallet.getKeys();
|
||||
|
@ -379,19 +395,23 @@ class _UnlockWalletKeysDesktopState
|
|||
);
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pop();
|
||||
}
|
||||
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 300),
|
||||
);
|
||||
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Invalid passphrase!",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Invalid passphrase!",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
|
|
|
@ -16,9 +16,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
||||
import '../../../../models/keys/cw_key_data.dart';
|
||||
import '../../../../models/keys/key_data_interface.dart';
|
||||
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../../models/keys/xpriv_data.dart';
|
||||
import '../../../../notifications/show_flush_bar.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/wallet_xprivs.dart';
|
||||
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
|
@ -180,32 +182,39 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
|
|||
],
|
||||
)
|
||||
: keyData != null
|
||||
? CustomTabView(
|
||||
titles: [
|
||||
if (words.isNotEmpty) "Mnemonic",
|
||||
if (keyData is XPrivData) "XPriv(s)",
|
||||
if (keyData is CWKeyData) "Keys",
|
||||
],
|
||||
children: [
|
||||
if (words.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: _Mnemonic(
|
||||
words: words,
|
||||
),
|
||||
? keyData is ViewOnlyWalletData
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ViewOnlyWalletDataWidget(
|
||||
data: keyData as ViewOnlyWalletData,
|
||||
),
|
||||
if (keyData is XPrivData)
|
||||
WalletXPrivs(
|
||||
xprivData: keyData as XPrivData,
|
||||
walletId: walletId,
|
||||
),
|
||||
if (keyData is CWKeyData)
|
||||
CNWalletKeys(
|
||||
cwKeyData: keyData as CWKeyData,
|
||||
walletId: walletId,
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
: CustomTabView(
|
||||
titles: [
|
||||
if (words.isNotEmpty) "Mnemonic",
|
||||
if (keyData is XPrivData) "XPriv(s)",
|
||||
if (keyData is CWKeyData) "Keys",
|
||||
],
|
||||
children: [
|
||||
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(
|
||||
words: words,
|
||||
),
|
||||
|
|
|
@ -31,6 +31,7 @@ import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
|||
import '../../../../wallets/crypto_currency/intermediate/nano_currency.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/view_only_option_interface.dart';
|
||||
import '../../../addresses/desktop_wallet_addresses_view.dart';
|
||||
import '../../../lelantus_coins/lelantus_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 bool xpubEnabled =
|
||||
ref.watch(pWallets).getWallet(walletId) is ExtendedKeysInterface;
|
||||
final wallet = ref.watch(pWallets).getWallet(walletId);
|
||||
bool xpubEnabled = wallet is ExtendedKeysInterface;
|
||||
|
||||
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||
xpubEnabled = false;
|
||||
}
|
||||
|
||||
final bool canChangeRep = coin is NanoCurrency;
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import 'models/isar/models/contact_entry.dart';
|
|||
import 'models/isar/models/isar_models.dart';
|
||||
import 'models/isar/ordinal.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/send_view_auto_fill_data.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/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/choose_coin_view.dart';
|
||||
import 'widgets/frost_scaffold.dart';
|
||||
|
||||
|
|
|
@ -84,6 +84,9 @@ class Wallets {
|
|||
key: Wallet.mnemonicPassphraseKey(walletId: walletId),
|
||||
);
|
||||
await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId));
|
||||
await secureStorage.delete(
|
||||
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
|
||||
);
|
||||
|
||||
if (info.coin is Wownero) {
|
||||
await lib_monero_compat.deleteWalletFiles(
|
||||
|
|
|
@ -92,7 +92,7 @@ class Wownero extends CryptonoteCurrency {
|
|||
bool get hasMnemonicPassphraseSupport => false;
|
||||
|
||||
@override
|
||||
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 25];
|
||||
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 16, 25];
|
||||
|
||||
@override
|
||||
BigInt get satsPerCoin => BigInt.from(100000000000);
|
||||
|
|
|
@ -5,9 +5,11 @@ import 'package:flutter/foundation.dart';
|
|||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
import '../../../utilities/enums/derive_path_type_enum.dart';
|
||||
import '../interfaces/view_only_option_currency_interface.dart';
|
||||
import 'bip39_currency.dart';
|
||||
|
||||
abstract class Bip39HDCurrency extends Bip39Currency {
|
||||
abstract class Bip39HDCurrency extends Bip39Currency
|
||||
implements ViewOnlyOptionCurrencyInterface {
|
||||
Bip39HDCurrency(super.network);
|
||||
|
||||
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) {
|
||||
final hash = sha256.convert(bytes.toList(growable: false)).toString();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:uuid/uuid.dart';
|
|||
import '../../../app_config.dart';
|
||||
import '../../../models/balance.dart';
|
||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../crypto_currency/crypto_currency.dart';
|
||||
import '../isar_id_interface.dart';
|
||||
import 'wallet_info_meta.dart';
|
||||
|
@ -121,6 +122,13 @@ class WalletInfo implements IsarId {
|
|||
bool get isViewOnly =>
|
||||
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 =>
|
||||
(await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst())
|
||||
?.isMnemonicVerified ==
|
||||
|
@ -517,4 +525,5 @@ abstract class WalletInfoKeys {
|
|||
static const String enableOptInRbf = "enableOptInRbfKey";
|
||||
static const String reuseAddress = "reuseAddressKey";
|
||||
static const String isViewOnlyKey = "isViewOnlyKey";
|
||||
static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey";
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../isar_id_interface.dart';
|
||||
|
||||
part 'wallet_info_meta.g.dart';
|
||||
|
|
|
@ -6,15 +6,17 @@ import 'package:isar/isar.dart';
|
|||
|
||||
import '../../../models/balance.dart';
|
||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
import '../../../utilities/enums/derive_path_type_enum.dart';
|
||||
import '../../../utilities/extensions/extensions.dart';
|
||||
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
|
||||
import '../wallet_mixin_interfaces/multi_address_interface.dart';
|
||||
import '../wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||
import 'bip39_wallet.dart';
|
||||
|
||||
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
||||
with MultiAddressInterface<T> {
|
||||
with MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
|
||||
Bip39HDWallet(super.cryptoCurrency);
|
||||
|
||||
Set<AddressType> get supportedAddressTypes =>
|
||||
|
@ -116,7 +118,9 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
|||
|
||||
@override
|
||||
Future<void> checkSaveInitialReceivingAddress() async {
|
||||
final current = await getCurrentChangeAddress();
|
||||
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) return;
|
||||
|
||||
final current = await getCurrentReceivingAddress();
|
||||
if (current == null) {
|
||||
final address = await _generateAddress(
|
||||
chain: 0, // receiving
|
||||
|
@ -174,15 +178,29 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
|||
required int index,
|
||||
required DerivePathType derivePathType,
|
||||
}) async {
|
||||
final root = await getRootHDNode();
|
||||
|
||||
final derivationPath = cryptoCurrency.constructDerivePath(
|
||||
derivePathType: derivePathType,
|
||||
chain: chain,
|
||||
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(
|
||||
publicKey: keys.publicKey,
|
||||
|
@ -205,7 +223,8 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
|||
value: convertAddressString(data.address.toString()),
|
||||
publicKey: keys.publicKey.data,
|
||||
derivationIndex: index,
|
||||
derivationPath: DerivationPath()..value = derivationPath,
|
||||
derivationPath:
|
||||
isViewOnly ? null : (DerivationPath()..value = derivationPath),
|
||||
type: data.addressType,
|
||||
subType: subType,
|
||||
);
|
||||
|
|
|
@ -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/transaction_v2.dart';
|
||||
import '../../../models/keys/cw_key_data.dart';
|
||||
import '../../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../../models/paymint/fee_object_model.dart';
|
||||
import '../../../services/event_bus/events/global/blocks_remaining_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>
|
||||
extends CryptonoteWallet<T>
|
||||
implements MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
|
||||
with ViewOnlyOptionInterface<T>
|
||||
implements MultiAddressInterface<T> {
|
||||
@override
|
||||
int get isarTransactionVersion => 2;
|
||||
|
||||
|
@ -91,8 +93,16 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
.walletIdEqualTo(walletId)
|
||||
.watch(fireImmediately: true)
|
||||
.listen((utxos) async {
|
||||
await onUTXOsChanged(utxos);
|
||||
await updateBalance(shouldUpdateUtxos: false);
|
||||
try {
|
||||
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
|
||||
Future<void> init({bool? isRestore}) async {
|
||||
final path = await pathForWallet(
|
||||
|
@ -1301,19 +1327,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
|
||||
// ============== View only ==================================================
|
||||
|
||||
@override
|
||||
bool get isViewOnly => info.isViewOnly;
|
||||
|
||||
@override
|
||||
Future<void> recoverViewOnly() async {
|
||||
await refreshMutex.protect(() async {
|
||||
final jsonEncodedString = await secureStorageInterface.read(
|
||||
key: Wallet.getViewOnlyWalletDataSecStoreKey(
|
||||
walletId: walletId,
|
||||
),
|
||||
);
|
||||
|
||||
final data = ViewOnlyWalletData.fromJsonEncodedString(jsonEncodedString!);
|
||||
final data =
|
||||
await getViewOnlyWalletData() as CryptonoteViewOnlyWalletData;
|
||||
|
||||
try {
|
||||
final height = max(info.restoreHeight, 0);
|
||||
|
@ -1338,8 +1356,8 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
|||
final wallet = await getRestoredFromViewKeyWallet(
|
||||
path: path,
|
||||
password: password,
|
||||
address: data.address!,
|
||||
privateViewKey: data.privateViewKey!,
|
||||
address: data.address,
|
||||
privateViewKey: data.privateViewKey,
|
||||
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 ====================================================
|
||||
|
||||
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:mutex/mutex.dart';
|
|||
import '../../db/isar/main_db.dart';
|
||||
import '../../models/isar/models/blockchain_data/address.dart';
|
||||
import '../../models/isar/models/ethereum/eth_contract.dart';
|
||||
import '../../models/keys/view_only_wallet_data.dart';
|
||||
import '../../models/node_model.dart';
|
||||
import '../../models/paymint/fee_object_model.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,
|
||||
);
|
||||
|
||||
if (wallet is ViewOnlyOptionInterface) {
|
||||
if (wallet is ViewOnlyOptionInterface && walletInfo.isViewOnly) {
|
||||
await secureStorageInterface.write(
|
||||
key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId),
|
||||
value: viewOnlyData!.toJsonEncodedString(),
|
||||
|
@ -583,6 +584,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
if (!tAlive) throw Exception("refresh alive ping failure");
|
||||
}
|
||||
|
||||
final viewOnly = this is ViewOnlyOptionInterface &&
|
||||
(this as ViewOnlyOptionInterface).isViewOnly;
|
||||
|
||||
try {
|
||||
// this acquire should be almost instant due to above check.
|
||||
// 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.
|
||||
final Set<String> codesToCheck = {};
|
||||
_checkAlive();
|
||||
if (this is PaynymInterface) {
|
||||
if (this is PaynymInterface && !viewOnly) {
|
||||
// isSegwit does not matter here at all
|
||||
final myCode =
|
||||
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.
|
||||
if (this is MultiAddressInterface) {
|
||||
await (this as MultiAddressInterface)
|
||||
.checkChangeAddressForTransactions();
|
||||
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
|
||||
await (this as MultiAddressInterface)
|
||||
.checkChangeAddressForTransactions();
|
||||
}
|
||||
}
|
||||
_checkAlive();
|
||||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
||||
|
@ -681,7 +687,7 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
await fetchFuture;
|
||||
|
||||
// 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();
|
||||
await (this as PaynymInterface)
|
||||
.checkForNotificationTransactionsTo(codesToCheck);
|
||||
|
|
|
@ -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/transaction_v2.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/signing_data.dart';
|
||||
import '../../../utilities/amount/amount.dart';
|
||||
|
@ -32,9 +33,10 @@ import '../intermediate/bip39_hd_wallet.dart';
|
|||
import 'cpfp_interface.dart';
|
||||
import 'paynym_interface.dart';
|
||||
import 'rbf_interface.dart';
|
||||
import 'view_only_option_interface.dart';
|
||||
|
||||
mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||
on Bip39HDWallet<T> {
|
||||
on Bip39HDWallet<T> implements ViewOnlyOptionInterface<T> {
|
||||
late ElectrumXClient electrumXClient;
|
||||
late CachedElectrumXClient electrumXCachedClient;
|
||||
|
||||
|
@ -137,7 +139,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
(e.used != true) &&
|
||||
(canCPFP ||
|
||||
e.isConfirmed(
|
||||
currentChainHeight, cryptoCurrency.minConfirms)),
|
||||
currentChainHeight,
|
||||
cryptoCurrency.minConfirms,
|
||||
)),
|
||||
)
|
||||
.toList();
|
||||
final spendableSatoshiValue =
|
||||
|
@ -944,7 +948,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
|
||||
Future<({List<Address> addresses, int index})> checkGapsBatched(
|
||||
int txCountBatchSize,
|
||||
coinlib.HDPrivateKey root,
|
||||
coinlib.HDKey node,
|
||||
DerivePathType type,
|
||||
int chain,
|
||||
) async {
|
||||
|
@ -969,7 +973,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
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(
|
||||
publicKey: keys.publicKey,
|
||||
|
@ -986,7 +997,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
publicKey: keys.publicKey.data,
|
||||
type: addressData.addressType,
|
||||
derivationIndex: index + j,
|
||||
derivationPath: DerivationPath()..value = derivePath,
|
||||
derivationPath:
|
||||
isViewOnly ? null : (DerivationPath()..value = derivePath),
|
||||
subType:
|
||||
chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
||||
);
|
||||
|
@ -1025,13 +1037,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
}
|
||||
|
||||
Future<({List<Address> addresses, int index})> checkGapsLinearly(
|
||||
coinlib.HDPrivateKey root,
|
||||
coinlib.HDKey node,
|
||||
DerivePathType type,
|
||||
int chain,
|
||||
) async {
|
||||
final List<Address> addressArray = [];
|
||||
int gapCounter = 0;
|
||||
int index = 0;
|
||||
|
||||
for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) {
|
||||
Logging.instance.log(
|
||||
"index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter",
|
||||
|
@ -1043,7 +1056,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
chain: chain,
|
||||
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(
|
||||
publicKey: keys.publicKey,
|
||||
derivePathType: type,
|
||||
|
@ -1059,7 +1081,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
publicKey: keys.publicKey.data,
|
||||
type: addressData.addressType,
|
||||
derivationIndex: index,
|
||||
derivationPath: DerivationPath()..value = derivePath,
|
||||
derivationPath:
|
||||
isViewOnly ? null : (DerivationPath()..value = derivePath),
|
||||
subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
||||
);
|
||||
|
||||
|
@ -1331,6 +1354,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
|
||||
@override
|
||||
Future<void> checkReceivingAddressForTransactions() async {
|
||||
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
|
||||
try {
|
||||
throw Exception();
|
||||
|
@ -1382,6 +1409,21 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
|
||||
@override
|
||||
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 {
|
||||
final currentChange = await getCurrentChangeAddress();
|
||||
|
||||
|
@ -1419,6 +1461,11 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
|
||||
@override
|
||||
Future<void> recover({required bool isRescan}) async {
|
||||
if (isViewOnly) {
|
||||
await recoverViewOnly(isRescan: isRescan);
|
||||
return;
|
||||
}
|
||||
|
||||
final root = await getRootHDNode();
|
||||
|
||||
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
|
||||
|
@ -1918,5 +1965,219 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
}
|
||||
|
|
|
@ -2,35 +2,35 @@ import '../../../models/keys/xpriv_data.dart';
|
|||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||
import 'electrumx_interface.dart';
|
||||
|
||||
typedef XPub = ({String path, String xpub});
|
||||
typedef XPriv = ({String path, String xpriv});
|
||||
abstract class XKey {
|
||||
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>
|
||||
on ElectrumXInterface<T> {
|
||||
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
|
||||
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
|
||||
(e) => (
|
||||
path: e,
|
||||
addressType: e.getAddressType(),
|
||||
),
|
||||
);
|
||||
final paths = cryptoCurrency.supportedHardenedDerivationPaths;
|
||||
|
||||
final master = await getRootHDNode();
|
||||
final fingerprint = master.fingerprint.toRadixString(16);
|
||||
|
||||
final futures = paths.map((e) 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 futures = paths.map((path) async {
|
||||
final node = master.derivePath(path);
|
||||
|
||||
return (
|
||||
return XPub(
|
||||
path: path,
|
||||
xpub: node.hdPublicKey.encode(
|
||||
encoded: node.hdPublicKey.encode(
|
||||
cryptoCurrency.networkParams.pubHDPrefix,
|
||||
// 0x04b24746,
|
||||
),
|
||||
|
@ -44,29 +44,17 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
|||
}
|
||||
|
||||
Future<XPrivData> getXPrivs() async {
|
||||
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
|
||||
(e) => (
|
||||
path: e,
|
||||
addressType: e.getAddressType(),
|
||||
),
|
||||
);
|
||||
final paths = cryptoCurrency.supportedHardenedDerivationPaths;
|
||||
|
||||
final master = await getRootHDNode();
|
||||
final fingerprint = master.fingerprint.toRadixString(16);
|
||||
|
||||
final futures = paths.map((e) 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 futures = paths.map((path) async {
|
||||
final node = master.derivePath(path);
|
||||
|
||||
return (
|
||||
return XPriv(
|
||||
path: path,
|
||||
xpriv: node.encode(
|
||||
encoded: node.encode(
|
||||
cryptoCurrency.networkParams.privHDPrefix,
|
||||
),
|
||||
);
|
||||
|
@ -76,9 +64,9 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
|||
walletId: walletId,
|
||||
fingerprint: fingerprint,
|
||||
xprivs: [
|
||||
(
|
||||
XPriv(
|
||||
path: "Master",
|
||||
xpriv: master.encode(
|
||||
encoded: master.encode(
|
||||
cryptoCurrency.networkParams.privHDPrefix,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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 '../wallet.dart';
|
||||
|
||||
class ViewOnlyWalletData {
|
||||
final String? address;
|
||||
final String? privateViewKey;
|
||||
|
||||
ViewOnlyWalletData({required this.address, required this.privateViewKey});
|
||||
|
||||
factory ViewOnlyWalletData.fromJsonEncodedString(String jsonEncodedString) {
|
||||
final map = jsonDecode(jsonEncodedString) as Map;
|
||||
final json = Map<String, dynamic>.from(map);
|
||||
return ViewOnlyWalletData(
|
||||
address: json["address"] as String?,
|
||||
privateViewKey: json["privateViewKey"] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
String toJsonEncodedString() => jsonEncode({
|
||||
"address": address,
|
||||
"privateViewKey": privateViewKey,
|
||||
});
|
||||
}
|
||||
|
||||
mixin ViewOnlyOptionInterface<T extends ViewOnlyOptionCurrencyInterface>
|
||||
on Wallet<T> {
|
||||
bool get isViewOnly;
|
||||
ViewOnlyWalletType get viewOnlyType => info.viewOnlyWalletType!;
|
||||
|
||||
bool get isViewOnly => info.isViewOnly;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ class StackDialog extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
child: SelectableText(
|
||||
title,
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
|
@ -110,7 +110,7 @@ class StackDialog extends StatelessWidget {
|
|||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
SelectableText(
|
||||
message!,
|
||||
style: STextStyles.smallMed14(context),
|
||||
),
|
||||
|
|
Loading…
Reference in a new issue