mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-22 19:39:22 +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/text_styles.dart';
|
||||||
import '../../../utilities/util.dart';
|
import '../../../utilities/util.dart';
|
||||||
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import '../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
||||||
import '../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
import '../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
||||||
import '../../../wallets/isar/models/wallet_info.dart';
|
import '../../../wallets/isar/models/wallet_info.dart';
|
||||||
import '../../../widgets/background.dart';
|
import '../../../widgets/background.dart';
|
||||||
|
@ -104,7 +105,9 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
||||||
case AddWalletType.New:
|
case AddWalletType.New:
|
||||||
unawaited(
|
unawaited(
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
coin.hasMnemonicPassphraseSupport
|
coin.possibleMnemonicLengths.length > 1 ||
|
||||||
|
coin.hasMnemonicPassphraseSupport ||
|
||||||
|
coin is ViewOnlyOptionCurrencyInterface
|
||||||
? NewWalletOptionsView.routeName
|
? NewWalletOptionsView.routeName
|
||||||
: NewWalletRecoveryPhraseWarningView.routeName,
|
: NewWalletRecoveryPhraseWarningView.routeName,
|
||||||
arguments: Tuple2(
|
arguments: Tuple2(
|
||||||
|
|
|
@ -12,9 +12,11 @@ import '../../../utilities/constants.dart';
|
||||||
import '../../../utilities/text_styles.dart';
|
import '../../../utilities/text_styles.dart';
|
||||||
import '../../../utilities/util.dart';
|
import '../../../utilities/util.dart';
|
||||||
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import '../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
||||||
import '../../../widgets/background.dart';
|
import '../../../widgets/background.dart';
|
||||||
import '../../../widgets/conditional_parent.dart';
|
import '../../../widgets/conditional_parent.dart';
|
||||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
import '../../../widgets/custom_buttons/checkbox_text_button.dart';
|
||||||
import '../../../widgets/desktop/desktop_app_bar.dart';
|
import '../../../widgets/desktop/desktop_app_bar.dart';
|
||||||
import '../../../widgets/desktop/desktop_scaffold.dart';
|
import '../../../widgets/desktop/desktop_scaffold.dart';
|
||||||
import '../../../widgets/desktop/primary_button.dart';
|
import '../../../widgets/desktop/primary_button.dart';
|
||||||
|
@ -25,8 +27,12 @@ import '../new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_wa
|
||||||
import '../restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart';
|
import '../restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart';
|
||||||
import '../restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart';
|
import '../restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart';
|
||||||
|
|
||||||
final pNewWalletOptions =
|
final pNewWalletOptions = StateProvider<
|
||||||
StateProvider<({String mnemonicPassphrase, int mnemonicWordsCount})?>(
|
({
|
||||||
|
String mnemonicPassphrase,
|
||||||
|
int mnemonicWordsCount,
|
||||||
|
bool convertToViewOnly,
|
||||||
|
})?>(
|
||||||
(ref) => null,
|
(ref) => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -59,6 +65,8 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
||||||
bool hidePassword = true;
|
bool hidePassword = true;
|
||||||
NewWalletOptions _selectedOptions = NewWalletOptions.Default;
|
NewWalletOptions _selectedOptions = NewWalletOptions.Default;
|
||||||
|
|
||||||
|
bool _convertToViewOnly = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
passwordController = TextEditingController();
|
passwordController = TextEditingController();
|
||||||
|
@ -210,7 +218,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
||||||
if (_selectedOptions == NewWalletOptions.Advanced)
|
if (_selectedOptions == NewWalletOptions.Advanced)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
if (Util.isDesktop)
|
if (Util.isDesktop && lengths.length > 1)
|
||||||
DropdownButtonHideUnderline(
|
DropdownButtonHideUnderline(
|
||||||
child: DropdownButton2<int>(
|
child: DropdownButton2<int>(
|
||||||
value: ref
|
value: ref
|
||||||
|
@ -265,7 +273,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!Util.isDesktop)
|
if (!Util.isDesktop && lengths.length > 1)
|
||||||
MobileMnemonicLengthSelector(
|
MobileMnemonicLengthSelector(
|
||||||
chooseMnemonicLength: () {
|
chooseMnemonicLength: () {
|
||||||
showModalBottomSheet<dynamic>(
|
showModalBottomSheet<dynamic>(
|
||||||
|
@ -284,91 +292,109 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||||
height: 24,
|
const SizedBox(
|
||||||
),
|
height: 24,
|
||||||
RoundedWhiteContainer(
|
),
|
||||||
child: Center(
|
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||||
child: Text(
|
RoundedWhiteContainer(
|
||||||
"You may add a BIP39 passphrase. This is optional. "
|
child: Center(
|
||||||
"You will need BOTH your seed and your passphrase to recover the wallet.",
|
child: Text(
|
||||||
style: Util.isDesktop
|
"You may add a BIP39 passphrase. This is optional. "
|
||||||
? STextStyles.desktopTextExtraSmall(context)
|
"You will need BOTH your seed and your passphrase to recover the wallet.",
|
||||||
.copyWith(
|
style: Util.isDesktop
|
||||||
color: Theme.of(context)
|
? STextStyles.desktopTextExtraSmall(context)
|
||||||
.extension<StackColors>()!
|
.copyWith(
|
||||||
.textSubtitle1,
|
color: Theme.of(context)
|
||||||
)
|
.extension<StackColors>()!
|
||||||
: STextStyles.itemSubtitle(context),
|
.textSubtitle1,
|
||||||
|
)
|
||||||
|
: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
Constants.size.circularBorderRadius,
|
|
||||||
),
|
),
|
||||||
child: TextField(
|
if (widget.coin.hasMnemonicPassphraseSupport)
|
||||||
key: const Key("mnemonicPassphraseFieldKey1"),
|
ClipRRect(
|
||||||
focusNode: passwordFocusNode,
|
borderRadius: BorderRadius.circular(
|
||||||
controller: passwordController,
|
Constants.size.circularBorderRadius,
|
||||||
style: Util.isDesktop
|
),
|
||||||
? STextStyles.desktopTextMedium(context).copyWith(
|
child: TextField(
|
||||||
height: 2,
|
key: const Key("mnemonicPassphraseFieldKey1"),
|
||||||
)
|
focusNode: passwordFocusNode,
|
||||||
: STextStyles.field(context),
|
controller: passwordController,
|
||||||
obscureText: hidePassword,
|
style: Util.isDesktop
|
||||||
enableSuggestions: false,
|
? STextStyles.desktopTextMedium(context).copyWith(
|
||||||
autocorrect: false,
|
height: 2,
|
||||||
decoration: standardInputDecoration(
|
)
|
||||||
"BIP39 passphrase",
|
: STextStyles.field(context),
|
||||||
passwordFocusNode,
|
obscureText: hidePassword,
|
||||||
context,
|
enableSuggestions: false,
|
||||||
).copyWith(
|
autocorrect: false,
|
||||||
suffixIcon: UnconstrainedBox(
|
decoration: standardInputDecoration(
|
||||||
child: ConditionalParent(
|
"BIP39 passphrase",
|
||||||
condition: Util.isDesktop,
|
passwordFocusNode,
|
||||||
builder: (child) => SizedBox(
|
context,
|
||||||
height: 70,
|
).copyWith(
|
||||||
child: child,
|
suffixIcon: UnconstrainedBox(
|
||||||
),
|
child: ConditionalParent(
|
||||||
child: Row(
|
condition: Util.isDesktop,
|
||||||
children: [
|
builder: (child) => SizedBox(
|
||||||
SizedBox(
|
height: 70,
|
||||||
width: Util.isDesktop ? 24 : 16,
|
child: child,
|
||||||
),
|
),
|
||||||
GestureDetector(
|
child: Row(
|
||||||
key: const Key(
|
children: [
|
||||||
"mnemonicPassphraseFieldShowPasswordButtonKey",
|
SizedBox(
|
||||||
),
|
|
||||||
onTap: () async {
|
|
||||||
setState(() {
|
|
||||||
hidePassword = !hidePassword;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
hidePassword
|
|
||||||
? Assets.svg.eye
|
|
||||||
: Assets.svg.eyeSlash,
|
|
||||||
color: Theme.of(context)
|
|
||||||
.extension<StackColors>()!
|
|
||||||
.textDark3,
|
|
||||||
width: Util.isDesktop ? 24 : 16,
|
width: Util.isDesktop ? 24 : 16,
|
||||||
height: Util.isDesktop ? 24 : 16,
|
|
||||||
),
|
),
|
||||||
),
|
GestureDetector(
|
||||||
const SizedBox(
|
key: const Key(
|
||||||
width: 12,
|
"mnemonicPassphraseFieldShowPasswordButtonKey",
|
||||||
),
|
),
|
||||||
],
|
onTap: () async {
|
||||||
|
setState(() {
|
||||||
|
hidePassword = !hidePassword;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
hidePassword
|
||||||
|
? Assets.svg.eye
|
||||||
|
: Assets.svg.eyeSlash,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textDark3,
|
||||||
|
width: Util.isDesktop ? 24 : 16,
|
||||||
|
height: Util.isDesktop ? 24 : 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.coin is ViewOnlyOptionCurrencyInterface)
|
||||||
|
const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
),
|
||||||
|
if (widget.coin is ViewOnlyOptionCurrencyInterface)
|
||||||
|
CheckboxTextButton(
|
||||||
|
label: "Convert to view only wallet. "
|
||||||
|
"You will only be shown the seed phrase once. "
|
||||||
|
"Save it somewhere. "
|
||||||
|
"If you lose it you will lose access to any funds in this wallet.",
|
||||||
|
onChanged: (value) {
|
||||||
|
_convertToViewOnly = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!Util.isDesktop) const Spacer(),
|
if (!Util.isDesktop) const Spacer(),
|
||||||
|
@ -383,6 +409,7 @@ class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
|
||||||
mnemonicWordsCount:
|
mnemonicWordsCount:
|
||||||
ref.read(mnemonicWordCountStateProvider.state).state,
|
ref.read(mnemonicWordCountStateProvider.state).state,
|
||||||
mnemonicPassphrase: passwordController.text,
|
mnemonicPassphrase: passwordController.text,
|
||||||
|
convertToViewOnly: _convertToViewOnly,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ref.read(pNewWalletOptions.notifier).state = null;
|
ref.read(pNewWalletOptions.notifier).state = null;
|
||||||
|
|
|
@ -582,7 +582,12 @@ class _NewWalletRecoveryPhraseWarningViewState
|
||||||
)
|
)
|
||||||
.state!
|
.state!
|
||||||
.mnemonicPassphrase;
|
.mnemonicPassphrase;
|
||||||
} else {}
|
} else {
|
||||||
|
// this may not be epiccash specific?
|
||||||
|
if (coin is Epiccash) {
|
||||||
|
mnemonicPassphrase = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wordCount = ref
|
wordCount = ref
|
||||||
.read(
|
.read(
|
||||||
|
|
|
@ -24,6 +24,7 @@ import '../../../../utilities/text_styles.dart';
|
||||||
import '../../../../utilities/util.dart';
|
import '../../../../utilities/util.dart';
|
||||||
import '../../../../wallets/crypto_currency/crypto_currency.dart';
|
import '../../../../wallets/crypto_currency/crypto_currency.dart';
|
||||||
import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
import '../../../../wallets/crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
||||||
|
import '../../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
|
||||||
import '../../../../widgets/conditional_parent.dart';
|
import '../../../../widgets/conditional_parent.dart';
|
||||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../../widgets/custom_buttons/checkbox_text_button.dart';
|
import '../../../../widgets/custom_buttons/checkbox_text_button.dart';
|
||||||
|
@ -656,7 +657,7 @@ class ViewOnlyRestoreOption extends StatefulWidget {
|
||||||
class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> {
|
class _ViewOnlyRestoreOptionState extends State<ViewOnlyRestoreOption> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final showDateOption = widget.coin is ViewOnlyOptionCurrencyInterface;
|
final showDateOption = widget.coin is CryptonoteCurrency;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
if (showDateOption)
|
if (showDateOption)
|
||||||
|
|
|
@ -4,32 +4,41 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
|
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
|
||||||
as cs_monero_deprecated;
|
as cs_monero_deprecated;
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../pages_desktop_specific/desktop_home_view.dart';
|
import '../../../pages_desktop_specific/desktop_home_view.dart';
|
||||||
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
import '../../../providers/db/main_db_provider.dart';
|
import '../../../providers/db/main_db_provider.dart';
|
||||||
import '../../../providers/global/secure_store_provider.dart';
|
import '../../../providers/global/secure_store_provider.dart';
|
||||||
import '../../../providers/providers.dart';
|
import '../../../providers/providers.dart';
|
||||||
import '../../../themes/stack_colors.dart';
|
import '../../../themes/stack_colors.dart';
|
||||||
|
import '../../../utilities/assets.dart';
|
||||||
import '../../../utilities/barcode_scanner_interface.dart';
|
import '../../../utilities/barcode_scanner_interface.dart';
|
||||||
import '../../../utilities/clipboard_interface.dart';
|
import '../../../utilities/clipboard_interface.dart';
|
||||||
|
import '../../../utilities/constants.dart';
|
||||||
import '../../../utilities/text_styles.dart';
|
import '../../../utilities/text_styles.dart';
|
||||||
import '../../../utilities/util.dart';
|
import '../../../utilities/util.dart';
|
||||||
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import '../../../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||||
|
import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
|
||||||
|
import '../../../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
|
||||||
import '../../../wallets/isar/models/wallet_info.dart';
|
import '../../../wallets/isar/models/wallet_info.dart';
|
||||||
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
|
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
|
||||||
import '../../../wallets/wallet/impl/monero_wallet.dart';
|
import '../../../wallets/wallet/impl/monero_wallet.dart';
|
||||||
import '../../../wallets/wallet/impl/wownero_wallet.dart';
|
import '../../../wallets/wallet/impl/wownero_wallet.dart';
|
||||||
import '../../../wallets/wallet/wallet.dart';
|
import '../../../wallets/wallet/wallet.dart';
|
||||||
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../widgets/desktop/desktop_app_bar.dart';
|
import '../../../widgets/desktop/desktop_app_bar.dart';
|
||||||
import '../../../widgets/desktop/desktop_scaffold.dart';
|
import '../../../widgets/desktop/desktop_scaffold.dart';
|
||||||
import '../../../widgets/desktop/primary_button.dart';
|
import '../../../widgets/desktop/primary_button.dart';
|
||||||
import '../../../widgets/stack_text_field.dart';
|
import '../../../widgets/stack_text_field.dart';
|
||||||
|
import '../../../widgets/toggle.dart';
|
||||||
import '../../home_view/home_view.dart';
|
import '../../home_view/home_view.dart';
|
||||||
import 'confirm_recovery_dialog.dart';
|
import 'confirm_recovery_dialog.dart';
|
||||||
import 'sub_widgets/restore_failed_dialog.dart';
|
import 'sub_widgets/restore_failed_dialog.dart';
|
||||||
|
@ -66,7 +75,10 @@ class _RestoreViewOnlyWalletViewState
|
||||||
late final TextEditingController addressController;
|
late final TextEditingController addressController;
|
||||||
late final TextEditingController viewKeyController;
|
late final TextEditingController viewKeyController;
|
||||||
|
|
||||||
|
late String _currentDropDownValue;
|
||||||
|
|
||||||
bool _enableRestoreButton = false;
|
bool _enableRestoreButton = false;
|
||||||
|
bool _addressOnly = false;
|
||||||
|
|
||||||
bool _buttonLock = false;
|
bool _buttonLock = false;
|
||||||
|
|
||||||
|
@ -106,30 +118,43 @@ class _RestoreViewOnlyWalletViewState
|
||||||
WalletInfoKeys.isViewOnlyKey: true,
|
WalletInfoKeys.isViewOnlyKey: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (widget.restoreFromDate != null) {
|
final ViewOnlyWalletType viewOnlyWalletType;
|
||||||
if (widget.coin is Monero) {
|
if (widget.coin is Bip39HDCurrency) {
|
||||||
height = cs_monero_deprecated.getMoneroHeightByDate(
|
if (widget.coin is Firo) {
|
||||||
date: widget.restoreFromDate!,
|
otherDataJson.addAll(
|
||||||
|
{
|
||||||
|
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
||||||
|
WalletInfoKeys.enableLelantusScanning:
|
||||||
|
widget.enableLelantusScanning,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (widget.coin is Wownero) {
|
viewOnlyWalletType = _addressOnly
|
||||||
height = cs_monero_deprecated.getWowneroHeightByDate(
|
? ViewOnlyWalletType.addressOnly
|
||||||
date: widget.restoreFromDate!,
|
: ViewOnlyWalletType.xPub;
|
||||||
);
|
} else if (widget.coin is CryptonoteCurrency) {
|
||||||
|
if (widget.restoreFromDate != null) {
|
||||||
|
if (widget.coin is Monero) {
|
||||||
|
height = cs_monero_deprecated.getMoneroHeightByDate(
|
||||||
|
date: widget.restoreFromDate!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widget.coin is Wownero) {
|
||||||
|
height = cs_monero_deprecated.getWowneroHeightByDate(
|
||||||
|
date: widget.restoreFromDate!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (height < 0) height = 0;
|
||||||
}
|
}
|
||||||
if (height < 0) {
|
|
||||||
height = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.coin is Firo) {
|
viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
|
||||||
otherDataJson.addAll(
|
} else {
|
||||||
{
|
throw Exception(
|
||||||
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
"Unsupported view only wallet currency type found: ${widget.coin.runtimeType}",
|
||||||
WalletInfoKeys.enableLelantusScanning: widget.enableLelantusScanning,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] =
|
||||||
|
viewOnlyWalletType.index;
|
||||||
|
|
||||||
if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable();
|
if (!Platform.isLinux && !Util.isDesktop) await WakelockPlus.enable();
|
||||||
|
|
||||||
|
@ -166,6 +191,43 @@ class _RestoreViewOnlyWalletViewState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ViewOnlyWalletData viewOnlyData;
|
||||||
|
switch (viewOnlyWalletType) {
|
||||||
|
case ViewOnlyWalletType.cryptonote:
|
||||||
|
if (addressController.text.isEmpty ||
|
||||||
|
viewKeyController.text.isEmpty) {
|
||||||
|
throw Exception("Missing address and/or private view key fields");
|
||||||
|
}
|
||||||
|
viewOnlyData = CryptonoteViewOnlyWalletData(
|
||||||
|
walletId: info.walletId,
|
||||||
|
address: addressController.text,
|
||||||
|
privateViewKey: viewKeyController.text,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ViewOnlyWalletType.addressOnly:
|
||||||
|
if (addressController.text.isEmpty) {
|
||||||
|
throw Exception("Address is empty");
|
||||||
|
}
|
||||||
|
viewOnlyData = AddressViewOnlyWalletData(
|
||||||
|
walletId: info.walletId,
|
||||||
|
address: addressController.text,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ViewOnlyWalletType.xPub:
|
||||||
|
viewOnlyData = ExtendedKeysViewOnlyWalletData(
|
||||||
|
walletId: info.walletId,
|
||||||
|
xPubs: [
|
||||||
|
XPub(
|
||||||
|
path: _currentDropDownValue,
|
||||||
|
encoded: viewKeyController.text,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
var node = ref
|
var node = ref
|
||||||
.read(nodeServiceChangeNotifierProvider)
|
.read(nodeServiceChangeNotifierProvider)
|
||||||
.getPrimaryNodeFor(currency: widget.coin);
|
.getPrimaryNodeFor(currency: widget.coin);
|
||||||
|
@ -185,10 +247,7 @@ class _RestoreViewOnlyWalletViewState
|
||||||
secureStorageInterface: ref.read(secureStoreProvider),
|
secureStorageInterface: ref.read(secureStoreProvider),
|
||||||
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||||
prefs: ref.read(prefsChangeNotifierProvider),
|
prefs: ref.read(prefsChangeNotifierProvider),
|
||||||
viewOnlyData: ViewOnlyWalletData(
|
viewOnlyData: viewOnlyData,
|
||||||
address: addressController.text,
|
|
||||||
privateViewKey: viewKeyController.text,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: extract interface with isRestore param
|
// TODO: extract interface with isRestore param
|
||||||
|
@ -278,11 +337,27 @@ class _RestoreViewOnlyWalletViewState
|
||||||
super.initState();
|
super.initState();
|
||||||
addressController = TextEditingController();
|
addressController = TextEditingController();
|
||||||
viewKeyController = TextEditingController();
|
viewKeyController = TextEditingController();
|
||||||
|
|
||||||
|
if (widget.coin is Bip39HDCurrency) {
|
||||||
|
_currentDropDownValue = (widget.coin as Bip39HDCurrency)
|
||||||
|
.supportedHardenedDerivationPaths
|
||||||
|
.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
addressController.dispose();
|
||||||
|
viewKeyController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDesktop = Util.isDesktop;
|
final isDesktop = Util.isDesktop;
|
||||||
|
|
||||||
|
final isElectrumX = widget.coin is ElectrumXCurrencyInterface;
|
||||||
|
|
||||||
return MasterScaffold(
|
return MasterScaffold(
|
||||||
isDesktop: isDesktop,
|
isDesktop: isDesktop,
|
||||||
appBar: isDesktop
|
appBar: isDesktop
|
||||||
|
@ -339,32 +414,156 @@ class _RestoreViewOnlyWalletViewState
|
||||||
? STextStyles.desktopH2(context)
|
? STextStyles.desktopH2(context)
|
||||||
: STextStyles.pageTitleH1(context),
|
: STextStyles.pageTitleH1(context),
|
||||||
),
|
),
|
||||||
|
if (isElectrumX)
|
||||||
|
SizedBox(
|
||||||
|
height: isDesktop ? 24 : 16,
|
||||||
|
),
|
||||||
|
if (isElectrumX)
|
||||||
|
SizedBox(
|
||||||
|
height: isDesktop ? 56 : 48,
|
||||||
|
width: isDesktop ? 490 : null,
|
||||||
|
child: Toggle(
|
||||||
|
key: UniqueKey(),
|
||||||
|
onText: "Extended pub key",
|
||||||
|
offText: "Single address",
|
||||||
|
onColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.popupBG,
|
||||||
|
offColor: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
isOn: _addressOnly,
|
||||||
|
onValueChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_addressOnly = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: isDesktop ? 24 : 16,
|
height: isDesktop ? 24 : 16,
|
||||||
),
|
),
|
||||||
FullTextField(
|
if (!isElectrumX || _addressOnly)
|
||||||
label: "Address",
|
FullTextField(
|
||||||
controller: addressController,
|
key: const Key("viewOnlyAddressRestoreFieldKey"),
|
||||||
onChanged: (newValue) {
|
label: "Address",
|
||||||
setState(() {
|
controller: addressController,
|
||||||
_enableRestoreButton = newValue.isNotEmpty &&
|
onChanged: (newValue) {
|
||||||
viewKeyController.text.isNotEmpty;
|
if (isElectrumX) {
|
||||||
});
|
viewKeyController.text = "";
|
||||||
},
|
setState(() {
|
||||||
),
|
_enableRestoreButton = newValue.isNotEmpty;
|
||||||
SizedBox(
|
});
|
||||||
height: isDesktop ? 16 : 12,
|
} else {
|
||||||
),
|
setState(() {
|
||||||
FullTextField(
|
_enableRestoreButton = newValue.isNotEmpty &&
|
||||||
label: "View Key",
|
viewKeyController.text.isNotEmpty;
|
||||||
controller: viewKeyController,
|
});
|
||||||
onChanged: (value) {
|
}
|
||||||
setState(() {
|
},
|
||||||
_enableRestoreButton = value.isNotEmpty &&
|
),
|
||||||
addressController.text.isNotEmpty;
|
if (!isElectrumX)
|
||||||
});
|
SizedBox(
|
||||||
},
|
height: isDesktop ? 16 : 12,
|
||||||
),
|
),
|
||||||
|
if (isElectrumX && !_addressOnly)
|
||||||
|
DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<String>(
|
||||||
|
value: _currentDropDownValue,
|
||||||
|
items: [
|
||||||
|
...(widget.coin as Bip39HDCurrency)
|
||||||
|
.supportedHardenedDerivationPaths
|
||||||
|
.map(
|
||||||
|
(e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(
|
||||||
|
e,
|
||||||
|
style: STextStyles.w500_14(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value is String) {
|
||||||
|
setState(() {
|
||||||
|
_currentDropDownValue = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded: true,
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
iconStyleData: IconStyleData(
|
||||||
|
icon: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.svg.chevronDown,
|
||||||
|
width: 12,
|
||||||
|
height: 6,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldActiveSearchIconRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownStyleData: DropdownStyleData(
|
||||||
|
offset: const Offset(0, -10),
|
||||||
|
elevation: 0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldDefaultBG,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
Constants.size.circularBorderRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isElectrumX && !_addressOnly)
|
||||||
|
SizedBox(
|
||||||
|
height: isDesktop ? 16 : 12,
|
||||||
|
),
|
||||||
|
if (!isElectrumX || !_addressOnly)
|
||||||
|
FullTextField(
|
||||||
|
key: const Key("viewOnlyKeyRestoreFieldKey"),
|
||||||
|
label:
|
||||||
|
"${isElectrumX ? "Extended" : "Private View"} Key",
|
||||||
|
controller: viewKeyController,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (isElectrumX) {
|
||||||
|
addressController.text = "";
|
||||||
|
setState(() {
|
||||||
|
_enableRestoreButton = value.isNotEmpty;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_enableRestoreButton = value.isNotEmpty &&
|
||||||
|
addressController.text.isNotEmpty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
if (!isDesktop) const Spacer(),
|
if (!isDesktop) const Spacer(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: isDesktop ? 24 : 16,
|
height: isDesktop ? 24 : 16,
|
||||||
|
|
|
@ -9,13 +9,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:cs_monero/src/deprecated/get_height_by_date.dart'
|
||||||
|
as cs_monero_deprecated;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../notifications/show_flush_bar.dart';
|
import '../../../notifications/show_flush_bar.dart';
|
||||||
import '../../../pages_desktop_specific/desktop_home_view.dart';
|
import '../../../pages_desktop_specific/desktop_home_view.dart';
|
||||||
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||||
|
@ -25,14 +29,25 @@ import '../../../providers/providers.dart';
|
||||||
import '../../../themes/stack_colors.dart';
|
import '../../../themes/stack_colors.dart';
|
||||||
import '../../../utilities/assets.dart';
|
import '../../../utilities/assets.dart';
|
||||||
import '../../../utilities/constants.dart';
|
import '../../../utilities/constants.dart';
|
||||||
|
import '../../../utilities/logger.dart';
|
||||||
|
import '../../../utilities/show_loading.dart';
|
||||||
import '../../../utilities/text_styles.dart';
|
import '../../../utilities/text_styles.dart';
|
||||||
import '../../../utilities/util.dart';
|
import '../../../utilities/util.dart';
|
||||||
import '../../../wallets/crypto_currency/coins/ethereum.dart';
|
import '../../../wallets/crypto_currency/crypto_currency.dart';
|
||||||
|
import '../../../wallets/crypto_currency/intermediate/bip39_hd_currency.dart';
|
||||||
|
import '../../../wallets/isar/models/wallet_info.dart';
|
||||||
import '../../../wallets/isar/providers/wallet_info_provider.dart';
|
import '../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||||
|
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
|
||||||
|
import '../../../wallets/wallet/impl/monero_wallet.dart';
|
||||||
|
import '../../../wallets/wallet/impl/wownero_wallet.dart';
|
||||||
|
import '../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||||
import '../../../wallets/wallet/wallet.dart';
|
import '../../../wallets/wallet/wallet.dart';
|
||||||
|
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||||
|
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../widgets/desktop/desktop_app_bar.dart';
|
import '../../../widgets/desktop/desktop_app_bar.dart';
|
||||||
import '../../../widgets/desktop/desktop_scaffold.dart';
|
import '../../../widgets/desktop/desktop_scaffold.dart';
|
||||||
|
import '../../../widgets/stack_dialog.dart';
|
||||||
import '../../home_view/home_view.dart';
|
import '../../home_view/home_view.dart';
|
||||||
import '../add_token_view/edit_wallet_tokens_view.dart';
|
import '../add_token_view/edit_wallet_tokens_view.dart';
|
||||||
import '../new_wallet_options/new_wallet_options_view.dart';
|
import '../new_wallet_options/new_wallet_options_view.dart';
|
||||||
|
@ -64,46 +79,25 @@ class _VerifyRecoveryPhraseViewState
|
||||||
extends ConsumerState<VerifyRecoveryPhraseView>
|
extends ConsumerState<VerifyRecoveryPhraseView>
|
||||||
// with WidgetsBindingObserver
|
// with WidgetsBindingObserver
|
||||||
{
|
{
|
||||||
late Wallet _wallet;
|
late String _walletId;
|
||||||
|
late CryptoCurrency _coin;
|
||||||
late List<String> _mnemonic;
|
late List<String> _mnemonic;
|
||||||
late final bool isDesktop;
|
late final bool isDesktop;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_wallet = widget.wallet;
|
_walletId = widget.wallet.walletId;
|
||||||
|
_coin = widget.wallet.cryptoCurrency;
|
||||||
_mnemonic = widget.mnemonic;
|
_mnemonic = widget.mnemonic;
|
||||||
isDesktop = Util.isDesktop;
|
isDesktop = Util.isDesktop;
|
||||||
// WidgetsBinding.instance?.addObserver(this);
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
dispose() {
|
dispose() {
|
||||||
// WidgetsBinding.instance?.removeObserver(this);
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @override
|
|
||||||
// void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
||||||
// switch (state) {
|
|
||||||
// case AppLifecycleState.inactive:
|
|
||||||
// debugPrint(
|
|
||||||
// "VerifyRecoveryPhraseView ========================= Inactive");
|
|
||||||
// break;
|
|
||||||
// case AppLifecycleState.paused:
|
|
||||||
// debugPrint("VerifyRecoveryPhraseView ========================= Paused");
|
|
||||||
// break;
|
|
||||||
// case AppLifecycleState.resumed:
|
|
||||||
// debugPrint(
|
|
||||||
// "VerifyRecoveryPhraseView ========================= Resumed");
|
|
||||||
// break;
|
|
||||||
// case AppLifecycleState.detached:
|
|
||||||
// debugPrint(
|
|
||||||
// "VerifyRecoveryPhraseView ========================= Detached");
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<bool> _verifyMnemonicPassphrase() async {
|
Future<bool> _verifyMnemonicPassphrase() async {
|
||||||
final result = await showDialog<String?>(
|
final result = await showDialog<String?>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -113,6 +107,153 @@ class _VerifyRecoveryPhraseViewState
|
||||||
return result == "verified";
|
return result == "verified";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _convertToViewOnly() async {
|
||||||
|
int height = 0;
|
||||||
|
final Map<String, dynamic> otherDataJson = {
|
||||||
|
WalletInfoKeys.isViewOnlyKey: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
final ViewOnlyWalletType viewOnlyWalletType;
|
||||||
|
if (widget.wallet is ExtendedKeysInterface) {
|
||||||
|
if (widget.wallet.cryptoCurrency is Firo) {
|
||||||
|
otherDataJson.addAll(
|
||||||
|
{
|
||||||
|
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
||||||
|
WalletInfoKeys.enableLelantusScanning: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
viewOnlyWalletType = ViewOnlyWalletType.xPub;
|
||||||
|
} else if (widget.wallet is LibMoneroWallet) {
|
||||||
|
if (widget.wallet.cryptoCurrency is Monero) {
|
||||||
|
height = cs_monero_deprecated.getMoneroHeightByDate(
|
||||||
|
date: DateTime.now().subtract(const Duration(days: 7)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widget.wallet.cryptoCurrency is Wownero) {
|
||||||
|
height = cs_monero_deprecated.getWowneroHeightByDate(
|
||||||
|
date: DateTime.now().subtract(const Duration(days: 7)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (height < 0) height = 0;
|
||||||
|
|
||||||
|
viewOnlyWalletType = ViewOnlyWalletType.cryptonote;
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
"Unsupported view only wallet type found: ${widget.wallet.runtimeType}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
otherDataJson[WalletInfoKeys.viewOnlyTypeIndexKey] =
|
||||||
|
viewOnlyWalletType.index;
|
||||||
|
|
||||||
|
final voInfo = WalletInfo.createNew(
|
||||||
|
coin: _coin,
|
||||||
|
name: widget.wallet.info.name,
|
||||||
|
restoreHeight: height,
|
||||||
|
otherDataJsonString: jsonEncode(otherDataJson),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ViewOnlyWalletData viewOnlyData;
|
||||||
|
if (widget.wallet is ExtendedKeysInterface) {
|
||||||
|
final extendedKeyInfo =
|
||||||
|
await (widget.wallet as ExtendedKeysInterface).getXPubs();
|
||||||
|
final testPath = (_coin as Bip39HDCurrency).constructDerivePath(
|
||||||
|
derivePathType: (_coin as Bip39HDCurrency).defaultDerivePathType,
|
||||||
|
chain: 0,
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
XPub? xPub;
|
||||||
|
for (final pub in extendedKeyInfo.xpubs) {
|
||||||
|
if (testPath.startsWith(pub.path)) {
|
||||||
|
xPub = pub;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xPub == null) {
|
||||||
|
throw Exception("Default derivation path not matched in xPubs");
|
||||||
|
}
|
||||||
|
|
||||||
|
viewOnlyData = ExtendedKeysViewOnlyWalletData(
|
||||||
|
walletId: voInfo.walletId,
|
||||||
|
xPubs: [xPub],
|
||||||
|
);
|
||||||
|
} else if (widget.wallet is LibMoneroWallet) {
|
||||||
|
final w = widget.wallet as LibMoneroWallet;
|
||||||
|
|
||||||
|
final info = await w
|
||||||
|
.hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing();
|
||||||
|
final address = info.$1;
|
||||||
|
final privateViewKey = info.$2;
|
||||||
|
|
||||||
|
viewOnlyData = CryptonoteViewOnlyWalletData(
|
||||||
|
walletId: voInfo.walletId,
|
||||||
|
address: address,
|
||||||
|
privateViewKey: privateViewKey,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw Exception(
|
||||||
|
"Unsupported view only wallet type found: ${widget.wallet.runtimeType}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final voWallet = await Wallet.create(
|
||||||
|
walletInfo: voInfo,
|
||||||
|
mainDB: ref.read(mainDBProvider),
|
||||||
|
secureStorageInterface: ref.read(secureStoreProvider),
|
||||||
|
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||||
|
prefs: ref.read(prefsChangeNotifierProvider),
|
||||||
|
viewOnlyData: viewOnlyData,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: extract interface with isRestore param
|
||||||
|
switch (voWallet.runtimeType) {
|
||||||
|
case const (EpiccashWallet):
|
||||||
|
await (voWallet as EpiccashWallet).init(isRestore: true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case const (MoneroWallet):
|
||||||
|
await (voWallet as MoneroWallet).init(isRestore: true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case const (WowneroWallet):
|
||||||
|
await (voWallet as WowneroWallet).init(isRestore: true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
await voWallet.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
await voWallet.recover(isRescan: false);
|
||||||
|
|
||||||
|
// don't remove this setMnemonicVerified thing
|
||||||
|
await voWallet.info.setMnemonicVerified(
|
||||||
|
isar: ref.read(mainDBProvider).isar,
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.read(pWallets).addWallet(voWallet);
|
||||||
|
|
||||||
|
await ref.read(pWallets).deleteWallet(
|
||||||
|
widget.wallet.info,
|
||||||
|
ref.read(secureStoreProvider),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
await ref.read(pWallets).deleteWallet(
|
||||||
|
widget.wallet.info,
|
||||||
|
ref.read(secureStoreProvider),
|
||||||
|
);
|
||||||
|
await ref.read(pWallets).deleteWallet(
|
||||||
|
voWallet.info,
|
||||||
|
ref.read(secureStoreProvider),
|
||||||
|
);
|
||||||
|
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _continue(bool isMatch) async {
|
Future<void> _continue(bool isMatch) async {
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
if (ref.read(pNewWalletOptions) != null &&
|
if (ref.read(pNewWalletOptions) != null &&
|
||||||
|
@ -124,11 +265,11 @@ class _VerifyRecoveryPhraseViewState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ref.read(pWalletInfo(_wallet.walletId)).setMnemonicVerified(
|
await ref.read(pWalletInfo(widget.wallet.walletId)).setMnemonicVerified(
|
||||||
isar: ref.read(mainDBProvider).isar,
|
isar: ref.read(mainDBProvider).isar,
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.read(pWallets).addWallet(_wallet);
|
ref.read(pWallets).addWallet(widget.wallet);
|
||||||
|
|
||||||
final isCreateSpecialEthWallet =
|
final isCreateSpecialEthWallet =
|
||||||
ref.read(createSpecialEthWalletRoutingFlag);
|
ref.read(createSpecialEthWalletRoutingFlag);
|
||||||
|
@ -142,6 +283,51 @@ class _VerifyRecoveryPhraseViewState
|
||||||
.state;
|
.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted &&
|
||||||
|
ref.read(pNewWalletOptions)?.convertToViewOnly == true &&
|
||||||
|
widget.wallet is ViewOnlyOptionInterface) {
|
||||||
|
try {
|
||||||
|
Exception? ex;
|
||||||
|
await showLoading(
|
||||||
|
whileFuture: _convertToViewOnly(),
|
||||||
|
context: context,
|
||||||
|
message: "Converting to view only wallet",
|
||||||
|
rootNavigator: Util.isDesktop,
|
||||||
|
onException: (e) {
|
||||||
|
ex = e;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ex != null) {
|
||||||
|
throw ex!;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$e\n$s",
|
||||||
|
level: LogLevel.Fatal,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => StackOkDialog(
|
||||||
|
title: e.toString(),
|
||||||
|
desktopPopRootNavigator: Util.isDesktop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).popUntil(
|
||||||
|
ModalRoute.withName(
|
||||||
|
NewWalletRecoveryPhraseView.routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
if (isCreateSpecialEthWallet) {
|
if (isCreateSpecialEthWallet) {
|
||||||
|
@ -156,7 +342,7 @@ class _VerifyRecoveryPhraseViewState
|
||||||
DesktopHomeView.routeName,
|
DesktopHomeView.routeName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (widget.wallet.info.coin is Ethereum) {
|
if (_coin is Ethereum) {
|
||||||
unawaited(
|
unawaited(
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
EditWalletTokensView.routeName,
|
EditWalletTokensView.routeName,
|
||||||
|
@ -179,7 +365,7 @@ class _VerifyRecoveryPhraseViewState
|
||||||
(route) => false,
|
(route) => false,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (widget.wallet.info.coin is Ethereum) {
|
if (_coin is Ethereum) {
|
||||||
unawaited(
|
unawaited(
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
EditWalletTokensView.routeName,
|
EditWalletTokensView.routeName,
|
||||||
|
@ -269,7 +455,7 @@ class _VerifyRecoveryPhraseViewState
|
||||||
|
|
||||||
Future<void> delete() async {
|
Future<void> delete() async {
|
||||||
await ref.read(pWallets).deleteWallet(
|
await ref.read(pWallets).deleteWallet(
|
||||||
_wallet.info,
|
widget.wallet.info,
|
||||||
ref.read(secureStoreProvider),
|
ref.read(secureStoreProvider),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -299,7 +485,7 @@ class _VerifyRecoveryPhraseViewState
|
||||||
trailing: ExitToMyStackButton(
|
trailing: ExitToMyStackButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await delete();
|
await delete();
|
||||||
if (mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).popUntil(
|
Navigator.of(context).popUntil(
|
||||||
ModalRoute.withName(DesktopHomeView.routeName),
|
ModalRoute.withName(DesktopHomeView.routeName),
|
||||||
);
|
);
|
||||||
|
|
|
@ -148,6 +148,7 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final coin = ref.watch(pWalletCoin(widget.walletId));
|
final coin = ref.watch(pWalletCoin(widget.walletId));
|
||||||
|
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
||||||
return ConditionalParent(
|
return ConditionalParent(
|
||||||
condition: !isDesktop,
|
condition: !isDesktop,
|
||||||
builder: (child) => Background(
|
builder: (child) => Background(
|
||||||
|
@ -383,13 +384,11 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
|
||||||
detail: address.zSafeFrost.toString(),
|
detail: address.zSafeFrost.toString(),
|
||||||
button: Container(),
|
button: Container(),
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is Bip39HDWallet && !wallet.isViewOnly)
|
||||||
is Bip39HDWallet)
|
|
||||||
const _Div(
|
const _Div(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is Bip39HDWallet && !wallet.isViewOnly)
|
||||||
is Bip39HDWallet)
|
|
||||||
AddressPrivateKey(
|
AddressPrivateKey(
|
||||||
walletId: widget.walletId,
|
walletId: widget.walletId,
|
||||||
address: address,
|
address: address,
|
||||||
|
|
|
@ -18,6 +18,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import '../../models/isar/models/isar_models.dart';
|
import '../../models/isar/models/isar_models.dart';
|
||||||
|
import '../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../notifications/show_flush_bar.dart';
|
import '../../notifications/show_flush_bar.dart';
|
||||||
import '../../providers/db/main_db_provider.dart';
|
import '../../providers/db/main_db_provider.dart';
|
||||||
import '../../providers/providers.dart';
|
import '../../providers/providers.dart';
|
||||||
|
@ -36,6 +37,7 @@ import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||||
import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
|
import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
|
||||||
import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||||
|
import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../widgets/background.dart';
|
import '../../widgets/background.dart';
|
||||||
import '../../widgets/conditional_parent.dart';
|
import '../../widgets/conditional_parent.dart';
|
||||||
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
|
@ -189,6 +191,16 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
clipboard = widget.clipboard;
|
clipboard = widget.clipboard;
|
||||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||||
_supportsSpark = wallet is SparkInterface;
|
_supportsSpark = wallet is SparkInterface;
|
||||||
|
|
||||||
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
|
_showMultiType = false;
|
||||||
|
} else {
|
||||||
|
_showMultiType = _supportsSpark ||
|
||||||
|
(wallet is! BCashInterface &&
|
||||||
|
wallet is Bip39HDWallet &&
|
||||||
|
wallet.supportedAddressTypes.length > 1);
|
||||||
|
}
|
||||||
|
|
||||||
_showMultiType = _supportsSpark ||
|
_showMultiType = _supportsSpark ||
|
||||||
(wallet is! BCashInterface &&
|
(wallet is! BCashInterface &&
|
||||||
wallet is Bip39HDWallet &&
|
wallet is Bip39HDWallet &&
|
||||||
|
@ -265,6 +277,18 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
address = ref.watch(pWalletReceivingAddress(walletId));
|
address = ref.watch(pWalletReceivingAddress(walletId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final wallet =
|
||||||
|
ref.watch(pWallets.select((value) => value.getWallet(walletId)));
|
||||||
|
|
||||||
|
final bool canGen;
|
||||||
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||||
|
canGen = false;
|
||||||
|
} else {
|
||||||
|
canGen = (wallet is MultiAddressInterface || _supportsSpark);
|
||||||
|
}
|
||||||
|
|
||||||
return Background(
|
return Background(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
@ -553,17 +577,11 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (ref.watch(
|
if (canGen)
|
||||||
pWallets.select((value) => value.getWallet(walletId)),
|
|
||||||
) is MultiAddressInterface ||
|
|
||||||
_supportsSpark)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 12,
|
height: 12,
|
||||||
),
|
),
|
||||||
if (ref.watch(
|
if (canGen)
|
||||||
pWallets.select((value) => value.getWallet(walletId)),
|
|
||||||
) is MultiAddressInterface ||
|
|
||||||
_supportsSpark)
|
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
label: "Generate new address",
|
label: "Generate new address",
|
||||||
onPressed: _supportsSpark &&
|
onPressed: _supportsSpark &&
|
||||||
|
|
|
@ -27,6 +27,7 @@ import '../../../../../models/exchange/change_now/exchange_transaction.dart';
|
||||||
import '../../../../../models/exchange/response_objects/trade.dart';
|
import '../../../../../models/exchange/response_objects/trade.dart';
|
||||||
import '../../../../../models/isar/models/contact_entry.dart';
|
import '../../../../../models/isar/models/contact_entry.dart';
|
||||||
import '../../../../../models/isar/models/transaction_note.dart';
|
import '../../../../../models/isar/models/transaction_note.dart';
|
||||||
|
import '../../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../../models/node_model.dart';
|
import '../../../../../models/node_model.dart';
|
||||||
import '../../../../../models/stack_restoring_ui_state.dart';
|
import '../../../../../models/stack_restoring_ui_state.dart';
|
||||||
import '../../../../../models/trade_wallet_lookup.dart';
|
import '../../../../../models/trade_wallet_lookup.dart';
|
||||||
|
@ -58,6 +59,7 @@ import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart';
|
||||||
import '../../../../../wallets/wallet/wallet.dart';
|
import '../../../../../wallets/wallet/wallet.dart';
|
||||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||||
import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
|
import '../../../../../wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
|
||||||
|
import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
|
|
||||||
class PreRestoreState {
|
class PreRestoreState {
|
||||||
final Set<String> walletIds;
|
final Set<String> walletIds;
|
||||||
|
@ -312,7 +314,10 @@ abstract class SWB {
|
||||||
backupWallet['isFavorite'] = wallet.info.isFavourite;
|
backupWallet['isFavorite'] = wallet.info.isFavourite;
|
||||||
backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;
|
backupWallet['otherDataJsonString'] = wallet.info.otherDataJsonString;
|
||||||
|
|
||||||
if (wallet is MnemonicInterface) {
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
|
backupWallet['viewOnlyWalletDataKey'] =
|
||||||
|
(await wallet.getViewOnlyWalletData()).toJsonEncodedString();
|
||||||
|
} else if (wallet is MnemonicInterface) {
|
||||||
backupWallet['mnemonic'] = await wallet.getMnemonic();
|
backupWallet['mnemonic'] = await wallet.getMnemonic();
|
||||||
backupWallet['mnemonicPassphrase'] =
|
backupWallet['mnemonicPassphrase'] =
|
||||||
await wallet.getMnemonicPassphrase();
|
await wallet.getMnemonicPassphrase();
|
||||||
|
@ -419,7 +424,16 @@ abstract class SWB {
|
||||||
|
|
||||||
String? mnemonic, mnemonicPassphrase, privateKey;
|
String? mnemonic, mnemonicPassphrase, privateKey;
|
||||||
|
|
||||||
if (walletbackup['mnemonic'] == null) {
|
ViewOnlyWalletData? viewOnlyData;
|
||||||
|
if (info.isViewOnly) {
|
||||||
|
final viewOnlyDataEncoded =
|
||||||
|
walletbackup['viewOnlyWalletDataKey'] as String;
|
||||||
|
|
||||||
|
viewOnlyData = ViewOnlyWalletData.fromJsonEncodedString(
|
||||||
|
viewOnlyDataEncoded,
|
||||||
|
walletId: info.walletId,
|
||||||
|
);
|
||||||
|
} else if (walletbackup['mnemonic'] == null) {
|
||||||
// probably private key based
|
// probably private key based
|
||||||
if (walletbackup['privateKey'] != null) {
|
if (walletbackup['privateKey'] != null) {
|
||||||
privateKey = walletbackup['privateKey'] as String;
|
privateKey = walletbackup['privateKey'] as String;
|
||||||
|
@ -486,6 +500,7 @@ abstract class SWB {
|
||||||
mnemonic: mnemonic,
|
mnemonic: mnemonic,
|
||||||
mnemonicPassphrase: mnemonicPassphrase,
|
mnemonicPassphrase: mnemonicPassphrase,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
|
viewOnlyData: viewOnlyData,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {
|
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {
|
||||||
|
|
|
@ -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 '../../../../app_config.dart';
|
||||||
import '../../../../models/keys/cw_key_data.dart';
|
import '../../../../models/keys/cw_key_data.dart';
|
||||||
import '../../../../models/keys/key_data_interface.dart';
|
import '../../../../models/keys/key_data_interface.dart';
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../models/keys/xpriv_data.dart';
|
import '../../../../models/keys/xpriv_data.dart';
|
||||||
import '../../../../notifications/show_flush_bar.dart';
|
import '../../../../notifications/show_flush_bar.dart';
|
||||||
import '../../../../themes/stack_colors.dart';
|
import '../../../../themes/stack_colors.dart';
|
||||||
|
@ -39,6 +40,7 @@ import '../../../../widgets/rounded_white_container.dart';
|
||||||
import '../../../../widgets/stack_dialog.dart';
|
import '../../../../widgets/stack_dialog.dart';
|
||||||
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||||
import '../../../wallet_view/transaction_views/transaction_details_view.dart';
|
import '../../../wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
import '../../sub_widgets/view_only_wallet_data_widget.dart';
|
||||||
import 'cn_wallet_keys.dart';
|
import 'cn_wallet_keys.dart';
|
||||||
import 'wallet_xprivs.dart';
|
import 'wallet_xprivs.dart';
|
||||||
|
|
||||||
|
@ -426,7 +428,7 @@ class _FrostKeys extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MobileKeyDataView extends StatelessWidget {
|
class MobileKeyDataView extends ConsumerWidget {
|
||||||
const MobileKeyDataView({
|
const MobileKeyDataView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.walletId,
|
required this.walletId,
|
||||||
|
@ -441,7 +443,7 @@ class MobileKeyDataView extends StatelessWidget {
|
||||||
final KeyDataInterface keyData;
|
final KeyDataInterface keyData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Background(
|
return Background(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
@ -455,6 +457,7 @@ class MobileKeyDataView extends StatelessWidget {
|
||||||
"Wallet ${switch (keyData.runtimeType) {
|
"Wallet ${switch (keyData.runtimeType) {
|
||||||
const (XPrivData) => "xpriv(s)",
|
const (XPrivData) => "xpriv(s)",
|
||||||
const (CWKeyData) => "keys",
|
const (CWKeyData) => "keys",
|
||||||
|
const (ViewOnlyWalletData) => "keys",
|
||||||
_ => throw UnimplementedError(
|
_ => throw UnimplementedError(
|
||||||
"Don't forget to add your KeyDataInterface here!",
|
"Don't forget to add your KeyDataInterface here!",
|
||||||
),
|
),
|
||||||
|
@ -483,6 +486,10 @@ class MobileKeyDataView extends StatelessWidget {
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
cwKeyData: keyData as CWKeyData,
|
cwKeyData: keyData as CWKeyData,
|
||||||
),
|
),
|
||||||
|
const (ViewOnlyWalletData) =>
|
||||||
|
ViewOnlyWalletDataWidget(
|
||||||
|
data: keyData as ViewOnlyWalletData,
|
||||||
|
),
|
||||||
_ => throw UnimplementedError(
|
_ => throw UnimplementedError(
|
||||||
"Don't forget to add your KeyDataInterface here!",
|
"Don't forget to add your KeyDataInterface here!",
|
||||||
),
|
),
|
||||||
|
|
|
@ -49,7 +49,7 @@ class WalletXPrivsState extends ConsumerState<WalletXPrivs> {
|
||||||
late String _currentDropDownValue;
|
late String _currentDropDownValue;
|
||||||
|
|
||||||
String _current(String key) =>
|
String _current(String key) =>
|
||||||
widget.xprivData.xprivs.firstWhere((e) => e.path == key).xpriv;
|
widget.xprivData.xprivs.firstWhere((e) => e.path == key).encoded;
|
||||||
|
|
||||||
Future<void> _copy() async {
|
Future<void> _copy() async {
|
||||||
await widget.clipboardInterface.setData(
|
await widget.clipboardInterface.setData(
|
||||||
|
|
|
@ -19,6 +19,7 @@ import '../../../db/hive/db.dart';
|
||||||
import '../../../db/sqlite/firo_cache.dart';
|
import '../../../db/sqlite/firo_cache.dart';
|
||||||
import '../../../models/epicbox_config_model.dart';
|
import '../../../models/epicbox_config_model.dart';
|
||||||
import '../../../models/keys/key_data_interface.dart';
|
import '../../../models/keys/key_data_interface.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../notifications/show_flush_bar.dart';
|
import '../../../notifications/show_flush_bar.dart';
|
||||||
import '../../../providers/global/wallets_provider.dart';
|
import '../../../providers/global/wallets_provider.dart';
|
||||||
import '../../../providers/ui/transaction_filter_provider.dart';
|
import '../../../providers/ui/transaction_filter_provider.dart';
|
||||||
|
@ -99,8 +100,13 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
||||||
void initState() {
|
void initState() {
|
||||||
walletId = widget.walletId;
|
walletId = widget.walletId;
|
||||||
coin = widget.coin;
|
coin = widget.coin;
|
||||||
xPubEnabled =
|
|
||||||
ref.read(pWallets).getWallet(walletId) is ExtendedKeysInterface;
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||||
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
|
xPubEnabled = false;
|
||||||
|
} else {
|
||||||
|
xPubEnabled = wallet is ExtendedKeysInterface;
|
||||||
|
}
|
||||||
|
|
||||||
xpub = "";
|
xpub = "";
|
||||||
|
|
||||||
|
@ -166,6 +172,15 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
debugPrint("BUILD: $runtimeType");
|
debugPrint("BUILD: $runtimeType");
|
||||||
|
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||||
|
|
||||||
|
bool canBackup = true;
|
||||||
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||||
|
canBackup = false;
|
||||||
|
}
|
||||||
|
|
||||||
return Background(
|
return Background(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
@ -248,146 +263,154 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(
|
if (canBackup)
|
||||||
height: 8,
|
const SizedBox(
|
||||||
),
|
height: 8,
|
||||||
Consumer(
|
),
|
||||||
builder: (_, ref, __) {
|
if (canBackup)
|
||||||
return SettingsListButton(
|
Consumer(
|
||||||
iconAssetName: Assets.svg.lock,
|
builder: (_, ref, __) {
|
||||||
iconSize: 16,
|
return SettingsListButton(
|
||||||
title: "Wallet backup",
|
iconAssetName: Assets.svg.lock,
|
||||||
onPressed: () async {
|
iconSize: 16,
|
||||||
final wallet = ref
|
title: "Wallet backup",
|
||||||
.read(pWallets)
|
onPressed: () async {
|
||||||
.getWallet(widget.walletId);
|
// TODO: [prio=med] take wallets that don't have a mnemonic into account
|
||||||
|
|
||||||
// TODO: [prio=med] take wallets that don't have a mnemonic into account
|
List<String>? mnemonic;
|
||||||
|
|
||||||
List<String>? mnemonic;
|
|
||||||
({
|
|
||||||
String myName,
|
|
||||||
String config,
|
|
||||||
String keys,
|
|
||||||
({
|
({
|
||||||
|
String myName,
|
||||||
String config,
|
String config,
|
||||||
String keys
|
String keys,
|
||||||
})? prevGen,
|
({
|
||||||
})? frostWalletData;
|
String config,
|
||||||
ViewOnlyWalletData? voData;
|
String keys
|
||||||
if (wallet is BitcoinFrostWallet) {
|
})? prevGen,
|
||||||
final futures = [
|
})? frostWalletData;
|
||||||
wallet.getSerializedKeys(),
|
if (wallet is BitcoinFrostWallet) {
|
||||||
wallet.getMultisigConfig(),
|
final futures = [
|
||||||
wallet.getSerializedKeysPrevGen(),
|
wallet.getSerializedKeys(),
|
||||||
wallet.getMultisigConfigPrevGen(),
|
wallet.getMultisigConfig(),
|
||||||
];
|
wallet.getSerializedKeysPrevGen(),
|
||||||
|
wallet.getMultisigConfigPrevGen(),
|
||||||
|
];
|
||||||
|
|
||||||
final results =
|
final results =
|
||||||
await Future.wait(futures);
|
await Future.wait(futures);
|
||||||
|
|
||||||
if (results.length == 4) {
|
if (results.length == 4) {
|
||||||
frostWalletData = (
|
frostWalletData = (
|
||||||
myName: wallet.frostInfo.myName,
|
myName: wallet.frostInfo.myName,
|
||||||
config: results[1]!,
|
config: results[1]!,
|
||||||
keys: results[0]!,
|
keys: results[0]!,
|
||||||
prevGen: results[2] == null ||
|
prevGen: results[2] == null ||
|
||||||
results[3] == null
|
results[3] == null
|
||||||
? null
|
? null
|
||||||
: (
|
: (
|
||||||
config: results[3]!,
|
config: results[3]!,
|
||||||
keys: results[2]!,
|
keys: results[2]!,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wallet is MnemonicInterface) {
|
||||||
|
if (wallet
|
||||||
|
is ViewOnlyOptionInterface &&
|
||||||
|
!(wallet
|
||||||
|
as ViewOnlyOptionInterface)
|
||||||
|
.isViewOnly) {
|
||||||
|
mnemonic = await wallet
|
||||||
|
.getMnemonicAsWords();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
KeyDataInterface? keyData;
|
||||||
if (wallet
|
if (wallet
|
||||||
is ViewOnlyOptionInterface &&
|
is ViewOnlyOptionInterface &&
|
||||||
wallet.isViewOnly) {
|
wallet.isViewOnly) {
|
||||||
voData = await wallet
|
keyData = await wallet
|
||||||
.getViewOnlyWalletData();
|
.getViewOnlyWalletData();
|
||||||
} else if (wallet
|
} else if (wallet
|
||||||
is MnemonicInterface) {
|
is ExtendedKeysInterface) {
|
||||||
mnemonic = await wallet
|
keyData = await wallet.getXPrivs();
|
||||||
.getMnemonicAsWords();
|
} else if (wallet
|
||||||
|
is LibMoneroWallet) {
|
||||||
|
keyData = await wallet.getKeys();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
KeyDataInterface? keyData;
|
if (context.mounted) {
|
||||||
if (wallet is ExtendedKeysInterface) {
|
if (keyData != null &&
|
||||||
keyData = await wallet.getXPrivs();
|
wallet
|
||||||
} else if (wallet is LibMoneroWallet) {
|
is ViewOnlyOptionInterface) {
|
||||||
keyData = await wallet.getKeys();
|
await Navigator.push(
|
||||||
}
|
context,
|
||||||
|
RouteGenerator.getRoute(
|
||||||
if (context.mounted) {
|
shouldUseMaterialRoute:
|
||||||
if (voData != null) {
|
RouteGenerator
|
||||||
await Navigator.push(
|
.useMaterialPageRoute,
|
||||||
context,
|
builder: (_) =>
|
||||||
RouteGenerator.getRoute(
|
LockscreenView(
|
||||||
shouldUseMaterialRoute:
|
routeOnSuccessArguments: (
|
||||||
RouteGenerator
|
walletId: walletId,
|
||||||
.useMaterialPageRoute,
|
keyData: keyData,
|
||||||
builder: (_) => LockscreenView(
|
),
|
||||||
routeOnSuccessArguments: (
|
showBackButton: true,
|
||||||
walletId: walletId,
|
routeOnSuccess:
|
||||||
keyData: keyData,
|
MobileKeyDataView
|
||||||
|
.routeName,
|
||||||
|
biometricsCancelButtonString:
|
||||||
|
"CANCEL",
|
||||||
|
biometricsLocalizedReason:
|
||||||
|
"Authenticate to view recovery data",
|
||||||
|
biometricsAuthenticationTitle:
|
||||||
|
"View recovery data",
|
||||||
),
|
),
|
||||||
showBackButton: true,
|
settings: const RouteSettings(
|
||||||
routeOnSuccess:
|
name:
|
||||||
MobileKeyDataView
|
"/viewRecoveryDataLockscreen",
|
||||||
.routeName,
|
|
||||||
biometricsCancelButtonString:
|
|
||||||
"CANCEL",
|
|
||||||
biometricsLocalizedReason:
|
|
||||||
"Authenticate to view recovery data",
|
|
||||||
biometricsAuthenticationTitle:
|
|
||||||
"View recovery data",
|
|
||||||
),
|
|
||||||
settings: const RouteSettings(
|
|
||||||
name:
|
|
||||||
"/viewRecoveryDataLockscreen",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
RouteGenerator.getRoute(
|
|
||||||
shouldUseMaterialRoute:
|
|
||||||
RouteGenerator
|
|
||||||
.useMaterialPageRoute,
|
|
||||||
builder: (_) => LockscreenView(
|
|
||||||
routeOnSuccessArguments: (
|
|
||||||
walletId: walletId,
|
|
||||||
mnemonic: mnemonic ?? [],
|
|
||||||
frostWalletData:
|
|
||||||
frostWalletData,
|
|
||||||
keyData: keyData,
|
|
||||||
),
|
),
|
||||||
showBackButton: true,
|
|
||||||
routeOnSuccess:
|
|
||||||
WalletBackupView
|
|
||||||
.routeName,
|
|
||||||
biometricsCancelButtonString:
|
|
||||||
"CANCEL",
|
|
||||||
biometricsLocalizedReason:
|
|
||||||
"Authenticate to view recovery phrase",
|
|
||||||
biometricsAuthenticationTitle:
|
|
||||||
"View recovery phrase",
|
|
||||||
),
|
),
|
||||||
settings: const RouteSettings(
|
);
|
||||||
name:
|
} else {
|
||||||
"/viewRecoverPhraseLockscreen",
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
RouteGenerator.getRoute(
|
||||||
|
shouldUseMaterialRoute:
|
||||||
|
RouteGenerator
|
||||||
|
.useMaterialPageRoute,
|
||||||
|
builder: (_) =>
|
||||||
|
LockscreenView(
|
||||||
|
routeOnSuccessArguments: (
|
||||||
|
walletId: walletId,
|
||||||
|
mnemonic: mnemonic ?? [],
|
||||||
|
frostWalletData:
|
||||||
|
frostWalletData,
|
||||||
|
keyData: keyData,
|
||||||
|
),
|
||||||
|
showBackButton: true,
|
||||||
|
routeOnSuccess:
|
||||||
|
WalletBackupView
|
||||||
|
.routeName,
|
||||||
|
biometricsCancelButtonString:
|
||||||
|
"CANCEL",
|
||||||
|
biometricsLocalizedReason:
|
||||||
|
"Authenticate to view recovery phrase",
|
||||||
|
biometricsAuthenticationTitle:
|
||||||
|
"View recovery phrase",
|
||||||
|
),
|
||||||
|
settings: const RouteSettings(
|
||||||
|
name:
|
||||||
|
"/viewRecoverPhraseLockscreen",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../app_config.dart';
|
import '../../../../app_config.dart';
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart';
|
import '../../../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart';
|
||||||
import '../../../../providers/global/secure_store_provider.dart';
|
import '../../../../providers/global/secure_store_provider.dart';
|
||||||
import '../../../../providers/global/wallets_provider.dart';
|
import '../../../../providers/global/wallets_provider.dart';
|
||||||
|
@ -10,16 +11,13 @@ import '../../../../themes/stack_colors.dart';
|
||||||
import '../../../../utilities/text_styles.dart';
|
import '../../../../utilities/text_styles.dart';
|
||||||
import '../../../../utilities/util.dart';
|
import '../../../../utilities/util.dart';
|
||||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
|
||||||
import '../../../../widgets/conditional_parent.dart';
|
import '../../../../widgets/conditional_parent.dart';
|
||||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
|
|
||||||
import '../../../../widgets/desktop/primary_button.dart';
|
import '../../../../widgets/desktop/primary_button.dart';
|
||||||
import '../../../../widgets/detail_item.dart';
|
|
||||||
import '../../../../widgets/rounded_white_container.dart';
|
import '../../../../widgets/rounded_white_container.dart';
|
||||||
import '../../../../widgets/stack_dialog.dart';
|
import '../../../../widgets/stack_dialog.dart';
|
||||||
import '../../../home_view/home_view.dart';
|
import '../../../home_view/home_view.dart';
|
||||||
import '../../../wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart';
|
import '../../sub_widgets/view_only_wallet_data_widget.dart';
|
||||||
|
|
||||||
class DeleteViewOnlyWalletKeysView extends ConsumerStatefulWidget {
|
class DeleteViewOnlyWalletKeysView extends ConsumerStatefulWidget {
|
||||||
const DeleteViewOnlyWalletKeysView({
|
const DeleteViewOnlyWalletKeysView({
|
||||||
|
@ -161,34 +159,9 @@ class _DeleteViewOnlyWalletKeysViewState
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 24,
|
height: 24,
|
||||||
),
|
),
|
||||||
if (widget.data.address != null)
|
ViewOnlyWalletDataWidget(
|
||||||
DetailItem(
|
data: widget.data,
|
||||||
title: "Address",
|
),
|
||||||
detail: widget.data.address!,
|
|
||||||
button: Util.isDesktop
|
|
||||||
? IconCopyButton(
|
|
||||||
data: widget.data.address!,
|
|
||||||
)
|
|
||||||
: SimpleCopyButton(
|
|
||||||
data: widget.data.address!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.data.address != null)
|
|
||||||
const SizedBox(
|
|
||||||
height: 16,
|
|
||||||
),
|
|
||||||
if (widget.data.privateViewKey != null)
|
|
||||||
DetailItem(
|
|
||||||
title: "Private view key",
|
|
||||||
detail: widget.data.privateViewKey!,
|
|
||||||
button: Util.isDesktop
|
|
||||||
? IconCopyButton(
|
|
||||||
data: widget.data.privateViewKey!,
|
|
||||||
)
|
|
||||||
: SimpleCopyButton(
|
|
||||||
data: widget.data.privateViewKey!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!Util.isDesktop) const Spacer(),
|
if (!Util.isDesktop) const Spacer(),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../app_config.dart';
|
import '../../../../app_config.dart';
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../providers/providers.dart';
|
import '../../../../providers/providers.dart';
|
||||||
import '../../../../themes/stack_colors.dart';
|
import '../../../../themes/stack_colors.dart';
|
||||||
import '../../../../utilities/text_styles.dart';
|
import '../../../../utilities/text_styles.dart';
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../providers/db/main_db_provider.dart';
|
import '../../../../providers/db/main_db_provider.dart';
|
||||||
import '../../../../providers/providers.dart';
|
import '../../../../providers/providers.dart';
|
||||||
import '../../../../route_generator.dart';
|
import '../../../../route_generator.dart';
|
||||||
|
@ -23,6 +24,7 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.da
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||||
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../../../widgets/background.dart';
|
import '../../../../widgets/background.dart';
|
||||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
|
||||||
|
@ -133,6 +135,12 @@ class _WalletSettingsWalletSettingsViewState
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
|
||||||
|
|
||||||
|
final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly;
|
||||||
|
|
||||||
return Background(
|
return Background(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
@ -189,13 +197,11 @@ class _WalletSettingsWalletSettingsViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is RbfInterface)
|
||||||
is RbfInterface)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is RbfInterface)
|
||||||
is RbfInterface)
|
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
|
@ -227,13 +233,11 @@ class _WalletSettingsWalletSettingsViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
|
||||||
is MultiAddressInterface)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is MultiAddressInterface && !isViewOnlyNoAddressGen)
|
||||||
is MultiAddressInterface)
|
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
|
@ -278,13 +282,11 @@ class _WalletSettingsWalletSettingsViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is LelantusInterface)
|
||||||
is LelantusInterface)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is LelantusInterface)
|
||||||
is LelantusInterface)
|
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
|
@ -316,13 +318,11 @@ class _WalletSettingsWalletSettingsViewState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is SparkInterface)
|
||||||
is SparkInterface)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
),
|
),
|
||||||
if (ref.watch(pWallets).getWallet(widget.walletId)
|
if (wallet is SparkInterface)
|
||||||
is SparkInterface)
|
|
||||||
RoundedWhiteContainer(
|
RoundedWhiteContainer(
|
||||||
padding: const EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: RawMaterialButton(
|
child: RawMaterialButton(
|
||||||
|
|
|
@ -57,7 +57,7 @@ class XPubViewState extends ConsumerState<XPubView> {
|
||||||
late String _currentDropDownValue;
|
late String _currentDropDownValue;
|
||||||
|
|
||||||
String _current(String key) =>
|
String _current(String key) =>
|
||||||
widget.xpubData.xpubs.firstWhere((e) => e.path == key).xpub;
|
widget.xpubData.xpubs.firstWhere((e) => e.path == key).encoded;
|
||||||
|
|
||||||
Future<void> _copy() async {
|
Future<void> _copy() async {
|
||||||
await widget.clipboardInterface.setData(
|
await widget.clipboardInterface.setData(
|
||||||
|
|
|
@ -186,6 +186,21 @@ class WalletSummaryInfo extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
if (ref.watch(pWalletInfo(walletId)).isViewOnly)
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: SelectableText(
|
||||||
|
"(View only)",
|
||||||
|
style: STextStyles.pageTitleH1(context).copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFavoriteCard
|
||||||
|
.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
FittedBox(
|
FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
|
|
|
@ -21,6 +21,7 @@ import 'package:isar/isar.dart';
|
||||||
import '../../../db/sqlite/firo_cache.dart';
|
import '../../../db/sqlite/firo_cache.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||||
import '../../../models/isar/models/isar_models.dart';
|
import '../../../models/isar/models/isar_models.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
|
import '../../../pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
|
||||||
import '../../../pages/token_view/my_tokens_view.dart';
|
import '../../../pages/token_view/my_tokens_view.dart';
|
||||||
import '../../../pages/wallet_view/sub_widgets/transactions_list.dart';
|
import '../../../pages/wallet_view/sub_widgets/transactions_list.dart';
|
||||||
|
@ -44,6 +45,7 @@ import '../../../utilities/wallet_tools.dart';
|
||||||
import '../../../wallets/isar/providers/wallet_info_provider.dart';
|
import '../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||||
import '../../../wallets/wallet/impl/banano_wallet.dart';
|
import '../../../wallets/wallet/impl/banano_wallet.dart';
|
||||||
import '../../../wallets/wallet/impl/firo_wallet.dart';
|
import '../../../wallets/wallet/impl/firo_wallet.dart';
|
||||||
|
import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../widgets/custom_buttons/blue_text_button.dart';
|
import '../../../widgets/custom_buttons/blue_text_button.dart';
|
||||||
import '../../../widgets/desktop/desktop_app_bar.dart';
|
import '../../../widgets/desktop/desktop_app_bar.dart';
|
||||||
|
@ -168,6 +170,11 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
||||||
|
|
||||||
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
|
final monke = wallet is BananoWallet ? wallet.getMonkeyImageBytes() : null;
|
||||||
|
|
||||||
|
// if the view only wallet watches a single address there are no keys of any kind
|
||||||
|
final showKeysButton = !(wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly);
|
||||||
|
|
||||||
return DesktopScaffold(
|
return DesktopScaffold(
|
||||||
appBar: DesktopAppBar(
|
appBar: DesktopAppBar(
|
||||||
background: Theme.of(context).extension<StackColors>()!.popupBG,
|
background: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||||
|
@ -216,6 +223,19 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (ref.watch(pWalletInfo(widget.walletId)).isViewOnly)
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
),
|
||||||
|
if (ref.watch(pWalletInfo(widget.walletId)).isViewOnly)
|
||||||
|
Text(
|
||||||
|
"(View only)",
|
||||||
|
style: STextStyles.desktopTextExtraSmall(context).copyWith(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.textFieldActiveSearchIconLeft,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (kDebugMode) const Spacer(),
|
if (kDebugMode) const Spacer(),
|
||||||
if (kDebugMode)
|
if (kDebugMode)
|
||||||
Column(
|
Column(
|
||||||
|
@ -312,12 +332,14 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
||||||
walletId: widget.walletId,
|
walletId: widget.walletId,
|
||||||
eventBus: eventBus,
|
eventBus: eventBus,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
if (showKeysButton)
|
||||||
width: 2,
|
const SizedBox(
|
||||||
),
|
width: 2,
|
||||||
WalletKeysButton(
|
),
|
||||||
walletId: widget.walletId,
|
if (showKeysButton)
|
||||||
),
|
WalletKeysButton(
|
||||||
|
walletId: widget.walletId,
|
||||||
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
|
|
|
@ -19,6 +19,7 @@ import 'package:isar/isar.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import '../../../../models/isar/models/isar_models.dart';
|
import '../../../../models/isar/models/isar_models.dart';
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../notifications/show_flush_bar.dart';
|
import '../../../../notifications/show_flush_bar.dart';
|
||||||
import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart';
|
import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart';
|
||||||
import '../../../../providers/db/main_db_provider.dart';
|
import '../../../../providers/db/main_db_provider.dart';
|
||||||
|
@ -40,6 +41,7 @@ import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||||
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../../../widgets/conditional_parent.dart';
|
import '../../../../widgets/conditional_parent.dart';
|
||||||
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
|
||||||
import '../../../../widgets/custom_loading_overlay.dart';
|
import '../../../../widgets/custom_loading_overlay.dart';
|
||||||
|
@ -185,10 +187,15 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
||||||
clipboard = widget.clipboard;
|
clipboard = widget.clipboard;
|
||||||
final wallet = ref.read(pWallets).getWallet(walletId);
|
final wallet = ref.read(pWallets).getWallet(walletId);
|
||||||
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
|
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
|
||||||
showMultiType = supportsSpark ||
|
|
||||||
(wallet is! BCashInterface &&
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
wallet is Bip39HDWallet &&
|
showMultiType = false;
|
||||||
wallet.supportedAddressTypes.length > 1);
|
} else {
|
||||||
|
showMultiType = supportsSpark ||
|
||||||
|
(wallet is! BCashInterface &&
|
||||||
|
wallet is Bip39HDWallet &&
|
||||||
|
wallet.supportedAddressTypes.length > 1);
|
||||||
|
}
|
||||||
|
|
||||||
_walletAddressTypes.add(wallet.info.mainAddressType);
|
_walletAddressTypes.add(wallet.info.mainAddressType);
|
||||||
|
|
||||||
|
@ -259,6 +266,18 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
||||||
address = ref.watch(pWalletReceivingAddress(walletId));
|
address = ref.watch(pWalletReceivingAddress(walletId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final wallet =
|
||||||
|
ref.watch(pWallets.select((value) => value.getWallet(walletId)));
|
||||||
|
|
||||||
|
final bool canGen;
|
||||||
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||||
|
canGen = false;
|
||||||
|
} else {
|
||||||
|
canGen = (wallet is MultiAddressInterface || supportsSpark);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
@ -430,16 +449,12 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (ref.watch(pWallets.select((value) => value.getWallet(walletId)))
|
if (canGen)
|
||||||
is MultiAddressInterface ||
|
|
||||||
supportsSpark)
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
),
|
),
|
||||||
|
|
||||||
if (ref.watch(pWallets.select((value) => value.getWallet(walletId)))
|
if (canGen)
|
||||||
is MultiAddressInterface ||
|
|
||||||
supportsSpark)
|
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
buttonHeight: ButtonHeight.l,
|
buttonHeight: ButtonHeight.l,
|
||||||
onPressed: supportsSpark &&
|
onPressed: supportsSpark &&
|
||||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
import '../../../../../app_config.dart';
|
import '../../../../../app_config.dart';
|
||||||
import '../../../../../db/sqlite/firo_cache.dart';
|
import '../../../../../db/sqlite/firo_cache.dart';
|
||||||
|
import '../../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../../providers/db/main_db_provider.dart';
|
import '../../../../../providers/db/main_db_provider.dart';
|
||||||
import '../../../../../providers/global/prefs_provider.dart';
|
import '../../../../../providers/global/prefs_provider.dart';
|
||||||
import '../../../../../providers/global/wallets_provider.dart';
|
import '../../../../../providers/global/wallets_provider.dart';
|
||||||
|
@ -240,6 +241,9 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
||||||
);
|
);
|
||||||
|
|
||||||
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
|
final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly;
|
||||||
|
final isViewOnlyNoAddressGen = wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly &&
|
||||||
|
wallet.viewOnlyType == ViewOnlyWalletType.addressOnly;
|
||||||
|
|
||||||
return DesktopDialog(
|
return DesktopDialog(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -386,40 +390,41 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// reuseAddress preference.
|
// reuseAddress preference.
|
||||||
_MoreFeaturesItemBase(
|
if (!isViewOnlyNoAddressGen)
|
||||||
onPressed: _switchReuseAddressToggled,
|
_MoreFeaturesItemBase(
|
||||||
child: Row(
|
onPressed: _switchReuseAddressToggled,
|
||||||
children: [
|
child: Row(
|
||||||
const SizedBox(width: 3),
|
children: [
|
||||||
SizedBox(
|
const SizedBox(width: 3),
|
||||||
height: 20,
|
SizedBox(
|
||||||
width: 40,
|
height: 20,
|
||||||
child: IgnorePointer(
|
width: 40,
|
||||||
child: DraggableSwitchButton(
|
child: IgnorePointer(
|
||||||
isOn: ref.watch(
|
child: DraggableSwitchButton(
|
||||||
pWalletInfo(widget.walletId)
|
isOn: ref.watch(
|
||||||
.select((value) => value.otherData),
|
pWalletInfo(widget.walletId)
|
||||||
)[WalletInfoKeys.reuseAddress] as bool? ??
|
.select((value) => value.otherData),
|
||||||
false,
|
)[WalletInfoKeys.reuseAddress] as bool? ??
|
||||||
controller: _switchController,
|
false,
|
||||||
|
controller: _switchController,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(
|
||||||
const SizedBox(
|
width: 16,
|
||||||
width: 16,
|
),
|
||||||
),
|
Column(
|
||||||
Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
"Reuse receiving address",
|
||||||
"Reuse receiving address",
|
style: STextStyles.w600_20(context),
|
||||||
style: STextStyles.w600_20(context),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 28,
|
height: 28,
|
||||||
),
|
),
|
||||||
|
|
|
@ -83,7 +83,9 @@ class _UnlockWalletKeysDesktopState
|
||||||
.verifyPassphrase(passwordController.text);
|
.verifyPassphrase(passwordController.text);
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
if (mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
|
}
|
||||||
|
|
||||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||||
({String keys, String config})? frostData;
|
({String keys, String config})? frostData;
|
||||||
|
@ -101,7 +103,8 @@ class _UnlockWalletKeysDesktopState
|
||||||
throw Exception("FIXME ~= see todo in code");
|
throw Exception("FIXME ~= see todo in code");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (wallet is ViewOnlyOptionInterface) {
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
(wallet as ViewOnlyOptionInterface).isViewOnly) {
|
||||||
// TODO: is something needed here?
|
// TODO: is something needed here?
|
||||||
} else {
|
} else {
|
||||||
words = await wallet.getMnemonicAsWords();
|
words = await wallet.getMnemonicAsWords();
|
||||||
|
@ -109,7 +112,9 @@ class _UnlockWalletKeysDesktopState
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyDataInterface? keyData;
|
KeyDataInterface? keyData;
|
||||||
if (wallet is ExtendedKeysInterface) {
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
|
keyData = await wallet.getViewOnlyWalletData();
|
||||||
|
} else if (wallet is ExtendedKeysInterface) {
|
||||||
keyData = await wallet.getXPrivs();
|
keyData = await wallet.getXPrivs();
|
||||||
} else if (wallet is LibMoneroWallet) {
|
} else if (wallet is LibMoneroWallet) {
|
||||||
keyData = await wallet.getKeys();
|
keyData = await wallet.getKeys();
|
||||||
|
@ -127,17 +132,20 @@ class _UnlockWalletKeysDesktopState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
if (mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
|
}
|
||||||
|
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 300));
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||||
|
if (mounted) {
|
||||||
unawaited(
|
unawaited(
|
||||||
showFloatingFlushBar(
|
showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Invalid passphrase!",
|
message: "Invalid passphrase!",
|
||||||
context: context,
|
context: context,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,7 +340,10 @@ class _UnlockWalletKeysDesktopState
|
||||||
.verifyPassphrase(passwordController.text);
|
.verifyPassphrase(passwordController.text);
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
if (context.mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true)
|
||||||
|
.pop();
|
||||||
|
}
|
||||||
|
|
||||||
({String keys, String config})? frostData;
|
({String keys, String config})? frostData;
|
||||||
List<String>? words;
|
List<String>? words;
|
||||||
|
@ -352,7 +363,9 @@ class _UnlockWalletKeysDesktopState
|
||||||
throw Exception("FIXME ~= see todo in code");
|
throw Exception("FIXME ~= see todo in code");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (wallet is ViewOnlyOptionInterface) {
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
(wallet as ViewOnlyOptionInterface)
|
||||||
|
.isViewOnly) {
|
||||||
// TODO: is something needed here?
|
// TODO: is something needed here?
|
||||||
} else {
|
} else {
|
||||||
words = await wallet.getMnemonicAsWords();
|
words = await wallet.getMnemonicAsWords();
|
||||||
|
@ -360,7 +373,10 @@ class _UnlockWalletKeysDesktopState
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyDataInterface? keyData;
|
KeyDataInterface? keyData;
|
||||||
if (wallet is ExtendedKeysInterface) {
|
if (wallet is ViewOnlyOptionInterface &&
|
||||||
|
wallet.isViewOnly) {
|
||||||
|
keyData = await wallet.getViewOnlyWalletData();
|
||||||
|
} else if (wallet is ExtendedKeysInterface) {
|
||||||
keyData = await wallet.getXPrivs();
|
keyData = await wallet.getXPrivs();
|
||||||
} else if (wallet is LibMoneroWallet) {
|
} else if (wallet is LibMoneroWallet) {
|
||||||
keyData = await wallet.getKeys();
|
keyData = await wallet.getKeys();
|
||||||
|
@ -379,19 +395,23 @@ class _UnlockWalletKeysDesktopState
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
if (context.mounted) {
|
||||||
|
Navigator.of(context, rootNavigator: true)
|
||||||
|
.pop();
|
||||||
|
}
|
||||||
|
|
||||||
await Future<void>.delayed(
|
await Future<void>.delayed(
|
||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
|
if (context.mounted) {
|
||||||
unawaited(
|
unawaited(
|
||||||
showFloatingFlushBar(
|
showFloatingFlushBar(
|
||||||
type: FlushBarType.warning,
|
type: FlushBarType.warning,
|
||||||
message: "Invalid passphrase!",
|
message: "Invalid passphrase!",
|
||||||
context: context,
|
context: context,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|
|
@ -16,9 +16,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../../models/keys/cw_key_data.dart';
|
import '../../../../models/keys/cw_key_data.dart';
|
||||||
import '../../../../models/keys/key_data_interface.dart';
|
import '../../../../models/keys/key_data_interface.dart';
|
||||||
|
import '../../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../../models/keys/xpriv_data.dart';
|
import '../../../../models/keys/xpriv_data.dart';
|
||||||
import '../../../../notifications/show_flush_bar.dart';
|
import '../../../../notifications/show_flush_bar.dart';
|
||||||
import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||||
|
import '../../../../pages/settings_views/sub_widgets/view_only_wallet_data_widget.dart';
|
||||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart';
|
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart';
|
||||||
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart';
|
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart';
|
||||||
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
|
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||||
|
@ -180,32 +182,39 @@ class WalletKeysDesktopPopup extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: keyData != null
|
: keyData != null
|
||||||
? CustomTabView(
|
? keyData is ViewOnlyWalletData
|
||||||
titles: [
|
? Padding(
|
||||||
if (words.isNotEmpty) "Mnemonic",
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
if (keyData is XPrivData) "XPriv(s)",
|
child: ViewOnlyWalletDataWidget(
|
||||||
if (keyData is CWKeyData) "Keys",
|
data: keyData as ViewOnlyWalletData,
|
||||||
],
|
|
||||||
children: [
|
|
||||||
if (words.isNotEmpty)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16),
|
|
||||||
child: _Mnemonic(
|
|
||||||
words: words,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (keyData is XPrivData)
|
)
|
||||||
WalletXPrivs(
|
: CustomTabView(
|
||||||
xprivData: keyData as XPrivData,
|
titles: [
|
||||||
walletId: walletId,
|
if (words.isNotEmpty) "Mnemonic",
|
||||||
),
|
if (keyData is XPrivData) "XPriv(s)",
|
||||||
if (keyData is CWKeyData)
|
if (keyData is CWKeyData) "Keys",
|
||||||
CNWalletKeys(
|
],
|
||||||
cwKeyData: keyData as CWKeyData,
|
children: [
|
||||||
walletId: walletId,
|
if (words.isNotEmpty)
|
||||||
),
|
Padding(
|
||||||
],
|
padding: const EdgeInsets.only(top: 16),
|
||||||
)
|
child: _Mnemonic(
|
||||||
|
words: words,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (keyData is XPrivData)
|
||||||
|
WalletXPrivs(
|
||||||
|
xprivData: keyData as XPrivData,
|
||||||
|
walletId: walletId,
|
||||||
|
),
|
||||||
|
if (keyData is CWKeyData)
|
||||||
|
CNWalletKeys(
|
||||||
|
cwKeyData: keyData as CWKeyData,
|
||||||
|
walletId: walletId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
: _Mnemonic(
|
: _Mnemonic(
|
||||||
words: words,
|
words: words,
|
||||||
),
|
),
|
||||||
|
|
|
@ -31,6 +31,7 @@ import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
|
||||||
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
|
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
|
||||||
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
|
||||||
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||||
|
import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import '../../../addresses/desktop_wallet_addresses_view.dart';
|
import '../../../addresses/desktop_wallet_addresses_view.dart';
|
||||||
import '../../../lelantus_coins/lelantus_coins_view.dart';
|
import '../../../lelantus_coins/lelantus_coins_view.dart';
|
||||||
import '../../../spark_coins/spark_coins_view.dart';
|
import '../../../spark_coins/spark_coins_view.dart';
|
||||||
|
@ -295,8 +296,12 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
||||||
|
|
||||||
final firoDebug = kDebugMode && (coin is Firo);
|
final firoDebug = kDebugMode && (coin is Firo);
|
||||||
|
|
||||||
final bool xpubEnabled =
|
final wallet = ref.watch(pWallets).getWallet(walletId);
|
||||||
ref.watch(pWallets).getWallet(walletId) is ExtendedKeysInterface;
|
bool xpubEnabled = wallet is ExtendedKeysInterface;
|
||||||
|
|
||||||
|
if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) {
|
||||||
|
xpubEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
final bool canChangeRep = coin is NanoCurrency;
|
final bool canChangeRep = coin is NanoCurrency;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import 'models/isar/models/contact_entry.dart';
|
||||||
import 'models/isar/models/isar_models.dart';
|
import 'models/isar/models/isar_models.dart';
|
||||||
import 'models/isar/ordinal.dart';
|
import 'models/isar/ordinal.dart';
|
||||||
import 'models/keys/key_data_interface.dart';
|
import 'models/keys/key_data_interface.dart';
|
||||||
|
import 'models/keys/view_only_wallet_data.dart';
|
||||||
import 'models/paynym/paynym_account_lite.dart';
|
import 'models/paynym/paynym_account_lite.dart';
|
||||||
import 'models/send_view_auto_fill_data.dart';
|
import 'models/send_view_auto_fill_data.dart';
|
||||||
import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
|
import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart';
|
||||||
|
@ -205,7 +206,6 @@ import 'wallets/crypto_currency/intermediate/frost_currency.dart';
|
||||||
import 'wallets/models/tx_data.dart';
|
import 'wallets/models/tx_data.dart';
|
||||||
import 'wallets/wallet/wallet.dart';
|
import 'wallets/wallet/wallet.dart';
|
||||||
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
|
||||||
import 'wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart';
|
|
||||||
import 'widgets/choose_coin_view.dart';
|
import 'widgets/choose_coin_view.dart';
|
||||||
import 'widgets/frost_scaffold.dart';
|
import 'widgets/frost_scaffold.dart';
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,9 @@ class Wallets {
|
||||||
key: Wallet.mnemonicPassphraseKey(walletId: walletId),
|
key: Wallet.mnemonicPassphraseKey(walletId: walletId),
|
||||||
);
|
);
|
||||||
await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId));
|
await secureStorage.delete(key: Wallet.privateKeyKey(walletId: walletId));
|
||||||
|
await secureStorage.delete(
|
||||||
|
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
|
||||||
|
);
|
||||||
|
|
||||||
if (info.coin is Wownero) {
|
if (info.coin is Wownero) {
|
||||||
await lib_monero_compat.deleteWalletFiles(
|
await lib_monero_compat.deleteWalletFiles(
|
||||||
|
|
|
@ -92,7 +92,7 @@ class Wownero extends CryptonoteCurrency {
|
||||||
bool get hasMnemonicPassphraseSupport => false;
|
bool get hasMnemonicPassphraseSupport => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 25];
|
List<int> get possibleMnemonicLengths => [defaultSeedPhraseLength, 16, 25];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
BigInt get satsPerCoin => BigInt.from(100000000000);
|
BigInt get satsPerCoin => BigInt.from(100000000000);
|
||||||
|
|
|
@ -5,9 +5,11 @@ import 'package:flutter/foundation.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||||
import '../../../utilities/amount/amount.dart';
|
import '../../../utilities/amount/amount.dart';
|
||||||
import '../../../utilities/enums/derive_path_type_enum.dart';
|
import '../../../utilities/enums/derive_path_type_enum.dart';
|
||||||
|
import '../interfaces/view_only_option_currency_interface.dart';
|
||||||
import 'bip39_currency.dart';
|
import 'bip39_currency.dart';
|
||||||
|
|
||||||
abstract class Bip39HDCurrency extends Bip39Currency {
|
abstract class Bip39HDCurrency extends Bip39Currency
|
||||||
|
implements ViewOnlyOptionCurrencyInterface {
|
||||||
Bip39HDCurrency(super.network);
|
Bip39HDCurrency(super.network);
|
||||||
|
|
||||||
coinlib.Network get networkParams;
|
coinlib.Network get networkParams;
|
||||||
|
@ -39,6 +41,25 @@ abstract class Bip39HDCurrency extends Bip39Currency {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> get supportedHardenedDerivationPaths {
|
||||||
|
final paths = supportedDerivationPathTypes.map(
|
||||||
|
(e) => (
|
||||||
|
path: e,
|
||||||
|
addressType: e.getAddressType(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return paths.map((e) {
|
||||||
|
final path = constructDerivePath(
|
||||||
|
derivePathType: e.path,
|
||||||
|
chain: 0,
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
// trim unhardened
|
||||||
|
return path.substring(0, path.lastIndexOf("'") + 1);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
static String convertBytesToScriptHash(Uint8List bytes) {
|
static String convertBytesToScriptHash(Uint8List bytes) {
|
||||||
final hash = sha256.convert(bytes.toList(growable: false)).toString();
|
final hash = sha256.convert(bytes.toList(growable: false)).toString();
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:uuid/uuid.dart';
|
||||||
import '../../../app_config.dart';
|
import '../../../app_config.dart';
|
||||||
import '../../../models/balance.dart';
|
import '../../../models/balance.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../crypto_currency/crypto_currency.dart';
|
import '../../crypto_currency/crypto_currency.dart';
|
||||||
import '../isar_id_interface.dart';
|
import '../isar_id_interface.dart';
|
||||||
import 'wallet_info_meta.dart';
|
import 'wallet_info_meta.dart';
|
||||||
|
@ -121,6 +122,13 @@ class WalletInfo implements IsarId {
|
||||||
bool get isViewOnly =>
|
bool get isViewOnly =>
|
||||||
otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false;
|
otherData[WalletInfoKeys.isViewOnlyKey] as bool? ?? false;
|
||||||
|
|
||||||
|
@ignore
|
||||||
|
ViewOnlyWalletType? get viewOnlyWalletType {
|
||||||
|
final index = otherData[WalletInfoKeys.viewOnlyTypeIndexKey] as int?;
|
||||||
|
if (index == null) return null;
|
||||||
|
return ViewOnlyWalletType.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> isMnemonicVerified(Isar isar) async =>
|
Future<bool> isMnemonicVerified(Isar isar) async =>
|
||||||
(await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst())
|
(await isar.walletInfoMeta.where().walletIdEqualTo(walletId).findFirst())
|
||||||
?.isMnemonicVerified ==
|
?.isMnemonicVerified ==
|
||||||
|
@ -517,4 +525,5 @@ abstract class WalletInfoKeys {
|
||||||
static const String enableOptInRbf = "enableOptInRbfKey";
|
static const String enableOptInRbf = "enableOptInRbfKey";
|
||||||
static const String reuseAddress = "reuseAddressKey";
|
static const String reuseAddress = "reuseAddressKey";
|
||||||
static const String isViewOnlyKey = "isViewOnlyKey";
|
static const String isViewOnlyKey = "isViewOnlyKey";
|
||||||
|
static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import '../isar_id_interface.dart';
|
import '../isar_id_interface.dart';
|
||||||
|
|
||||||
part 'wallet_info_meta.g.dart';
|
part 'wallet_info_meta.g.dart';
|
||||||
|
|
|
@ -6,15 +6,17 @@ import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import '../../../models/balance.dart';
|
import '../../../models/balance.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/address.dart';
|
import '../../../models/isar/models/blockchain_data/address.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../utilities/amount/amount.dart';
|
import '../../../utilities/amount/amount.dart';
|
||||||
import '../../../utilities/enums/derive_path_type_enum.dart';
|
import '../../../utilities/enums/derive_path_type_enum.dart';
|
||||||
import '../../../utilities/extensions/extensions.dart';
|
import '../../../utilities/extensions/extensions.dart';
|
||||||
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
|
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
|
||||||
import '../wallet_mixin_interfaces/multi_address_interface.dart';
|
import '../wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
|
import '../wallet_mixin_interfaces/view_only_option_interface.dart';
|
||||||
import 'bip39_wallet.dart';
|
import 'bip39_wallet.dart';
|
||||||
|
|
||||||
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
||||||
with MultiAddressInterface<T> {
|
with MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
|
||||||
Bip39HDWallet(super.cryptoCurrency);
|
Bip39HDWallet(super.cryptoCurrency);
|
||||||
|
|
||||||
Set<AddressType> get supportedAddressTypes =>
|
Set<AddressType> get supportedAddressTypes =>
|
||||||
|
@ -116,7 +118,9 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> checkSaveInitialReceivingAddress() async {
|
Future<void> checkSaveInitialReceivingAddress() async {
|
||||||
final current = await getCurrentChangeAddress();
|
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) return;
|
||||||
|
|
||||||
|
final current = await getCurrentReceivingAddress();
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
final address = await _generateAddress(
|
final address = await _generateAddress(
|
||||||
chain: 0, // receiving
|
chain: 0, // receiving
|
||||||
|
@ -174,15 +178,29 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
||||||
required int index,
|
required int index,
|
||||||
required DerivePathType derivePathType,
|
required DerivePathType derivePathType,
|
||||||
}) async {
|
}) async {
|
||||||
final root = await getRootHDNode();
|
|
||||||
|
|
||||||
final derivationPath = cryptoCurrency.constructDerivePath(
|
final derivationPath = cryptoCurrency.constructDerivePath(
|
||||||
derivePathType: derivePathType,
|
derivePathType: derivePathType,
|
||||||
chain: chain,
|
chain: chain,
|
||||||
index: index,
|
index: index,
|
||||||
);
|
);
|
||||||
|
|
||||||
final keys = root.derivePath(derivationPath);
|
final coinlib.HDKey keys;
|
||||||
|
if (isViewOnly) {
|
||||||
|
final idx = derivationPath.lastIndexOf("'/");
|
||||||
|
final path = derivationPath.substring(idx + 2);
|
||||||
|
final data =
|
||||||
|
await getViewOnlyWalletData() as ExtendedKeysViewOnlyWalletData;
|
||||||
|
|
||||||
|
final xPub = data.xPubs.firstWhere(
|
||||||
|
(e) => derivationPath.startsWith(e.path),
|
||||||
|
);
|
||||||
|
|
||||||
|
final node = coinlib.HDPublicKey.decode(xPub.encoded);
|
||||||
|
keys = node.derivePath(path);
|
||||||
|
} else {
|
||||||
|
final root = await getRootHDNode();
|
||||||
|
keys = root.derivePath(derivationPath);
|
||||||
|
}
|
||||||
|
|
||||||
final data = cryptoCurrency.getAddressForPublicKey(
|
final data = cryptoCurrency.getAddressForPublicKey(
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
|
@ -205,7 +223,8 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
|
||||||
value: convertAddressString(data.address.toString()),
|
value: convertAddressString(data.address.toString()),
|
||||||
publicKey: keys.publicKey.data,
|
publicKey: keys.publicKey.data,
|
||||||
derivationIndex: index,
|
derivationIndex: index,
|
||||||
derivationPath: DerivationPath()..value = derivationPath,
|
derivationPath:
|
||||||
|
isViewOnly ? null : (DerivationPath()..value = derivationPath),
|
||||||
type: data.addressType,
|
type: data.addressType,
|
||||||
subType: subType,
|
subType: subType,
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,6 +18,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
|
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||||
import '../../../models/keys/cw_key_data.dart';
|
import '../../../models/keys/cw_key_data.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../models/paymint/fee_object_model.dart';
|
import '../../../models/paymint/fee_object_model.dart';
|
||||||
import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
|
import '../../../services/event_bus/events/global/blocks_remaining_event.dart';
|
||||||
import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart';
|
import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart';
|
||||||
|
@ -41,7 +42,8 @@ import 'cryptonote_wallet.dart';
|
||||||
|
|
||||||
abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
extends CryptonoteWallet<T>
|
extends CryptonoteWallet<T>
|
||||||
implements MultiAddressInterface<T>, ViewOnlyOptionInterface<T> {
|
with ViewOnlyOptionInterface<T>
|
||||||
|
implements MultiAddressInterface<T> {
|
||||||
@override
|
@override
|
||||||
int get isarTransactionVersion => 2;
|
int get isarTransactionVersion => 2;
|
||||||
|
|
||||||
|
@ -91,8 +93,16 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
.walletIdEqualTo(walletId)
|
.walletIdEqualTo(walletId)
|
||||||
.watch(fireImmediately: true)
|
.watch(fireImmediately: true)
|
||||||
.listen((utxos) async {
|
.listen((utxos) async {
|
||||||
await onUTXOsChanged(utxos);
|
try {
|
||||||
await updateBalance(shouldUpdateUtxos: false);
|
await onUTXOsChanged(utxos);
|
||||||
|
await updateBalance(shouldUpdateUtxos: false);
|
||||||
|
} catch (e, s) {
|
||||||
|
lib_monero.Logging.log?.i(
|
||||||
|
"_startInit",
|
||||||
|
error: e,
|
||||||
|
stackTrace: s,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -286,6 +296,22 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<(String, String)>
|
||||||
|
hackToCreateNewViewOnlyWalletDataFromNewlyCreatedWalletThisFunctionShouldNotBeCalledUnlessYouKnowWhatYouAreDoing() async {
|
||||||
|
final path = await pathForWallet(name: walletId, type: compatType);
|
||||||
|
final String password;
|
||||||
|
try {
|
||||||
|
password = (await secureStorageInterface.read(
|
||||||
|
key: lib_monero_compat.libMoneroWalletPasswordKey(walletId),
|
||||||
|
))!;
|
||||||
|
} catch (e, s) {
|
||||||
|
throw Exception("Password not found $e, $s");
|
||||||
|
}
|
||||||
|
loadWallet(path: path, password: password);
|
||||||
|
final wallet = libMoneroWallet!;
|
||||||
|
return (wallet.getAddress().value, wallet.getPrivateViewKey());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> init({bool? isRestore}) async {
|
Future<void> init({bool? isRestore}) async {
|
||||||
final path = await pathForWallet(
|
final path = await pathForWallet(
|
||||||
|
@ -1301,19 +1327,11 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
|
|
||||||
// ============== View only ==================================================
|
// ============== View only ==================================================
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isViewOnly => info.isViewOnly;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> recoverViewOnly() async {
|
Future<void> recoverViewOnly() async {
|
||||||
await refreshMutex.protect(() async {
|
await refreshMutex.protect(() async {
|
||||||
final jsonEncodedString = await secureStorageInterface.read(
|
final data =
|
||||||
key: Wallet.getViewOnlyWalletDataSecStoreKey(
|
await getViewOnlyWalletData() as CryptonoteViewOnlyWalletData;
|
||||||
walletId: walletId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final data = ViewOnlyWalletData.fromJsonEncodedString(jsonEncodedString!);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final height = max(info.restoreHeight, 0);
|
final height = max(info.restoreHeight, 0);
|
||||||
|
@ -1338,8 +1356,8 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
final wallet = await getRestoredFromViewKeyWallet(
|
final wallet = await getRestoredFromViewKeyWallet(
|
||||||
path: path,
|
path: path,
|
||||||
password: password,
|
password: password,
|
||||||
address: data.address!,
|
address: data.address,
|
||||||
privateViewKey: data.privateViewKey!,
|
privateViewKey: data.privateViewKey,
|
||||||
height: height,
|
height: height,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1387,14 +1405,6 @@ abstract class LibMoneroWallet<T extends CryptonoteCurrency>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ViewOnlyWalletData> getViewOnlyWalletData() async {
|
|
||||||
return ViewOnlyWalletData(
|
|
||||||
address: libMoneroWallet!.getAddress().value,
|
|
||||||
privateViewKey: libMoneroWallet!.getPrivateViewKey(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============== Private ====================================================
|
// ============== Private ====================================================
|
||||||
|
|
||||||
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:mutex/mutex.dart';
|
||||||
import '../../db/isar/main_db.dart';
|
import '../../db/isar/main_db.dart';
|
||||||
import '../../models/isar/models/blockchain_data/address.dart';
|
import '../../models/isar/models/blockchain_data/address.dart';
|
||||||
import '../../models/isar/models/ethereum/eth_contract.dart';
|
import '../../models/isar/models/ethereum/eth_contract.dart';
|
||||||
|
import '../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../models/node_model.dart';
|
import '../../models/node_model.dart';
|
||||||
import '../../models/paymint/fee_object_model.dart';
|
import '../../models/paymint/fee_object_model.dart';
|
||||||
import '../../services/event_bus/events/global/node_connection_status_changed_event.dart';
|
import '../../services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||||
|
@ -161,7 +162,7 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
prefs: prefs,
|
prefs: prefs,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (wallet is ViewOnlyOptionInterface) {
|
if (wallet is ViewOnlyOptionInterface && walletInfo.isViewOnly) {
|
||||||
await secureStorageInterface.write(
|
await secureStorageInterface.write(
|
||||||
key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId),
|
key: getViewOnlyWalletDataSecStoreKey(walletId: walletInfo.walletId),
|
||||||
value: viewOnlyData!.toJsonEncodedString(),
|
value: viewOnlyData!.toJsonEncodedString(),
|
||||||
|
@ -583,6 +584,9 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
if (!tAlive) throw Exception("refresh alive ping failure");
|
if (!tAlive) throw Exception("refresh alive ping failure");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final viewOnly = this is ViewOnlyOptionInterface &&
|
||||||
|
(this as ViewOnlyOptionInterface).isViewOnly;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// this acquire should be almost instant due to above check.
|
// this acquire should be almost instant due to above check.
|
||||||
// Slight possibility of race but should be irrelevant
|
// Slight possibility of race but should be irrelevant
|
||||||
|
@ -607,7 +611,7 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
||||||
final Set<String> codesToCheck = {};
|
final Set<String> codesToCheck = {};
|
||||||
_checkAlive();
|
_checkAlive();
|
||||||
if (this is PaynymInterface) {
|
if (this is PaynymInterface && !viewOnly) {
|
||||||
// isSegwit does not matter here at all
|
// isSegwit does not matter here at all
|
||||||
final myCode =
|
final myCode =
|
||||||
await (this as PaynymInterface).getPaymentCode(isSegwit: false);
|
await (this as PaynymInterface).getPaymentCode(isSegwit: false);
|
||||||
|
@ -654,8 +658,10 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
|
|
||||||
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
||||||
if (this is MultiAddressInterface) {
|
if (this is MultiAddressInterface) {
|
||||||
await (this as MultiAddressInterface)
|
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
|
||||||
.checkChangeAddressForTransactions();
|
await (this as MultiAddressInterface)
|
||||||
|
.checkChangeAddressForTransactions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_checkAlive();
|
_checkAlive();
|
||||||
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
|
||||||
|
@ -681,7 +687,7 @@ abstract class Wallet<T extends CryptoCurrency> {
|
||||||
await fetchFuture;
|
await fetchFuture;
|
||||||
|
|
||||||
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
|
||||||
if (this is PaynymInterface && codesToCheck.isNotEmpty) {
|
if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) {
|
||||||
_checkAlive();
|
_checkAlive();
|
||||||
await (this as PaynymInterface)
|
await (this as PaynymInterface)
|
||||||
.checkForNotificationTransactionsTo(codesToCheck);
|
.checkForNotificationTransactionsTo(codesToCheck);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
|
import '../../../models/isar/models/blockchain_data/v2/output_v2.dart';
|
||||||
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart';
|
||||||
import '../../../models/isar/models/isar_models.dart';
|
import '../../../models/isar/models/isar_models.dart';
|
||||||
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
import '../../../models/paymint/fee_object_model.dart';
|
import '../../../models/paymint/fee_object_model.dart';
|
||||||
import '../../../models/signing_data.dart';
|
import '../../../models/signing_data.dart';
|
||||||
import '../../../utilities/amount/amount.dart';
|
import '../../../utilities/amount/amount.dart';
|
||||||
|
@ -32,9 +33,10 @@ import '../intermediate/bip39_hd_wallet.dart';
|
||||||
import 'cpfp_interface.dart';
|
import 'cpfp_interface.dart';
|
||||||
import 'paynym_interface.dart';
|
import 'paynym_interface.dart';
|
||||||
import 'rbf_interface.dart';
|
import 'rbf_interface.dart';
|
||||||
|
import 'view_only_option_interface.dart';
|
||||||
|
|
||||||
mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
on Bip39HDWallet<T> {
|
on Bip39HDWallet<T> implements ViewOnlyOptionInterface<T> {
|
||||||
late ElectrumXClient electrumXClient;
|
late ElectrumXClient electrumXClient;
|
||||||
late CachedElectrumXClient electrumXCachedClient;
|
late CachedElectrumXClient electrumXCachedClient;
|
||||||
|
|
||||||
|
@ -137,7 +139,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
(e.used != true) &&
|
(e.used != true) &&
|
||||||
(canCPFP ||
|
(canCPFP ||
|
||||||
e.isConfirmed(
|
e.isConfirmed(
|
||||||
currentChainHeight, cryptoCurrency.minConfirms)),
|
currentChainHeight,
|
||||||
|
cryptoCurrency.minConfirms,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final spendableSatoshiValue =
|
final spendableSatoshiValue =
|
||||||
|
@ -944,7 +948,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
|
|
||||||
Future<({List<Address> addresses, int index})> checkGapsBatched(
|
Future<({List<Address> addresses, int index})> checkGapsBatched(
|
||||||
int txCountBatchSize,
|
int txCountBatchSize,
|
||||||
coinlib.HDPrivateKey root,
|
coinlib.HDKey node,
|
||||||
DerivePathType type,
|
DerivePathType type,
|
||||||
int chain,
|
int chain,
|
||||||
) async {
|
) async {
|
||||||
|
@ -969,7 +973,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
index: index + j,
|
index: index + j,
|
||||||
);
|
);
|
||||||
|
|
||||||
final keys = root.derivePath(derivePath);
|
final coinlib.HDKey keys;
|
||||||
|
if (isViewOnly) {
|
||||||
|
final idx = derivePath.lastIndexOf("'/");
|
||||||
|
final path = derivePath.substring(idx + 2);
|
||||||
|
keys = node.derivePath(path);
|
||||||
|
} else {
|
||||||
|
keys = node.derivePath(derivePath);
|
||||||
|
}
|
||||||
|
|
||||||
final addressData = cryptoCurrency.getAddressForPublicKey(
|
final addressData = cryptoCurrency.getAddressForPublicKey(
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
|
@ -986,7 +997,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
publicKey: keys.publicKey.data,
|
publicKey: keys.publicKey.data,
|
||||||
type: addressData.addressType,
|
type: addressData.addressType,
|
||||||
derivationIndex: index + j,
|
derivationIndex: index + j,
|
||||||
derivationPath: DerivationPath()..value = derivePath,
|
derivationPath:
|
||||||
|
isViewOnly ? null : (DerivationPath()..value = derivePath),
|
||||||
subType:
|
subType:
|
||||||
chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
||||||
);
|
);
|
||||||
|
@ -1025,13 +1037,14 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<({List<Address> addresses, int index})> checkGapsLinearly(
|
Future<({List<Address> addresses, int index})> checkGapsLinearly(
|
||||||
coinlib.HDPrivateKey root,
|
coinlib.HDKey node,
|
||||||
DerivePathType type,
|
DerivePathType type,
|
||||||
int chain,
|
int chain,
|
||||||
) async {
|
) async {
|
||||||
final List<Address> addressArray = [];
|
final List<Address> addressArray = [];
|
||||||
int gapCounter = 0;
|
int gapCounter = 0;
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
||||||
for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) {
|
for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter",
|
"index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter",
|
||||||
|
@ -1043,7 +1056,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
chain: chain,
|
chain: chain,
|
||||||
index: index,
|
index: index,
|
||||||
);
|
);
|
||||||
final keys = root.derivePath(derivePath);
|
|
||||||
|
final coinlib.HDKey keys;
|
||||||
|
if (isViewOnly) {
|
||||||
|
final idx = derivePath.lastIndexOf("'/");
|
||||||
|
final path = derivePath.substring(idx + 2);
|
||||||
|
keys = node.derivePath(path);
|
||||||
|
} else {
|
||||||
|
keys = node.derivePath(derivePath);
|
||||||
|
}
|
||||||
|
|
||||||
final addressData = cryptoCurrency.getAddressForPublicKey(
|
final addressData = cryptoCurrency.getAddressForPublicKey(
|
||||||
publicKey: keys.publicKey,
|
publicKey: keys.publicKey,
|
||||||
derivePathType: type,
|
derivePathType: type,
|
||||||
|
@ -1059,7 +1081,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
publicKey: keys.publicKey.data,
|
publicKey: keys.publicKey.data,
|
||||||
type: addressData.addressType,
|
type: addressData.addressType,
|
||||||
derivationIndex: index,
|
derivationIndex: index,
|
||||||
derivationPath: DerivationPath()..value = derivePath,
|
derivationPath:
|
||||||
|
isViewOnly ? null : (DerivationPath()..value = derivePath),
|
||||||
subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
subType: chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1331,6 +1354,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> checkReceivingAddressForTransactions() async {
|
Future<void> checkReceivingAddressForTransactions() async {
|
||||||
|
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
|
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
|
||||||
try {
|
try {
|
||||||
throw Exception();
|
throw Exception();
|
||||||
|
@ -1382,6 +1409,21 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> checkChangeAddressForTransactions() async {
|
Future<void> checkChangeAddressForTransactions() async {
|
||||||
|
if (isViewOnly && viewOnlyType == ViewOnlyWalletType.addressOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
|
||||||
|
try {
|
||||||
|
throw Exception();
|
||||||
|
} catch (_, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"checkChangeAddressForTransactions called but reuse address flag set: $s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final currentChange = await getCurrentChangeAddress();
|
final currentChange = await getCurrentChangeAddress();
|
||||||
|
|
||||||
|
@ -1419,6 +1461,11 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> recover({required bool isRescan}) async {
|
Future<void> recover({required bool isRescan}) async {
|
||||||
|
if (isViewOnly) {
|
||||||
|
await recoverViewOnly(isRescan: isRescan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final root = await getRootHDNode();
|
final root = await getRootHDNode();
|
||||||
|
|
||||||
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
|
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
|
||||||
|
@ -1918,5 +1965,219 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============== View only ==================================================
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> recoverViewOnly({bool isRescan = false}) async {
|
||||||
|
final data = await getViewOnlyWalletData();
|
||||||
|
|
||||||
|
final coinlib.HDKey? root;
|
||||||
|
if (data is AddressViewOnlyWalletData) {
|
||||||
|
root = null;
|
||||||
|
} else {
|
||||||
|
if ((data as ExtendedKeysViewOnlyWalletData).xPubs.length != 1) {
|
||||||
|
throw Exception(
|
||||||
|
"Only single xpub view only wallets are currently supported",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
root = coinlib.HDPublicKey.decode(data.xPubs.first.encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Future<({int index, List<Address> addresses})>> receiveFutures =
|
||||||
|
[];
|
||||||
|
final List<Future<({int index, List<Address> addresses})>> changeFutures =
|
||||||
|
[];
|
||||||
|
|
||||||
|
const receiveChain = 0;
|
||||||
|
const changeChain = 1;
|
||||||
|
|
||||||
|
const txCountBatchSize = 12;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await refreshMutex.protect(() async {
|
||||||
|
if (isRescan) {
|
||||||
|
// clear cache
|
||||||
|
await electrumXCachedClient.clearSharedTransactionCache(
|
||||||
|
cryptoCurrency: info.coin,
|
||||||
|
);
|
||||||
|
// clear blockchain info
|
||||||
|
await mainDB.deleteWalletBlockchainData(walletId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Address> addressesToStore = [];
|
||||||
|
|
||||||
|
if (root != null) {
|
||||||
|
// receiving addresses
|
||||||
|
Logging.instance.log(
|
||||||
|
"checking receiving addresses...",
|
||||||
|
level: LogLevel.Info,
|
||||||
|
);
|
||||||
|
|
||||||
|
final canBatch = await serverCanBatch;
|
||||||
|
|
||||||
|
for (final type in cryptoCurrency.supportedDerivationPathTypes) {
|
||||||
|
final path = cryptoCurrency.constructDerivePath(
|
||||||
|
derivePathType: type,
|
||||||
|
chain: 0,
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
if (path.startsWith(
|
||||||
|
(data as ExtendedKeysViewOnlyWalletData).xPubs.first.path,
|
||||||
|
)) {
|
||||||
|
receiveFutures.add(
|
||||||
|
canBatch
|
||||||
|
? checkGapsBatched(
|
||||||
|
txCountBatchSize,
|
||||||
|
root,
|
||||||
|
type,
|
||||||
|
receiveChain,
|
||||||
|
)
|
||||||
|
: checkGapsLinearly(
|
||||||
|
root,
|
||||||
|
type,
|
||||||
|
receiveChain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// change addresses
|
||||||
|
Logging.instance.log(
|
||||||
|
"checking change addresses...",
|
||||||
|
level: LogLevel.Info,
|
||||||
|
);
|
||||||
|
for (final type in cryptoCurrency.supportedDerivationPathTypes) {
|
||||||
|
final path = cryptoCurrency.constructDerivePath(
|
||||||
|
derivePathType: type,
|
||||||
|
chain: 0,
|
||||||
|
index: 0,
|
||||||
|
);
|
||||||
|
if (path.startsWith(
|
||||||
|
(data as ExtendedKeysViewOnlyWalletData).xPubs.first.path,
|
||||||
|
)) {
|
||||||
|
changeFutures.add(
|
||||||
|
canBatch
|
||||||
|
? checkGapsBatched(
|
||||||
|
txCountBatchSize,
|
||||||
|
root,
|
||||||
|
type,
|
||||||
|
changeChain,
|
||||||
|
)
|
||||||
|
: checkGapsLinearly(
|
||||||
|
root,
|
||||||
|
type,
|
||||||
|
changeChain,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// io limitations may require running these linearly instead
|
||||||
|
final futuresResult = await Future.wait([
|
||||||
|
Future.wait(receiveFutures),
|
||||||
|
Future.wait(changeFutures),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final receiveResults = futuresResult[0];
|
||||||
|
final changeResults = futuresResult[1];
|
||||||
|
|
||||||
|
int highestReceivingIndexWithHistory = 0;
|
||||||
|
|
||||||
|
for (final tuple in receiveResults) {
|
||||||
|
if (tuple.addresses.isEmpty) {
|
||||||
|
await checkReceivingAddressForTransactions();
|
||||||
|
} else {
|
||||||
|
highestReceivingIndexWithHistory = max(
|
||||||
|
tuple.index,
|
||||||
|
highestReceivingIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.addAll(tuple.addresses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int highestChangeIndexWithHistory = 0;
|
||||||
|
// If restoring a wallet that never sent any funds with change, then set changeArray
|
||||||
|
// manually. If we didn't do this, it'd store an empty array.
|
||||||
|
for (final tuple in changeResults) {
|
||||||
|
if (tuple.addresses.isEmpty) {
|
||||||
|
await checkChangeAddressForTransactions();
|
||||||
|
} else {
|
||||||
|
highestChangeIndexWithHistory = max(
|
||||||
|
tuple.index,
|
||||||
|
highestChangeIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.addAll(tuple.addresses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove extra addresses to help minimize risk of creating a large gap
|
||||||
|
addressesToStore.removeWhere(
|
||||||
|
(e) =>
|
||||||
|
e.subType == AddressSubType.change &&
|
||||||
|
e.derivationIndex > highestChangeIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.removeWhere(
|
||||||
|
(e) =>
|
||||||
|
e.subType == AddressSubType.receiving &&
|
||||||
|
e.derivationIndex > highestReceivingIndexWithHistory,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final clAddress = coinlib.Address.fromString(
|
||||||
|
(data as AddressViewOnlyWalletData).address,
|
||||||
|
cryptoCurrency.networkParams,
|
||||||
|
);
|
||||||
|
|
||||||
|
final AddressType addressType;
|
||||||
|
switch (clAddress.runtimeType) {
|
||||||
|
case const (coinlib.P2PKHAddress):
|
||||||
|
addressType = AddressType.p2pkh;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case const (coinlib.P2SHAddress):
|
||||||
|
addressType = AddressType.p2sh;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case const (coinlib.P2WPKHAddress):
|
||||||
|
addressType = AddressType.p2wpkh;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case const (coinlib.P2TRAddress):
|
||||||
|
addressType = AddressType.p2tr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception(
|
||||||
|
"Unsupported address type: ${clAddress.runtimeType}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addressesToStore.add(
|
||||||
|
Address(
|
||||||
|
walletId: walletId,
|
||||||
|
value: clAddress.toString(),
|
||||||
|
publicKey: [],
|
||||||
|
derivationIndex: -1,
|
||||||
|
derivationPath: null,
|
||||||
|
type: addressType,
|
||||||
|
subType: AddressSubType.receiving,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainDB.updateOrPutAddresses(addressesToStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
unawaited(refresh());
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Exception rethrown from electrumx_mixin recoverViewOnly(): $e\n$s",
|
||||||
|
level: LogLevel.Info,
|
||||||
|
);
|
||||||
|
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,35 +2,35 @@ import '../../../models/keys/xpriv_data.dart';
|
||||||
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
|
||||||
import 'electrumx_interface.dart';
|
import 'electrumx_interface.dart';
|
||||||
|
|
||||||
typedef XPub = ({String path, String xpub});
|
abstract class XKey {
|
||||||
typedef XPriv = ({String path, String xpriv});
|
XKey({required this.path});
|
||||||
|
final String path;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XPub extends XKey {
|
||||||
|
XPub({required super.path, required this.encoded});
|
||||||
|
final String encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XPriv extends XKey {
|
||||||
|
XPriv({required super.path, required this.encoded});
|
||||||
|
final String encoded;
|
||||||
|
}
|
||||||
|
|
||||||
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
||||||
on ElectrumXInterface<T> {
|
on ElectrumXInterface<T> {
|
||||||
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
|
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
|
||||||
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
|
final paths = cryptoCurrency.supportedHardenedDerivationPaths;
|
||||||
(e) => (
|
|
||||||
path: e,
|
|
||||||
addressType: e.getAddressType(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final master = await getRootHDNode();
|
final master = await getRootHDNode();
|
||||||
final fingerprint = master.fingerprint.toRadixString(16);
|
final fingerprint = master.fingerprint.toRadixString(16);
|
||||||
|
|
||||||
final futures = paths.map((e) async {
|
final futures = paths.map((path) async {
|
||||||
String path = cryptoCurrency.constructDerivePath(
|
|
||||||
derivePathType: e.path,
|
|
||||||
chain: 0,
|
|
||||||
index: 0,
|
|
||||||
);
|
|
||||||
// trim chain and address index
|
|
||||||
path = path.substring(0, path.lastIndexOf("'") + 1);
|
|
||||||
final node = master.derivePath(path);
|
final node = master.derivePath(path);
|
||||||
|
|
||||||
return (
|
return XPub(
|
||||||
path: path,
|
path: path,
|
||||||
xpub: node.hdPublicKey.encode(
|
encoded: node.hdPublicKey.encode(
|
||||||
cryptoCurrency.networkParams.pubHDPrefix,
|
cryptoCurrency.networkParams.pubHDPrefix,
|
||||||
// 0x04b24746,
|
// 0x04b24746,
|
||||||
),
|
),
|
||||||
|
@ -44,29 +44,17 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<XPrivData> getXPrivs() async {
|
Future<XPrivData> getXPrivs() async {
|
||||||
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
|
final paths = cryptoCurrency.supportedHardenedDerivationPaths;
|
||||||
(e) => (
|
|
||||||
path: e,
|
|
||||||
addressType: e.getAddressType(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final master = await getRootHDNode();
|
final master = await getRootHDNode();
|
||||||
final fingerprint = master.fingerprint.toRadixString(16);
|
final fingerprint = master.fingerprint.toRadixString(16);
|
||||||
|
|
||||||
final futures = paths.map((e) async {
|
final futures = paths.map((path) async {
|
||||||
String path = cryptoCurrency.constructDerivePath(
|
|
||||||
derivePathType: e.path,
|
|
||||||
chain: 0,
|
|
||||||
index: 0,
|
|
||||||
);
|
|
||||||
// trim chain and address index
|
|
||||||
path = path.substring(0, path.lastIndexOf("'") + 1);
|
|
||||||
final node = master.derivePath(path);
|
final node = master.derivePath(path);
|
||||||
|
|
||||||
return (
|
return XPriv(
|
||||||
path: path,
|
path: path,
|
||||||
xpriv: node.encode(
|
encoded: node.encode(
|
||||||
cryptoCurrency.networkParams.privHDPrefix,
|
cryptoCurrency.networkParams.privHDPrefix,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -76,9 +64,9 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
||||||
walletId: walletId,
|
walletId: walletId,
|
||||||
fingerprint: fingerprint,
|
fingerprint: fingerprint,
|
||||||
xprivs: [
|
xprivs: [
|
||||||
(
|
XPriv(
|
||||||
path: "Master",
|
path: "Master",
|
||||||
xpriv: master.encode(
|
encoded: master.encode(
|
||||||
cryptoCurrency.networkParams.privHDPrefix,
|
cryptoCurrency.networkParams.privHDPrefix,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,34 +1,27 @@
|
||||||
import 'dart:convert';
|
import '../../../models/keys/view_only_wallet_data.dart';
|
||||||
|
|
||||||
import '../../crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
import '../../crypto_currency/interfaces/view_only_option_currency_interface.dart';
|
||||||
import '../wallet.dart';
|
import '../wallet.dart';
|
||||||
|
|
||||||
class ViewOnlyWalletData {
|
|
||||||
final String? address;
|
|
||||||
final String? privateViewKey;
|
|
||||||
|
|
||||||
ViewOnlyWalletData({required this.address, required this.privateViewKey});
|
|
||||||
|
|
||||||
factory ViewOnlyWalletData.fromJsonEncodedString(String jsonEncodedString) {
|
|
||||||
final map = jsonDecode(jsonEncodedString) as Map;
|
|
||||||
final json = Map<String, dynamic>.from(map);
|
|
||||||
return ViewOnlyWalletData(
|
|
||||||
address: json["address"] as String?,
|
|
||||||
privateViewKey: json["privateViewKey"] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJsonEncodedString() => jsonEncode({
|
|
||||||
"address": address,
|
|
||||||
"privateViewKey": privateViewKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mixin ViewOnlyOptionInterface<T extends ViewOnlyOptionCurrencyInterface>
|
mixin ViewOnlyOptionInterface<T extends ViewOnlyOptionCurrencyInterface>
|
||||||
on Wallet<T> {
|
on Wallet<T> {
|
||||||
bool get isViewOnly;
|
ViewOnlyWalletType get viewOnlyType => info.viewOnlyWalletType!;
|
||||||
|
|
||||||
|
bool get isViewOnly => info.isViewOnly;
|
||||||
|
|
||||||
Future<void> recoverViewOnly();
|
Future<void> recoverViewOnly();
|
||||||
|
|
||||||
Future<ViewOnlyWalletData> getViewOnlyWalletData();
|
Future<ViewOnlyWalletData> getViewOnlyWalletData() async {
|
||||||
|
if (!isViewOnly) {
|
||||||
|
throw Exception("This is not a view only wallet");
|
||||||
|
}
|
||||||
|
|
||||||
|
final encoded = await secureStorageInterface.read(
|
||||||
|
key: Wallet.getViewOnlyWalletDataSecStoreKey(walletId: walletId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ViewOnlyWalletData.fromJsonEncodedString(
|
||||||
|
encoded!,
|
||||||
|
walletId: walletId,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ class StackDialog extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: SelectableText(
|
||||||
title,
|
title,
|
||||||
style: STextStyles.pageTitleH2(context),
|
style: STextStyles.pageTitleH2(context),
|
||||||
),
|
),
|
||||||
|
@ -110,7 +110,7 @@ class StackDialog extends StatelessWidget {
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
SelectableText(
|
||||||
message!,
|
message!,
|
||||||
style: STextStyles.smallMed14(context),
|
style: STextStyles.smallMed14(context),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue