Merge branch 'staging' into hideex

This commit is contained in:
julian-CStack 2024-07-29 10:37:36 -06:00 committed by GitHub
commit 08d3e0abb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 4066 additions and 1128 deletions
asset_sources
crypto_plugins
ios
lib
app_config.dartmain.dart
models/isar/models
pages
pages_desktop_specific
services
themes
utilities
wallets
widgets
pubspec.lock
scripts/app_config
test

Binary file not shown.

After

(image error) Size: 15 KiB

Binary file not shown.

After

(image error) Size: 44 KiB

Binary file not shown.

After

(image error) Size: 73 KiB

Binary file not shown.

After

(image error) Size: 2.2 KiB

Binary file not shown.

After

(image error) Size: 4.8 KiB

Binary file not shown.

After

(image error) Size: 16 KiB

@ -1 +1 @@
Subproject commit 19c76409e55f1bfed58855eb767574604376edb6 Subproject commit 46a7da857d4113eb3998567b18ac0b33a470f4fd

@ -1 +1 @@
Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2 Subproject commit 2a74a97fb0f0e22a5280b22c010b710cdeec33bb

3
ios/.gitignore vendored
View file

@ -30,3 +30,6 @@ Runner/GeneratedPluginRegistrant.*
!default.mode2v3 !default.mode2v3
!default.pbxuser !default.pbxuser
!default.perspectivev3 !default.perspectivev3
# app specific, handled by scripts
Runner/Assets.xcassets/LaunchImage.imageset/*.png

View file

@ -15,6 +15,8 @@ abstract class AppConfig {
static const prefix = _prefix; static const prefix = _prefix;
static const suffix = _suffix; static const suffix = _suffix;
static const emptyWalletsMessage = _emptyWalletsMessage;
static String get appDefaultDataDirName => _appDataDirName; static String get appDefaultDataDirName => _appDataDirName;
static String get shortDescriptionText => _shortDescriptionText; static String get shortDescriptionText => _shortDescriptionText;
static String get commitHash => _commitHash; static String get commitHash => _commitHash;

View file

@ -242,7 +242,7 @@ void main(List<String> args) async {
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
// overlays: [SystemUiOverlay.bottom]); // overlays: [SystemUiOverlay.bottom]);
await NotificationApi.init(); unawaited(NotificationApi.init());
await loadCoinlibFuture; await loadCoinlibFuture;

View file

@ -87,7 +87,10 @@ class TransactionV2 {
); );
} }
@ignore
int? get size => _getFromOtherData(key: TxV2OdKeys.size) as int?; int? get size => _getFromOtherData(key: TxV2OdKeys.size) as int?;
@ignore
int? get vSize => _getFromOtherData(key: TxV2OdKeys.vSize) as int?; int? get vSize => _getFromOtherData(key: TxV2OdKeys.vSize) as int?;
bool get isEpiccashTransaction => bool get isEpiccashTransaction =>
@ -192,6 +195,9 @@ class TransactionV2 {
required int currentChainHeight, required int currentChainHeight,
required int minConfirms, required int minConfirms,
}) { }) {
String prettyConfirms() =>
"(${getConfirmations(currentChainHeight)}/$minConfirms)";
if (subType == TransactionSubType.cashFusion || if (subType == TransactionSubType.cashFusion ||
subType == TransactionSubType.mint || subType == TransactionSubType.mint ||
(subType == TransactionSubType.sparkMint && (subType == TransactionSubType.sparkMint &&
@ -199,7 +205,7 @@ class TransactionV2 {
if (isConfirmed(currentChainHeight, minConfirms)) { if (isConfirmed(currentChainHeight, minConfirms)) {
return "Anonymized"; return "Anonymized";
} else { } else {
return "Anonymizing"; return "Anonymizing ${prettyConfirms()}";
} }
} }
@ -219,7 +225,7 @@ class TransactionV2 {
} else if ((numberOfMessages ?? 0) > 1) { } else if ((numberOfMessages ?? 0) > 1) {
return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no)
} else { } else {
return "Receiving"; return "Receiving ${prettyConfirms()}";
} }
} }
} else if (type == TransactionType.outgoing) { } else if (type == TransactionType.outgoing) {
@ -231,7 +237,7 @@ class TransactionV2 {
} else if ((numberOfMessages ?? 0) > 1) { } else if ((numberOfMessages ?? 0) > 1) {
return "Sending (waiting for confirmations)"; return "Sending (waiting for confirmations)";
} else { } else {
return "Sending"; return "Sending ${prettyConfirms()}";
} }
} }
} }
@ -244,16 +250,20 @@ class TransactionV2 {
if (isConfirmed(currentChainHeight, minConfirms)) { if (isConfirmed(currentChainHeight, minConfirms)) {
return "Received"; return "Received";
} else { } else {
return "Receiving"; return "Receiving ${prettyConfirms()}";
} }
} else if (type == TransactionType.outgoing) { } else if (type == TransactionType.outgoing) {
if (isConfirmed(currentChainHeight, minConfirms)) { if (isConfirmed(currentChainHeight, minConfirms)) {
return "Sent"; return "Sent";
} else { } else {
return "Sending"; return "Sending ${prettyConfirms()}";
} }
} else if (type == TransactionType.sentToSelf) { } else if (type == TransactionType.sentToSelf) {
return "Sent to self"; if (isConfirmed(currentChainHeight, minConfirms)) {
return "Sent to self";
} else {
return "Sent to self ${prettyConfirms()}";
}
} else { } else {
return type.name; return type.name;
} }

View file

@ -0,0 +1,35 @@
/*
* This file is part of Stack Wallet.
*
* Copyright (c) 2023 Cypher Stack
* All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26
*
*/
import 'package:isar/isar.dart';
part 'sent_to_address.g.dart';
@Collection()
class SentToAddress {
SentToAddress({
required this.walletId,
required this.txid,
required this.value,
this.label = "",
});
Id id = Isar.autoIncrement;
@Index()
late final String walletId;
@Index(unique: true, composite: [CompositeIndex("walletId")])
late final String txid;
late final String value;
late final String label;
}

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ import 'dart:typed_data';
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 '../../../../../app_config.dart';
import '../../../../../frost_route_generator.dart'; import '../../../../../frost_route_generator.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';
@ -45,7 +46,7 @@ class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> {
static const _warning = "These are your private keys. Please back them up, " static const _warning = "These are your private keys. Please back them up, "
"keep them safe and never share it with anyone. Your private keys are the" "keep them safe and never share it with anyone. Your private keys are the"
" only way you can access your funds if you forget PIN, lose your phone, " " only way you can access your funds if you forget PIN, lose your phone, "
"etc. Stack Wallet does not keep nor is able to restore your private keys" "etc. ${AppConfig.prefix} does not keep nor is able to restore your private keys"
"."; ".";
late final String seed, recoveryString, serializedKeys, multisigConfig; late final String seed, recoveryString, serializedKeys, multisigConfig;

View file

@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../app_config.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';
@ -173,7 +174,7 @@ class _NewWalletRecoveryPhraseWarningViewState
"write it down. Keep it safe and never share it with " "write it down. Keep it safe and never share it with "
"anyone. Your recovery phrase is the only way you can" "anyone. Your recovery phrase is the only way you can"
" access your funds if you forget your PIN, lose your" " access your funds if you forget your PIN, lose your"
" phone, etc.\n\nStack Wallet does not keep nor is " " phone, etc.\n\n${AppConfig.appName} does not keep nor is "
"able to restore your recover phrase. Only you have " "able to restore your recover phrase. Only you have "
"access to your wallet.", "access to your wallet.",
style: isDesktop style: isDesktop
@ -427,7 +428,7 @@ class _NewWalletRecoveryPhraseWarningViewState
), ),
Flexible( Flexible(
child: Text( child: Text(
"I understand that Stack Wallet does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.", "I understand that ${AppConfig.appName} does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.",
style: isDesktop style: isDesktop
? STextStyles.desktopTextMedium( ? STextStyles.desktopTextMedium(
context, context,

View file

@ -14,14 +14,9 @@ import 'dart:math';
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 '../../../notifications/show_flush_bar.dart'; import '../../../notifications/show_flush_bar.dart';
import '../add_token_view/edit_wallet_tokens_view.dart';
import '../new_wallet_options/new_wallet_options_view.dart';
import '../new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import '../select_wallet_for_token_view.dart';
import 'sub_widgets/word_table.dart';
import 'verify_mnemonic_passphrase_dialog.dart';
import '../../home_view/home_view.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';
@ -38,7 +33,13 @@ import '../../../wallets/wallet/wallet.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 'package:tuple/tuple.dart'; import '../../home_view/home_view.dart';
import '../add_token_view/edit_wallet_tokens_view.dart';
import '../new_wallet_options/new_wallet_options_view.dart';
import '../new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import '../select_wallet_for_token_view.dart';
import 'sub_widgets/word_table.dart';
import 'verify_mnemonic_passphrase_dialog.dart';
final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false); final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false);
@ -114,7 +115,8 @@ class _VerifyRecoveryPhraseViewState
Future<void> _continue(bool isMatch) async { Future<void> _continue(bool isMatch) async {
if (isMatch) { if (isMatch) {
if (ref.read(pNewWalletOptions.state).state != null) { if (ref.read(pNewWalletOptions) != null &&
ref.read(pNewWalletOptions)!.mnemonicPassphrase.isNotEmpty) {
final passphraseVerified = await _verifyMnemonicPassphrase(); final passphraseVerified = await _verifyMnemonicPassphrase();
if (!passphraseVerified) { if (!passphraseVerified) {

View file

@ -75,6 +75,14 @@ class _NewContactAddressEntryFormState
addressLabelFocusNode = FocusNode(); addressLabelFocusNode = FocusNode();
addressFocusNode = FocusNode(); addressFocusNode = FocusNode();
coins = [...AppConfig.coins]; coins = [...AppConfig.coins];
if (AppConfig.isSingleCoinApp) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
ref.read(addressEntryDataProvider(widget.id)).coin = coins.first;
}
});
}
super.initState(); super.initState();
} }
@ -109,7 +117,7 @@ class _NewContactAddressEntryFormState
return Column( return Column(
children: [ children: [
if (isDesktop) if (isDesktop && !AppConfig.isSingleCoinApp)
DropdownButtonHideUnderline( DropdownButtonHideUnderline(
child: DropdownButton2<CryptoCurrency>( child: DropdownButton2<CryptoCurrency>(
hint: Text( hint: Text(
@ -188,7 +196,7 @@ class _NewContactAddressEntryFormState
], ],
), ),
), ),
if (!isDesktop) if (!isDesktop && !AppConfig.isSingleCoinApp)
TextField( TextField(
autocorrect: Util.isDesktop ? false : true, autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true,
@ -280,9 +288,10 @@ class _NewContactAddressEntryFormState
), ),
), ),
), ),
const SizedBox( if (!AppConfig.isSingleCoinApp)
height: 8, const SizedBox(
), height: 8,
),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius, Constants.size.circularBorderRadius,

View file

@ -1155,7 +1155,7 @@ class _BuyFormState extends ConsumerState<BuyForm> {
), ),
if (AppConfig.isStackCoin(selectedCrypto?.ticker)) if (AppConfig.isStackCoin(selectedCrypto?.ticker))
CustomTextButton( CustomTextButton(
text: "Choose from Stack", text: "Choose from ${AppConfig.prefix}",
onTap: () { onTap: () {
try { try {
final coin = AppConfig.getCryptoCurrencyForTicker( final coin = AppConfig.getCryptoCurrencyForTicker(

View file

@ -194,7 +194,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
), ),
if (AppConfig.isStackCoin(model.receiveTicker)) if (AppConfig.isStackCoin(model.receiveTicker))
CustomTextButton( CustomTextButton(
text: "Choose from Stack", text: "Choose from ${AppConfig.prefix}",
onTap: () { onTap: () {
try { try {
final coin = AppConfig.coins.firstWhere( final coin = AppConfig.coins.firstWhere(
@ -480,7 +480,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
), ),
if (AppConfig.isStackCoin(model.sendTicker)) if (AppConfig.isStackCoin(model.sendTicker))
CustomTextButton( CustomTextButton(
text: "Choose from Stack", text: "Choose from ${AppConfig.prefix}",
onTap: () { onTap: () {
try { try {
final coin = AppConfig.coins.firstWhere( final coin = AppConfig.coins.firstWhere(

View file

@ -14,10 +14,9 @@ import 'dart:io';
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:flutter_svg/svg.dart';
import '../../app_config.dart';
import '../../models/exchange/response_objects/trade.dart'; import '../../models/exchange/response_objects/trade.dart';
import 'confirm_change_now_send.dart';
import '../home_view/home_view.dart';
import '../send_view/sub_widgets/building_transaction_dialog.dart';
import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart';
import '../../providers/providers.dart'; import '../../providers/providers.dart';
import '../../route_generator.dart'; import '../../route_generator.dart';
@ -29,12 +28,14 @@ import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/assets.dart'; import '../../utilities/assets.dart';
import '../../utilities/constants.dart'; import '../../utilities/constants.dart';
import '../../utilities/enums/fee_rate_type_enum.dart'; import '../../utilities/enums/fee_rate_type_enum.dart';
import '../../utilities/logger.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/isar/providers/wallet_info_provider.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart'; import '../../wallets/models/tx_data.dart';
import '../../wallets/wallet/impl/firo_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_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';
@ -43,6 +44,9 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/expandable.dart'; import '../../widgets/expandable.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 '../send_view/sub_widgets/building_transaction_dialog.dart';
import 'confirm_change_now_send.dart';
class SendFromView extends ConsumerStatefulWidget { class SendFromView extends ConsumerStatefulWidget {
const SendFromView({ const SendFromView({
@ -135,7 +139,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> {
left: 32, left: 32,
), ),
child: Text( child: Text(
"Send from Stack", "Send from ${AppConfig.prefix}",
style: STextStyles.desktopH3(context), style: STextStyles.desktopH3(context),
), ),
), ),
@ -269,6 +273,15 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
), ),
); );
// Currently CwBasedInterface wallets (xmr/wow) shouldn't even have
// access to this screen but this is needed to get past an error that
// would occur only to lead to another error which is why xmr/wow wallets
// don't have access to this screen currently
if (wallet is CwBasedInterface) {
await wallet.init();
await wallet.open();
}
final time = Future<dynamic>.delayed( final time = Future<dynamic>.delayed(
const Duration( const Duration(
milliseconds: 2500, milliseconds: 2500,
@ -373,7 +386,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
); );
} }
} }
} catch (e) { } catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
if (mounted) { if (mounted) {
// pop building dialog // pop building dialog
Navigator.of(context).pop(); Navigator.of(context).pop();

View file

@ -190,6 +190,15 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
final isDesktop = Util.isDesktop; final isDesktop = Util.isDesktop;
final showSendFromStackButton = !hasTx &&
!["xmr", "monero", "wow", "wownero"]
.contains(trade.payInCurrency.toLowerCase()) &&
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting");
return ConditionalParent( return ConditionalParent(
condition: !isDesktop, condition: !isDesktop,
builder: (child) => Background( builder: (child) => Background(
@ -248,23 +257,13 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
children: children, children: children,
), ),
), ),
if (!hasTx && if (showSendFromStackButton)
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
const SizedBox( const SizedBox(
height: 32, height: 32,
), ),
if (!hasTx && if (showSendFromStackButton)
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
SecondaryButton( SecondaryButton(
label: "Send from Stack", label: "Send from ${AppConfig.prefix}",
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
onPressed: () { onPressed: () {
CryptoCurrency coin; CryptoCurrency coin;
@ -1371,15 +1370,9 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
if (!isDesktop && if (!isDesktop && showSendFromStackButton)
!hasTx &&
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
SecondaryButton( SecondaryButton(
label: "Send from Stack", label: "Send from ${AppConfig.prefix}",
onPressed: () { onPressed: () {
CryptoCurrency coin; CryptoCurrency coin;
try { try {

View file

@ -49,6 +49,8 @@ class _IntroViewState extends ConsumerState<IntroView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType "); debugPrint("BUILD: $runtimeType ");
final stack =
ref.watch(themeProvider.select((value) => value.assets.stack));
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -68,16 +70,22 @@ class _IntroViewState extends ConsumerState<IntroView> {
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 300, maxWidth: 300,
), ),
child: SvgPicture.file( child: SizedBox(
File(
ref.watch(
themeProvider.select(
(value) => value.assets.stack,
),
),
),
width: isDesktop ? 324 : 266, width: isDesktop ? 324 : 266,
height: isDesktop ? 324 : 266, height: isDesktop ? 324 : 266,
child: (stack.endsWith(".png"))
? Image.file(
File(
stack,
),
)
: SvgPicture.file(
File(
stack,
),
width: isDesktop ? 324 : 266,
height: isDesktop ? 324 : 266,
),
), ),
), ),
), ),
@ -163,7 +171,7 @@ class _IntroViewState extends ConsumerState<IntroView> {
), ),
if (isDesktop) if (isDesktop)
SecondaryButton( SecondaryButton(
label: "Restore from Stack backup", label: "Restore from ${AppConfig.prefix} backup",
onPressed: () { onPressed: () {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
CreatePasswordView.routeName, CreatePasswordView.routeName,
@ -306,7 +314,7 @@ class GetStartedButton extends StatelessWidget {
); );
}, },
child: Text( child: Text(
"Create new Stack", "Create new ${AppConfig.prefix}",
style: STextStyles.button(context).copyWith(fontSize: 20), style: STextStyles.button(context).copyWith(fontSize: 20),
), ),
), ),

View file

@ -12,6 +12,7 @@ import 'dart:async';
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:mutex/mutex.dart';
import '../../notifications/show_flush_bar.dart'; import '../../notifications/show_flush_bar.dart';
// import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart';
@ -70,6 +71,7 @@ class LockscreenView extends ConsumerStatefulWidget {
class _LockscreenViewState extends ConsumerState<LockscreenView> { class _LockscreenViewState extends ConsumerState<LockscreenView> {
late final ShakeController _shakeController; late final ShakeController _shakeController;
late final bool _autoPin;
late int _attempts; late int _attempts;
bool _attemptLock = false; bool _attemptLock = false;
@ -186,16 +188,18 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
biometrics = widget.biometrics; biometrics = widget.biometrics;
_attempts = 0; _attempts = 0;
_timeout = Duration.zero; _timeout = Duration.zero;
_autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (_autoPin) {
_pinTextController.addListener(_onPinChangedAutologinCheck);
}
_checkUseBiometrics(); _checkUseBiometrics();
_pinTextController.addListener(_onPinChanged);
super.initState(); super.initState();
} }
@override @override
dispose() { dispose() {
// _shakeController.dispose(); // _shakeController.dispose();
_pinTextController.removeListener(_onPinChanged); _pinTextController.removeListener(_onPinChangedAutologinCheck);
super.dispose(); super.dispose();
} }
@ -218,16 +222,120 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
final _pinTextController = TextEditingController(); final _pinTextController = TextEditingController();
void _onPinChanged() async { final Mutex _autoPinCheckLock = Mutex();
String enteredPin = _pinTextController.text; void _onPinChangedAutologinCheck() async {
final storedPin = await _secureStore.read(key: 'stack_pin'); if (mounted) {
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; await _autoPinCheckLock.acquire();
}
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { try {
if (_autoPin && _pinTextController.text.length >= 4) {
final storedPin = await _secureStore.read(key: 'stack_pin');
if (_pinTextController.text == storedPin) {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
}
}
} finally {
_autoPinCheckLock.release();
}
}
void _onSubmitPin(String pin) async {
_attempts++;
if (_attempts > maxAttemptsBeforeThrottling) {
_attemptLock = true;
switch (_attempts) {
case 4:
_timeout = const Duration(seconds: 30);
break;
case 5:
_timeout = const Duration(seconds: 60);
break;
case 6:
_timeout = const Duration(minutes: 5);
break;
case 7:
_timeout = const Duration(minutes: 10);
break;
case 8:
_timeout = const Duration(minutes: 20);
break;
case 9:
_timeout = const Duration(minutes: 30);
break;
default:
_timeout = const Duration(minutes: 60);
}
_timer?.cancel();
_timer = Timer(_timeout, () {
_attemptLock = false;
_attempts = 0;
});
}
if (_attemptLock) {
String prettyTime = "";
if (_timeout.inSeconds >= 60) {
prettyTime += "${_timeout.inMinutes} minutes";
} else {
prettyTime += "${_timeout.inSeconds} seconds";
}
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Incorrect PIN entered too many times. Please wait $prettyTime",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
return;
}
final storedPin = await _secureStore.read(key: 'stack_pin');
if (storedPin == pin) {
await Future<void>.delayed( await Future<void>.delayed(
const Duration(milliseconds: 200), const Duration(milliseconds: 200),
); );
unawaited(_onUnlock()); unawaited(_onUnlock());
} else {
unawaited(_shakeController.shake());
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Incorrect PIN. Please try again",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
}
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
} }
} }
@ -329,98 +437,9 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
isRandom: ref isRandom: ref
.read(prefsChangeNotifierProvider) .read(prefsChangeNotifierProvider)
.randomizePIN, .randomizePIN,
onSubmit: (String pin) async { onSubmit: (pin) {
_attempts++; if (!_autoPinCheckLock.isLocked) {
_onSubmitPin(pin);
if (_attempts > maxAttemptsBeforeThrottling) {
_attemptLock = true;
switch (_attempts) {
case 4:
_timeout = const Duration(seconds: 30);
break;
case 5:
_timeout = const Duration(seconds: 60);
break;
case 6:
_timeout = const Duration(minutes: 5);
break;
case 7:
_timeout = const Duration(minutes: 10);
break;
case 8:
_timeout = const Duration(minutes: 20);
break;
case 9:
_timeout = const Duration(minutes: 30);
break;
default:
_timeout = const Duration(minutes: 60);
}
_timer?.cancel();
_timer = Timer(_timeout, () {
_attemptLock = false;
_attempts = 0;
});
}
if (_attemptLock) {
String prettyTime = "";
if (_timeout.inSeconds >= 60) {
prettyTime += "${_timeout.inMinutes} minutes";
} else {
prettyTime += "${_timeout.inSeconds} seconds";
}
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Incorrect PIN entered too many times. Please wait $prettyTime",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
return;
}
final storedPin =
await _secureStore.read(key: 'stack_pin');
if (storedPin == pin) {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
} else {
unawaited(_shakeController.shake());
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Incorrect PIN. Please try again",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
} }
}, },
), ),

View file

@ -2,6 +2,7 @@ import 'dart:async';
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:mutex/mutex.dart';
import '../../notifications/show_flush_bar.dart'; import '../../notifications/show_flush_bar.dart';
import '../../providers/global/prefs_provider.dart'; import '../../providers/global/prefs_provider.dart';
@ -38,6 +39,8 @@ class PinpadDialog extends ConsumerStatefulWidget {
class _PinpadDialogState extends ConsumerState<PinpadDialog> { class _PinpadDialogState extends ConsumerState<PinpadDialog> {
late final ShakeController _shakeController; late final ShakeController _shakeController;
late final bool _autoPin;
late int _attempts; late int _attempts;
bool _attemptLock = false; bool _attemptLock = false;
late Duration _timeout; late Duration _timeout;
@ -63,16 +66,24 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> {
); );
} }
Future<void> _onPinChanged() async { final Mutex _autoPinCheckLock = Mutex();
final enteredPin = _pinTextController.text; void _onPinChangedAutologinCheck() async {
final storedPin = await _secureStore.read(key: 'stack_pin'); if (mounted) {
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; await _autoPinCheckLock.acquire();
}
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { try {
await Future<void>.delayed( if (_autoPin && _pinTextController.text.length >= 4) {
const Duration(milliseconds: 200), final storedPin = await _secureStore.read(key: 'stack_pin');
); if (_pinTextController.text == storedPin) {
unawaited(_onUnlock()); await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
}
}
} finally {
_autoPinCheckLock.release();
} }
} }
@ -215,16 +226,19 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> {
biometrics = widget.biometrics; biometrics = widget.biometrics;
_attempts = 0; _attempts = 0;
_timeout = Duration.zero; _timeout = Duration.zero;
_autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (_autoPin) {
_pinTextController.addListener(_onPinChangedAutologinCheck);
}
_checkUseBiometrics(); _checkUseBiometrics();
_pinTextController.addListener(_onPinChanged);
super.initState(); super.initState();
} }
@override @override
dispose() { dispose() {
// _shakeController.dispose(); // _shakeController.dispose();
_pinTextController.removeListener(_onPinChanged); _pinTextController.removeListener(_onPinChangedAutologinCheck);
super.dispose(); super.dispose();
} }
@ -276,7 +290,11 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> {
submittedFieldDecoration: _pinPutDecoration, submittedFieldDecoration: _pinPutDecoration,
isRandom: isRandom:
ref.read(prefsChangeNotifierProvider).randomizePIN, ref.read(prefsChangeNotifierProvider).randomizePIN,
onSubmit: _onSubmit, onSubmit: (pin) {
if (!_autoPinCheckLock.isLocked) {
_onSubmit(pin);
}
},
), ),
const SizedBox( const SizedBox(
height: 32, height: 32,

View file

@ -22,6 +22,7 @@ import '../../../utilities/address_utils.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/intermediate/bip39_hd_wallet.dart';
import '../../../widgets/address_private_key.dart'; import '../../../widgets/address_private_key.dart';
import '../../../widgets/background.dart'; import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart'; import '../../../widgets/conditional_parent.dart';
@ -371,13 +372,17 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
detail: address.subType.prettyName, detail: address.subType.prettyName,
button: Container(), button: Container(),
), ),
const _Div( if (ref.watch(pWallets).getWallet(widget.walletId)
height: 12, is Bip39HDWallet)
), const _Div(
AddressPrivateKey( height: 12,
walletId: widget.walletId, ),
address: address, if (ref.watch(pWallets).getWallet(widget.walletId)
), is Bip39HDWallet)
AddressPrivateKey(
walletId: widget.walletId,
address: address,
),
if (!isDesktop) if (!isDesktop)
const SizedBox( const SizedBox(
height: 20, height: 20,

View file

@ -92,7 +92,6 @@ class _FrostSendViewState extends ConsumerState<FrostSendView> {
final txData = await wallet.frostCreateSignConfig( final txData = await wallet.frostCreateSignConfig(
txData: TxData(recipients: recipients), txData: TxData(recipients: recipients),
changeAddress: (await wallet.getCurrentReceivingAddress())!.value,
feePerWeight: customFeeRate, feePerWeight: customFeeRate,
); );

View file

@ -942,6 +942,9 @@ class _SendViewState extends ConsumerState<SendView> {
if (isPaynymSend) { if (isPaynymSend) {
sendToController.text = widget.accountLite!.nymName; sendToController.text = widget.accountLite!.nymName;
noteController.text = "PayNym send"; noteController.text = "PayNym send";
WidgetsBinding.instance.addPostFrameCallback(
(_) => _setValidAddressProviders(sendToController.text),
);
} }
// if (coin is! Epiccash) { // if (coin is! Epiccash) {

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../../app_config.dart';
import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/prefs_provider.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/constants.dart'; import '../../../../utilities/constants.dart';
@ -267,7 +268,7 @@ class AdvancedSettingsView extends StatelessWidget {
text: TextSpan( text: TextSpan(
children: [ children: [
TextSpan( TextSpan(
text: "Stack Experience", text: "${AppConfig.prefix} Experience",
style: STextStyles.titleBold12(context), style: STextStyles.titleBold12(context),
), ),
TextSpan( TextSpan(

View file

@ -24,6 +24,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import '../../../../app_config.dart';
import '../../../../models/isar/models/log.dart'; import '../../../../models/isar/models/log.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/debug_service_provider.dart'; import '../../../../providers/global/debug_service_provider.dart';
@ -421,7 +422,7 @@ class _DebugViewState extends ConsumerState<DebugView> {
}, },
child: CustomLoadingOverlay( child: CustomLoadingOverlay(
message: message:
"Generating Stack logs file", "Generating ${AppConfig.prefix} logs file",
eventBus: eventBus, eventBus: eventBus,
), ),
), ),

View file

@ -11,6 +11,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/block_explorers.dart'; import '../../../../utilities/block_explorers.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
@ -91,7 +92,7 @@ class _ManageExplorerViewState extends ConsumerState<ManageExplorerView> {
"every block explorer has a slightly different URL " "every block explorer has a slightly different URL "
"scheme.\n\nPaste in your block explorer of choice," "scheme.\n\nPaste in your block explorer of choice,"
" then edit in [TXID] where the transaction ID " " then edit in [TXID] where the transaction ID "
"should go, and Stack Wallet will auto fill the " "should go, and ${AppConfig.appName} will auto fill the "
"transaction ID in that place of URL.", "transaction ID in that place of URL.",
style: STextStyles.itemSubtitle(context), style: STextStyles.itemSubtitle(context),
), ),

View file

@ -99,7 +99,7 @@ class GlobalSettingsView extends StatelessWidget {
SettingsListButton( SettingsListButton(
iconAssetName: Assets.svg.downloadFolder, iconAssetName: Assets.svg.downloadFolder,
iconSize: 14, iconSize: 14,
title: "Stack backup & restore", title: "${AppConfig.prefix} backup & restore",
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
@ -113,9 +113,9 @@ class GlobalSettingsView extends StatelessWidget {
biometricsCancelButtonString: biometricsCancelButtonString:
"CANCEL", "CANCEL",
biometricsLocalizedReason: biometricsLocalizedReason:
"Authenticate to access Stack backup & restore settings", "Authenticate to access ${AppConfig.prefix} backup & restore settings",
biometricsAuthenticationTitle: biometricsAuthenticationTitle:
"Stack backup", "${AppConfig.prefix} backup",
), ),
settings: const RouteSettings( settings: const RouteSettings(
name: "/swblockscreen", name: "/swblockscreen",

View file

@ -61,12 +61,9 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
int pinCount = 1; int pinCount = 1;
final TextEditingController _pinTextController = TextEditingController();
@override @override
void initState() { void initState() {
_secureStore = ref.read(secureStoreProvider); _secureStore = ref.read(secureStoreProvider);
_pinTextController.addListener(_onPinChanged);
super.initState(); super.initState();
} }
@ -77,23 +74,9 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
_pinPutController2.dispose(); _pinPutController2.dispose();
_pinPutFocusNode1.dispose(); _pinPutFocusNode1.dispose();
_pinPutFocusNode2.dispose(); _pinPutFocusNode2.dispose();
_pinTextController.removeListener(_onPinChanged);
super.dispose(); super.dispose();
} }
void _onPinChanged() async {
String enteredPin = _pinTextController.text;
final storedPin = await _secureStore.read(key: 'stack_pin');
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
await _pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Background( return Background(

View file

@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../../app_config.dart';
import '../../../../providers/global/auto_swb_service_provider.dart'; import '../../../../providers/global/auto_swb_service_provider.dart';
import '../../../../providers/providers.dart'; import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
@ -316,7 +317,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> {
children: [ children: [
const TextSpan( const TextSpan(
text: text:
"Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ", "Auto Backup is a custom ${AppConfig.appName} feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ",
), ),
TextSpan( TextSpan(
text: "stackwallet.com.", text: "stackwallet.com.",

View file

@ -18,6 +18,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:zxcvbn/zxcvbn.dart'; import 'package:zxcvbn/zxcvbn.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/prefs_provider.dart';
import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/global/secure_store_provider.dart';
@ -651,12 +652,12 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
builder: (_) => Platform.isAndroid builder: (_) => Platform.isAndroid
? StackOkDialog( ? StackOkDialog(
title: title:
"Stack Auto Backup enabled and saved to:", "${AppConfig.prefix} Auto Backup enabled and saved to:",
message: fileToSave, message: fileToSave,
) )
: const StackOkDialog( : const StackOkDialog(
title: title:
"Stack Auto Backup enabled!", "${AppConfig.prefix} Auto Backup enabled!",
), ),
); );
if (mounted) { if (mounted) {

View file

@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:zxcvbn/zxcvbn.dart'; import 'package:zxcvbn/zxcvbn.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/global/secure_store_provider.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
@ -774,7 +775,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> {
height: 26, height: 26,
), ),
Text( Text(
"Stack backup saved to: \n", "${AppConfig.prefix} backup saved to: \n",
style: STextStyles style: STextStyles
.desktopH3(context), .desktopH3(context),
), ),

View file

@ -10,6 +10,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../../app_config.dart';
import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/stack_colors.dart';
import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/text_styles.dart';
import '../../../../../utilities/util.dart'; import '../../../../../utilities/util.dart';
@ -89,7 +90,7 @@ class CancelStackRestoreDialog extends StatelessWidget {
.snackBarBackError, .snackBarBackError,
child: Text( child: Text(
"If you cancel, the restore will not complete, and " "If you cancel, the restore will not complete, and "
"the wallets will not appear in your Stack.", "the wallets will not appear in your ${AppConfig.prefix}.",
style: STextStyles.desktopTextMedium(context), style: STextStyles.desktopTextMedium(context),
), ),
), ),

View file

@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:zxcvbn/zxcvbn.dart'; import 'package:zxcvbn/zxcvbn.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/prefs_provider.dart';
import '../../../../providers/global/secure_store_provider.dart'; import '../../../../providers/global/secure_store_provider.dart';
@ -221,10 +222,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
barrierDismissible: false, barrierDismissible: false,
builder: (_) => Platform.isAndroid builder: (_) => Platform.isAndroid
? StackOkDialog( ? StackOkDialog(
title: "Stack Auto Backup saved to:", title: "${AppConfig.prefix} Auto Backup saved to:",
message: fileToSave, message: fileToSave,
) )
: const StackOkDialog(title: "Stack Auto Backup saved"), : const StackOkDialog(
title: "${AppConfig.prefix} Auto Backup saved"),
); );
if (mounted) { if (mounted) {
passwordController.text = ""; passwordController.text = "";

View file

@ -14,6 +14,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../../../../app_config.dart';
import '../../../../../utilities/util.dart'; import '../../../../../utilities/util.dart';
class SWBFileSystem { class SWBFileSystem {
@ -39,17 +41,18 @@ class SWBFileSystem {
// debugPrint(rootPath!.absolute.toString()); // debugPrint(rootPath!.absolute.toString());
late Directory sampleFolder; late Directory sampleFolder;
const dirName = "${AppConfig.prefix}_backup";
if (Platform.isIOS) { if (Platform.isIOS) {
sampleFolder = Directory(rootPath!.path); sampleFolder = Directory(rootPath!.path);
} else if (Platform.isAndroid) { } else if (Platform.isAndroid) {
sampleFolder = Directory('${rootPath!.path}Documents/Stack_backups'); sampleFolder = Directory('${rootPath!.path}Documents/$dirName');
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
sampleFolder = Directory('${rootPath!.path}/Stack_backups'); sampleFolder = Directory('${rootPath!.path}/$dirName');
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
sampleFolder = Directory('${rootPath!.path}/Stack_backups'); sampleFolder = Directory('${rootPath!.path}/$dirName');
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
sampleFolder = Directory('${rootPath!.path}/Stack_backups'); sampleFolder = Directory('${rootPath!.path}/$dirName');
} }
try { try {
@ -79,17 +82,20 @@ class SWBFileSystem {
} }
Future<void> pickDir(BuildContext context) async { Future<void> pickDir(BuildContext context) async {
final String? path; final String? chosenPath;
if (Platform.isIOS) { if (Platform.isIOS) {
path = startPath?.path; chosenPath = startPath?.path;
} else { } else {
path = await FilePicker.platform.getDirectoryPath( final String path = Platform.isWindows
? startPath!.path.replaceAll("/", "\\")
: startPath!.path;
chosenPath = await FilePicker.platform.getDirectoryPath(
dialogTitle: "Choose Backup location", dialogTitle: "Choose Backup location",
initialDirectory: startPath!.path, initialDirectory: path,
lockParentWindow: true, lockParentWindow: true,
); );
} }
dirPath = path; dirPath = chosenPath;
} }
Future<void> openFile(BuildContext context) async { Future<void> openFile(BuildContext context) async {

View file

@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../route_generator.dart'; import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
@ -205,7 +206,7 @@ class _RestoreFromEncryptedStringViewState
color: Colors.transparent, color: Colors.transparent,
child: Center( child: Center(
child: Text( child: Text(
"Decrypting Stack backup file", "Decrypting ${AppConfig.prefix} backup file",
style: style:
STextStyles.pageTitleH2( STextStyles.pageTitleH2(
context, context,

View file

@ -338,7 +338,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
color: Colors.transparent, color: Colors.transparent,
child: Center( child: Center(
child: Text( child: Text(
"Decrypting Stack backup file", "Decrypting ${AppConfig.prefix} backup file",
style: STextStyles.pageTitleH2( style: STextStyles.pageTitleH2(
context, context,
).copyWith( ).copyWith(
@ -452,7 +452,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
color: Colors.transparent, color: Colors.transparent,
child: Center( child: Center(
child: Text( child: Text(
"Decrypting Stack backup file", "Decrypting ${AppConfig.prefix} backup file",
style: STextStyles.pageTitleH2( style: STextStyles.pageTitleH2(
context, context,
).copyWith( ).copyWith(

View file

@ -10,9 +10,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'auto_backup_view.dart';
import 'create_backup_view.dart'; import '../../../../app_config.dart';
import 'restore_from_file_view.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';
@ -20,6 +19,9 @@ import '../../../../utilities/text_styles.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/rounded_white_container.dart'; import '../../../../widgets/rounded_white_container.dart';
import 'auto_backup_view.dart';
import 'create_backup_view.dart';
import 'restore_from_file_view.dart';
class StackBackupView extends StatelessWidget { class StackBackupView extends StatelessWidget {
const StackBackupView({ const StackBackupView({
@ -42,7 +44,7 @@ class StackBackupView extends StatelessWidget {
}, },
), ),
title: Text( title: Text(
"Stack backup", "${AppConfig.prefix} backup",
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
), ),
), ),

View file

@ -170,7 +170,11 @@ class _Mnemonic extends ConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
child: Text( child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", "Please write down your backup key. Keep it safe and never share "
"it with anyone. Your backup key is the only way you can access"
" your funds if you forget your PIN, lose your phone, etc.\n\n"
"${AppConfig.appName} does not keep nor is able to restore your"
" backup key. Only you have access to your wallet.",
style: STextStyles.label(context), style: STextStyles.label(context),
), ),
), ),

View file

@ -11,6 +11,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../app_config.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';
@ -69,7 +70,11 @@ class DeleteWalletWarningView extends ConsumerWidget {
.extension<StackColors>()! .extension<StackColors>()!
.warningBackground, .warningBackground,
child: Text( child: Text(
"You are going to permanently delete your wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", "You are going to permanently delete your wallet.\n\n"
"If you delete your wallet, the only way you can have access"
" to your funds is by using your backup key.\n\n"
"${AppConfig.appName} does not keep nor is able to restore "
"your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.",
style: STextStyles.baseXS(context).copyWith( style: STextStyles.baseXS(context).copyWith(
color: Theme.of(context) color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!

View file

@ -11,17 +11,22 @@
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 '../../../../providers/db/main_db_provider.dart';
import '../../../../providers/providers.dart'; import '../../../../providers/providers.dart';
import '../../../../route_generator.dart'; import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/constants.dart'; import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart'; import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.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/wallet_mixin_interfaces/lelantus_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_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 '../../../../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/rounded_white_container.dart'; import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart'; import '../../../../widgets/stack_dialog.dart';
import '../../../pinpad_views/lock_screen_view.dart'; import '../../../pinpad_views/lock_screen_view.dart';
@ -31,7 +36,7 @@ import 'rbf_settings_view.dart';
import 'rename_wallet_view.dart'; import 'rename_wallet_view.dart';
import 'spark_info.dart'; import 'spark_info.dart';
class WalletSettingsWalletSettingsView extends ConsumerWidget { class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget {
const WalletSettingsWalletSettingsView({ const WalletSettingsWalletSettingsView({
super.key, super.key,
required this.walletId, required this.walletId,
@ -42,7 +47,88 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
final String walletId; final String walletId;
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<WalletSettingsWalletSettingsView> createState() =>
_WalletSettingsWalletSettingsViewState();
}
class _WalletSettingsWalletSettingsViewState
extends ConsumerState<WalletSettingsWalletSettingsView> {
bool _switchReuseAddressToggledLock = false; // Mutex.
Future<void> _switchReuseAddressToggled(bool newValue) async {
if (newValue) {
await showDialog(
context: context,
builder: (context) {
final isDesktop = Util.isDesktop;
return StackDialog(
title: "Warning!",
message:
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Continue",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
},
).then((confirmed) async {
if (_switchReuseAddressToggledLock) {
return;
}
_switchReuseAddressToggledLock = true; // Lock mutex.
try {
if (confirmed == true) {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: true,
},
isar: ref.read(mainDBProvider).isar,
);
} else {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: false,
},
isar: ref.read(mainDBProvider).isar,
);
}
} finally {
// ensure _switchReuseAddressToggledLock is set to false no matter what.
_switchReuseAddressToggledLock = false;
}
});
} else {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: false,
},
isar: ref.read(mainDBProvider).isar,
);
}
}
@override
Widget build(BuildContext context) {
return Background( return Background(
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background, backgroundColor: Theme.of(context).extension<StackColors>()!.background,
@ -80,7 +166,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
onPressed: () { onPressed: () {
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
RenameWalletView.routeName, RenameWalletView.routeName,
arguments: walletId, arguments: widget.walletId,
); );
}, },
child: Padding( child: Padding(
@ -99,6 +185,172 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
RbfSettingsView.routeName,
arguments: widget.walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"RBF settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is MultiAddressInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is MultiAddressInterface)
RoundedWhiteContainer(
child: Consumer(
builder: (_, ref, __) {
return RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Reuse receiving address",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId).select(
(value) => value.otherData),
)[WalletInfoKeys.reuseAddress]
as bool? ??
false,
onValueChanged: (newValue) {
_switchReuseAddressToggled(newValue);
},
),
),
],
),
),
);
},
),
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is LelantusInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is LelantusInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
LelantusSettingsView.routeName,
arguments: widget.walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Lelantus settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is SparkInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is SparkInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
SparkInfoView.routeName,
arguments: widget.walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Spark info",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
@ -119,7 +371,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
context: context, context: context,
builder: (_) => StackDialog( builder: (_) => StackDialog(
title: title:
"Do you want to delete ${ref.read(pWalletName(walletId))}?", "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?",
leftButton: TextButton( leftButton: TextButton(
style: Theme.of(context) style: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
@ -148,7 +400,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
shouldUseMaterialRoute: shouldUseMaterialRoute:
RouteGenerator.useMaterialPageRoute, RouteGenerator.useMaterialPageRoute,
builder: (_) => LockscreenView( builder: (_) => LockscreenView(
routeOnSuccessArguments: walletId, routeOnSuccessArguments: widget.walletId,
showBackButton: true, showBackButton: true,
routeOnSuccess: routeOnSuccess:
DeleteWalletWarningView.routeName, DeleteWalletWarningView.routeName,
@ -188,116 +440,6 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget {
), ),
), ),
), ),
if (ref.watch(pWallets).getWallet(walletId)
is LelantusInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId)
is LelantusInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
LelantusSettingsView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Lelantus settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId) is SparkInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
SparkInfoView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"Spark info",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(walletId) is RbfInterface)
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
Navigator.of(context).pushNamed(
RbfSettingsView.routeName,
arguments: walletId,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
children: [
Text(
"RBF settings",
style: STextStyles.titleBold12(context),
),
],
),
),
),
),
], ],
), ),
), ),

View file

@ -10,14 +10,17 @@
import 'dart:async'; import 'dart:async';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart'; import '../../../../utilities/assets.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/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart';
@ -25,16 +28,14 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interfa
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_tab_view.dart';
import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/detail_item.dart'; import '../../../../widgets/detail_item.dart';
import '../../../../widgets/qr.dart'; import '../../../../widgets/qr.dart';
import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/rounded_white_container.dart';
class XPubView extends ConsumerWidget { class XPubView extends ConsumerStatefulWidget {
const XPubView({ const XPubView({
super.key, super.key,
required this.walletId, required this.walletId,
@ -49,7 +50,39 @@ class XPubView extends ConsumerWidget {
static const String routeName = "/xpub"; static const String routeName = "/xpub";
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<XPubView> createState() => XPubViewState();
}
class XPubViewState extends ConsumerState<XPubView> {
late String _currentDropDownValue;
String _current(String key) =>
widget.xpubData.xpubs.firstWhere((e) => e.path == key).xpub;
Future<void> _copy() async {
await widget.clipboardInterface.setData(
ClipboardData(text: _current(_currentDropDownValue)),
);
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
}
}
@override
void initState() {
_currentDropDownValue = widget.xpubData.xpubs.first.path;
super.initState();
}
@override
Widget build(BuildContext context) {
final bool isDesktop = Util.isDesktop; final bool isDesktop = Util.isDesktop;
return ConditionalParent( return ConditionalParent(
@ -75,7 +108,25 @@ class XPubView extends ConsumerWidget {
left: 16, left: 16,
right: 16, right: 16,
), ),
child: SingleChildScrollView(child: child), child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
children: [
Expanded(
child: child,
),
const SizedBox(
height: 16,
),
],
),
),
),
),
),
), ),
), ),
), ),
@ -95,7 +146,7 @@ class XPubView extends ConsumerWidget {
left: 32, left: 32,
), ),
child: Text( child: Text(
"${ref.watch(pWalletName(walletId))} xpub(s)", "${ref.watch(pWalletName(widget.walletId))} xpub(s)",
style: STextStyles.desktopH2(context), style: STextStyles.desktopH2(context),
), ),
), ),
@ -119,28 +170,146 @@ class XPubView extends ConsumerWidget {
), ),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (isDesktop) const SizedBox(height: 16), SizedBox(
height: Util.isDesktop ? 12 : 16,
),
DetailItem( DetailItem(
title: "Master fingerprint", title: "Master fingerprint",
detail: xpubData.fingerprint, detail: widget.xpubData.fingerprint,
horizontal: true, horizontal: true,
borderColor: Util.isDesktop
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG
: null,
), ),
if (isDesktop) const SizedBox(height: 16), SizedBox(
CustomTabView( height: Util.isDesktop ? 12 : 16,
titles: xpubData.xpubs.map((e) => e.path).toList(), ),
children: xpubData.xpubs DetailItemBase(
.map( horizontal: true,
(e) => Padding( borderColor: Util.isDesktop
padding: const EdgeInsets.only(top: 16), ? Theme.of(context)
child: _XPub( .extension<StackColors>()!
xpub: e.xpub, .textFieldDefaultBG
derivation: e.path, : null,
title: Text(
"Derivation",
style: STextStyles.itemSubtitle(context),
),
detail: SizedBox(
width: Util.isDesktop ? 200 : 170,
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: _currentDropDownValue,
items: [
...widget.xpubData.xpubs.map(
(e) => DropdownMenuItem(
value: e.path,
child: Text(
e.path,
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(
.toList(), 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,
),
),
),
),
),
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
QR(
data: _current(_currentDropDownValue),
size: Util.isDesktop
? 256
: MediaQuery.of(context).size.width / 1.5,
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
RoundedWhiteContainer(
borderColor: Util.isDesktop
? Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG
: null,
child: SelectableText(
_current(_currentDropDownValue),
style: STextStyles.w500_14(context),
),
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
if (!Util.isDesktop) const Spacer(),
Row(
children: [
if (Util.isDesktop) const Spacer(),
if (Util.isDesktop)
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Copy",
onPressed: _copy,
),
),
],
), ),
], ],
), ),
@ -148,91 +317,3 @@ class XPubView extends ConsumerWidget {
); );
} }
} }
class _XPub extends StatelessWidget {
const _XPub({
super.key,
required this.xpub,
required this.derivation,
this.clipboardInterface = const ClipboardWrapper(),
});
final String xpub;
final String derivation;
final ClipboardInterface clipboardInterface;
@override
Widget build(BuildContext context) {
final bool isDesktop = Util.isDesktop;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 25,
),
ConditionalParent(
condition: !isDesktop,
builder: (child) => RoundedWhiteContainer(
child: child,
),
child: QR(
data: xpub,
size: isDesktop ? 280 : MediaQuery.of(context).size.width / 1.5,
),
),
const SizedBox(height: 25),
RoundedWhiteContainer(
padding: const EdgeInsets.all(16),
borderColor:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
child: SelectableText(
xpub,
style: STextStyles.largeMedium14(context),
),
),
const SizedBox(height: 32),
Row(
children: [
if (isDesktop)
Expanded(
child: SecondaryButton(
buttonHeight: ButtonHeight.xl,
label: "Cancel",
onPressed: Navigator.of(
context,
rootNavigator: true,
).pop,
),
),
if (isDesktop) const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.xl,
label: "Copy",
onPressed: () async {
await clipboardInterface.setData(
ClipboardData(
text: xpub,
),
);
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
}
},
),
),
],
),
],
);
}
}

View file

@ -15,6 +15,7 @@ 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:flutter_svg/svg.dart';
import '../app_config.dart';
import '../db/hive/db.dart'; import '../db/hive/db.dart';
import '../pages_desktop_specific/password/create_password_view.dart'; import '../pages_desktop_specific/password/create_password_view.dart';
import '../providers/global/prefs_provider.dart'; import '../providers/global/prefs_provider.dart';
@ -104,190 +105,199 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> {
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: isDesktop ? 480 : double.infinity, maxWidth: isDesktop ? 480 : double.infinity,
), ),
child: Column( child: ConditionalParent(
mainAxisAlignment: MainAxisAlignment.center, condition: isDesktop,
children: [ builder: (child) => SingleChildScrollView(
Text( child: child,
"Choose your Stack experience", ),
style: isDesktop child: Column(
? STextStyles.desktopH2(context) mainAxisAlignment: MainAxisAlignment.center,
: STextStyles.pageTitleH1(context), children: [
), Text(
SizedBox( "Choose your ${AppConfig.prefix} experience",
height: isDesktop ? 16 : 8, textAlign: TextAlign.center,
), style: isDesktop
Text( ? STextStyles.desktopH2(context)
!widget.isSettings : STextStyles.pageTitleH1(context),
? "You can change it later in Settings"
: "",
style: isDesktop
? STextStyles.desktopSubtitleH2(context)
: STextStyles.subtitle(context),
),
SizedBox(
height: isDesktop ? 32 : 36,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 0 : 16,
), ),
child: PrivacyToggle( SizedBox(
externalCallsEnabled: isEasy, height: isDesktop ? 16 : 8,
onChanged: (externalCalls) {
isEasy = externalCalls;
setState(() {
infoToggle = isEasy;
});
},
), ),
), Text(
SizedBox( !widget.isSettings
height: isDesktop ? 16 : 36, ? "You can change it later in Settings"
), : "",
Padding( style: isDesktop
padding: isDesktop ? STextStyles.desktopSubtitleH2(context)
? const EdgeInsets.all(0) : STextStyles.subtitle(context),
: const EdgeInsets.all(16.0), ),
child: RoundedWhiteContainer( SizedBox(
child: Center( height: isDesktop ? 32 : 36,
child: RichText( ),
textAlign: TextAlign.left, Padding(
text: TextSpan( padding: EdgeInsets.symmetric(
style: isDesktop horizontal: isDesktop ? 0 : 16,
? STextStyles.desktopTextExtraExtraSmall( ),
context, child: PrivacyToggle(
) externalCallsEnabled: isEasy,
: STextStyles.label(context).copyWith( onChanged: (externalCalls) {
fontSize: 12.0, isEasy = externalCalls;
), setState(() {
children: infoToggle infoToggle = isEasy;
? [ });
if (Constants.enableExchange) },
),
),
SizedBox(
height: isDesktop ? 16 : 36,
),
Padding(
padding: isDesktop
? const EdgeInsets.all(0)
: const EdgeInsets.all(16.0),
child: RoundedWhiteContainer(
child: Center(
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(
context,
)
: STextStyles.label(context).copyWith(
fontSize: 12.0,
),
children: infoToggle
? [
if (Constants.enableExchange)
const TextSpan(
text:
"Exchange data preloaded for a seamless experience.\n\n",
),
const TextSpan( const TextSpan(
text: text:
"Exchange data preloaded for a seamless experience.\n\n", "CoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency).\n\n",
), ),
const TextSpan( TextSpan(
text: text:
"CoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency).\n\n", "Recommended for most crypto users.",
), style: isDesktop
TextSpan( ? STextStyles
text: .desktopTextExtraExtraSmall600(
"Recommended for most crypto users.", context,
style: isDesktop )
? STextStyles : TextStyle(
.desktopTextExtraExtraSmall600( color: Theme.of(context)
context, .extension<StackColors>()!
) .textDark,
: TextStyle( fontWeight: FontWeight.w600,
color: Theme.of(context) ),
.extension<StackColors>()! ),
.textDark, ]
fontWeight: FontWeight.w600, : [
), if (Constants.enableExchange)
), const TextSpan(
] text:
: [ "Exchange data not preloaded (slower experience).\n\n",
if (Constants.enableExchange) ),
const TextSpan( const TextSpan(
text: text:
"Exchange data not preloaded (slower experience).\n\n", "CoinGecko disabled (price changes not shown, no wallet value shown in other currencies).\n\n",
), ),
const TextSpan( TextSpan(
text: text:
"CoinGecko disabled (price changes not shown, no wallet value shown in other currencies).\n\n", "Recommended for the privacy conscious.",
), style: isDesktop
TextSpan( ? STextStyles
text: .desktopTextExtraExtraSmall600(
"Recommended for the privacy conscious.", context,
style: isDesktop )
? STextStyles : TextStyle(
.desktopTextExtraExtraSmall600( color: Theme.of(context)
context, .extension<StackColors>()!
) .textDark,
: TextStyle( fontWeight: FontWeight.w600,
color: Theme.of(context) ),
.extension<StackColors>()! ),
.textDark, ],
fontWeight: FontWeight.w600, ),
),
),
],
), ),
), ),
), ),
), ),
), if (!isDesktop)
if (!isDesktop) const Spacer(
const Spacer( flex: 4,
flex: 4, ),
), if (isDesktop)
if (isDesktop) const SizedBox(
const SizedBox( height: 32,
height: 32, ),
), Padding(
Padding( padding: isDesktop
padding: isDesktop ? const EdgeInsets.all(0)
? const EdgeInsets.all(0) : const EdgeInsets.symmetric(
: const EdgeInsets.symmetric( horizontal: 16,
horizontal: 16, vertical: 16,
vertical: 16, ),
), child: Row(
child: Row( children: [
children: [ Expanded(
Expanded( child: PrimaryButton(
child: PrimaryButton( label: !widget.isSettings
label: !widget.isSettings ? "Continue"
? "Continue" : "Save changes",
: "Save changes", onPressed: () {
onPressed: () { ref
ref .read(prefsChangeNotifierProvider)
.read(prefsChangeNotifierProvider) .externalCalls = isEasy;
.externalCalls = isEasy;
DB.instance DB.instance
.put<dynamic>( .put<dynamic>(
boxName: DB.boxNamePrefs, boxName: DB.boxNamePrefs,
key: "externalCalls", key: "externalCalls",
value: isEasy, value: isEasy,
) )
.then((_) { .then((_) {
if (isEasy) { if (isEasy) {
unawaited( if (AppConfig.hasFeature(AppFeature.swap)) {
ExchangeDataLoadingService.instance unawaited(
.loadAll(), ExchangeDataLoadingService.instance
); .loadAll(),
// unawaited( );
// BuyDataLoadingService().loadAll(ref)); }
ref // unawaited(
.read(priceAnd24hChangeNotifierProvider) // BuyDataLoadingService().loadAll(ref));
.start(true); ref
} .read(priceAnd24hChangeNotifierProvider)
}); .start(true);
if (!widget.isSettings) { }
if (isDesktop) { });
Navigator.of(context).pushNamed( if (!widget.isSettings) {
CreatePasswordView.routeName, if (isDesktop) {
); Navigator.of(context).pushNamed(
CreatePasswordView.routeName,
);
} else {
Navigator.of(context).pushNamed(
CreatePinView.routeName,
);
}
} else { } else {
Navigator.of(context).pushNamed( Navigator.pop(context);
CreatePinView.routeName,
);
} }
} else { },
Navigator.pop(context); ),
}
},
), ),
), ],
], ),
), ),
), if (isDesktop)
if (isDesktop) const SizedBox(
const SizedBox( height: kDesktopAppBarHeight,
height: kDesktopAppBarHeight, ),
), ],
], ),
), ),
), ),
), ),

View file

@ -960,8 +960,7 @@ class _DesktopTransactionCardRowState
unawaited( unawaited(
showFloatingFlushBar( showFloatingFlushBar(
context: context, context: context,
message: message: "Restored Epic funds from your Seed have no Data.",
"Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.",
type: FlushBarType.warning, type: FlushBarType.warning,
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
), ),

View file

@ -15,14 +15,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../notifications/show_flush_bar.dart'; import '../../../notifications/show_flush_bar.dart';
import '../../receive_view/addresses/address_details_view.dart';
import '../sub_widgets/tx_icon.dart';
import 'dialogs/cancelling_transaction_progress_dialog.dart';
import 'edit_note_view.dart';
import '../wallet_view.dart';
import '../../../providers/db/main_db_provider.dart'; import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/address_book_service_provider.dart'; import '../../../providers/global/address_book_service_provider.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
@ -52,8 +50,11 @@ import '../../../widgets/icon_widgets/copy_icon.dart';
import '../../../widgets/icon_widgets/pencil_icon.dart'; import '../../../widgets/icon_widgets/pencil_icon.dart';
import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/stack_dialog.dart'; import '../../../widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart'; import '../../receive_view/addresses/address_details_view.dart';
import 'package:url_launcher/url_launcher.dart'; import '../sub_widgets/tx_icon.dart';
import '../wallet_view.dart';
import 'dialogs/cancelling_transaction_progress_dialog.dart';
import 'edit_note_view.dart';
class TransactionDetailsView extends ConsumerStatefulWidget { class TransactionDetailsView extends ConsumerStatefulWidget {
const TransactionDetailsView({ const TransactionDetailsView({
@ -133,13 +134,15 @@ class _TransactionDetailsViewState
} }
String whatIsIt(Transaction tx, int height) { String whatIsIt(Transaction tx, int height) {
String prettyConfirms() => "(${tx.getConfirmations(height)}/$minConfirms)";
final type = tx.type; final type = tx.type;
if (coin is Firo) { if (coin is Firo) {
if (tx.subType == TransactionSubType.mint) { if (tx.subType == TransactionSubType.mint) {
if (tx.isConfirmed(height, minConfirms)) { if (tx.isConfirmed(height, minConfirms)) {
return "Minted"; return "Minted";
} else { } else {
return "Minting"; return "Minting ${prettyConfirms()}";
} }
} }
} }
@ -156,7 +159,7 @@ class _TransactionDetailsViewState
} else if ((_transaction.numberOfMessages ?? 0) > 1) { } else if ((_transaction.numberOfMessages ?? 0) > 1) {
return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no)
} else { } else {
return "Receiving"; return "Receiving ${prettyConfirms()}";
} }
} }
} else if (type == TransactionType.outgoing) { } else if (type == TransactionType.outgoing) {
@ -168,7 +171,7 @@ class _TransactionDetailsViewState
} else if ((_transaction.numberOfMessages ?? 0) > 1) { } else if ((_transaction.numberOfMessages ?? 0) > 1) {
return "Sending (waiting for confirmations)"; return "Sending (waiting for confirmations)";
} else { } else {
return "Sending"; return "Sending ${prettyConfirms()}";
} }
} }
} }
@ -181,16 +184,20 @@ class _TransactionDetailsViewState
if (tx.isConfirmed(height, minConfirms)) { if (tx.isConfirmed(height, minConfirms)) {
return "Received"; return "Received";
} else { } else {
return "Receiving"; return "Receiving ${prettyConfirms()}";
} }
} else if (type == TransactionType.outgoing) { } else if (type == TransactionType.outgoing) {
if (tx.isConfirmed(height, minConfirms)) { if (tx.isConfirmed(height, minConfirms)) {
return "Sent"; return "Sent";
} else { } else {
return "Sending"; return "Sending ${prettyConfirms()}";
} }
} else if (type == TransactionType.sentToSelf) { } else if (type == TransactionType.sentToSelf) {
return "Sent to self"; if (tx.isConfirmed(height, minConfirms)) {
return "Sent to self";
} else {
return "Sent to self ${prettyConfirms()}";
}
} else { } else {
return type.name; return type.name;
} }
@ -1157,59 +1164,95 @@ class _TransactionDetailsViewState
: const SizedBox( : const SizedBox(
height: 12, height: 12,
), ),
RoundedWhiteContainer( Builder(
padding: isDesktop builder: (context) {
? const EdgeInsets.all(16) final String height;
: const EdgeInsets.all(12), final String confirmations;
child: Builder( final confirms = _transaction.getConfirmations(
builder: (context) { currentHeight,
final String height; );
if (widget.coin is Bitcoincash || if (widget.coin is Bitcoincash ||
widget.coin is Ecash) { widget.coin is Ecash) {
height = _transaction.height != null &&
_transaction.height! > 0
? "${_transaction.height!}"
: "Pending";
confirmations = confirms.toString();
} else if (widget.coin is Epiccash &&
_transaction.slateId == null) {
confirmations = "Unknown";
height = "Unknown";
} else {
final confirmed = _transaction.isConfirmed(
currentHeight,
minConfirms,
);
if (widget.coin is! Epiccash && confirmed) {
height = height =
"${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; "${_transaction.height == 0 ? "Unknown" : _transaction.height}";
} else { } else {
height = widget.coin is! Epiccash && height = confirms > 0
_transaction.isConfirmed( ? "${_transaction.height}"
currentHeight, : "Pending";
minConfirms,
)
? "${_transaction.height == 0 ? "Unknown" : _transaction.height}"
: _transaction.getConfirmations(
currentHeight,
) >
0
? "${_transaction.height}"
: "Pending";
} }
return Row( confirmations = confirms.toString();
mainAxisAlignment: }
MainAxisAlignment.spaceBetween,
crossAxisAlignment: return Column(
CrossAxisAlignment.start, children: [
children: [ RoundedWhiteContainer(
Column( padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
Text( Column(
"Block height", crossAxisAlignment:
style: isDesktop CrossAxisAlignment.start,
? STextStyles children: [
.desktopTextExtraExtraSmall( Text(
context, "Block height",
) style: isDesktop
: STextStyles.itemSubtitle( ? STextStyles
context, .desktopTextExtraExtraSmall(
), context,
)
: STextStyles.itemSubtitle(
context,
),
),
if (isDesktop)
const SizedBox(
height: 2,
),
if (isDesktop)
SelectableText(
height,
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles
.itemSubtitle12(
context,
),
),
],
), ),
if (isDesktop) if (!isDesktop)
const SizedBox(
height: 2,
),
if (isDesktop)
SelectableText( SelectableText(
height, height,
style: isDesktop style: isDesktop
@ -1226,30 +1269,91 @@ class _TransactionDetailsViewState
context, context,
), ),
), ),
if (isDesktop)
IconCopyButton(data: height),
], ],
), ),
if (!isDesktop) ),
SelectableText( isDesktop
height, ? const _Divider()
style: isDesktop : const SizedBox(
? STextStyles height: 12,
.desktopTextExtraExtraSmall( ),
context, RoundedWhiteContainer(
).copyWith( padding: isDesktop
color: Theme.of(context) ? const EdgeInsets.all(16)
.extension<StackColors>()! : const EdgeInsets.all(12),
.textDark, child: Row(
) mainAxisAlignment:
: STextStyles.itemSubtitle12( MainAxisAlignment.spaceBetween,
context, crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Confirmations",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
)
: STextStyles.itemSubtitle(
context,
),
),
if (isDesktop)
const SizedBox(
height: 2,
), ),
), if (isDesktop)
if (isDesktop) SelectableText(
IconCopyButton(data: height), confirmations,
], style: isDesktop
); ? STextStyles
}, .desktopTextExtraExtraSmall(
), context,
).copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles
.itemSubtitle12(
context,
),
),
],
),
if (!isDesktop)
SelectableText(
confirmations,
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(
context,
),
),
if (isDesktop)
IconCopyButton(data: height),
],
),
),
],
);
},
), ),
if (coin is Ethereum) if (coin is Ethereum)
isDesktop isDesktop

View file

@ -1053,6 +1053,12 @@ class _TransactionV2DetailsViewState
], ],
), ),
), ),
if (coin is Epiccash)
isDesktop
? const _Divider()
: const SizedBox(
height: 12,
),
if (coin is Epiccash) if (coin is Epiccash)
RoundedWhiteContainer( RoundedWhiteContainer(
padding: isDesktop padding: isDesktop
@ -1457,59 +1463,95 @@ class _TransactionV2DetailsViewState
: const SizedBox( : const SizedBox(
height: 12, height: 12,
), ),
RoundedWhiteContainer( Builder(
padding: isDesktop builder: (context) {
? const EdgeInsets.all(16) final String height;
: const EdgeInsets.all(12), final String confirmations;
child: Builder( final confirms = _transaction.getConfirmations(
builder: (context) { currentHeight,
final String height; );
if (widget.coin is Bitcoincash || if (widget.coin is Bitcoincash ||
widget.coin is Ecash) { widget.coin is Ecash) {
height = _transaction.height != null &&
_transaction.height! > 0
? "${_transaction.height!}"
: "Pending";
confirmations = confirms.toString();
} else if (widget.coin is Epiccash &&
_transaction.slateId == null) {
confirmations = "Unknown";
height = "Unknown";
} else {
final confirmed = _transaction.isConfirmed(
currentHeight,
minConfirms,
);
if (widget.coin is! Epiccash && confirmed) {
height = height =
"${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; "${_transaction.height == 0 ? "Unknown" : _transaction.height}";
} else { } else {
height = widget.coin is! Epiccash && height = confirms > 0
_transaction.isConfirmed( ? "${_transaction.height}"
currentHeight, : "Pending";
minConfirms,
)
? "${_transaction.height == 0 ? "Unknown" : _transaction.height}"
: _transaction.getConfirmations(
currentHeight,
) >
0
? "${_transaction.height}"
: "Pending";
} }
return Row( confirmations = confirms.toString();
mainAxisAlignment: }
MainAxisAlignment.spaceBetween,
crossAxisAlignment: return Column(
CrossAxisAlignment.start, children: [
children: [ RoundedWhiteContainer(
Column( padding: isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: [ children: [
Text( Column(
"Block height", crossAxisAlignment:
style: isDesktop CrossAxisAlignment.start,
? STextStyles children: [
.desktopTextExtraExtraSmall( Text(
context, "Block height",
) style: isDesktop
: STextStyles.itemSubtitle( ? STextStyles
context, .desktopTextExtraExtraSmall(
), context,
)
: STextStyles.itemSubtitle(
context,
),
),
if (isDesktop)
const SizedBox(
height: 2,
),
if (isDesktop)
SelectableText(
height,
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles
.itemSubtitle12(
context,
),
),
],
), ),
if (isDesktop) if (!isDesktop)
const SizedBox(
height: 2,
),
if (isDesktop)
SelectableText( SelectableText(
height, height,
style: isDesktop style: isDesktop
@ -1526,30 +1568,91 @@ class _TransactionV2DetailsViewState
context, context,
), ),
), ),
if (isDesktop)
IconCopyButton(data: height),
], ],
), ),
if (!isDesktop) ),
SelectableText( isDesktop
height, ? const _Divider()
style: isDesktop : const SizedBox(
? STextStyles height: 12,
.desktopTextExtraExtraSmall( ),
context, RoundedWhiteContainer(
).copyWith( padding: isDesktop
color: Theme.of(context) ? const EdgeInsets.all(16)
.extension<StackColors>()! : const EdgeInsets.all(12),
.textDark, child: Row(
) mainAxisAlignment:
: STextStyles.itemSubtitle12( MainAxisAlignment.spaceBetween,
context, crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Confirmations",
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
)
: STextStyles.itemSubtitle(
context,
),
),
if (isDesktop)
const SizedBox(
height: 2,
), ),
), if (isDesktop)
if (isDesktop) SelectableText(
IconCopyButton(data: height), confirmations,
], style: isDesktop
); ? STextStyles
}, .desktopTextExtraExtraSmall(
), context,
).copyWith(
color: Theme.of(
context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles
.itemSubtitle12(
context,
),
),
],
),
if (!isDesktop)
SelectableText(
confirmations,
style: isDesktop
? STextStyles
.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<
StackColors>()!
.textDark,
)
: STextStyles.itemSubtitle12(
context,
),
),
if (isDesktop)
IconCopyButton(data: height),
],
),
),
],
);
},
), ),
if (kDebugMode) if (kDebugMode)

View file

@ -33,6 +33,9 @@ class EmptyWallets extends ConsumerWidget {
final isDesktop = Util.isDesktop; final isDesktop = Util.isDesktop;
final stack =
ref.watch(themeProvider.select((value) => value.assets.stack));
return SafeArea( return SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -47,21 +50,28 @@ class EmptyWallets extends ConsumerWidget {
const Spacer( const Spacer(
flex: 2, flex: 2,
), ),
SvgPicture.file( SizedBox(
File(
ref.watch(
themeProvider.select(
(value) => value.assets.stack,
),
),
),
width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3, width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3,
child: (stack.endsWith(".png"))
? Image.file(
File(
stack,
),
)
: SvgPicture.file(
File(
stack,
),
width: isDesktop
? 324
: MediaQuery.of(context).size.width / 3,
),
), ),
SizedBox( SizedBox(
height: isDesktop ? 30 : 16, height: isDesktop ? 30 : 16,
), ),
Text( Text(
"You do not have any wallets yet. Start building your crypto Stack!", AppConfig.emptyWalletsMessage,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: isDesktop style: isDesktop
? STextStyles.desktopSubtitleH2(context).copyWith( ? STextStyles.desktopSubtitleH2(context).copyWith(

View file

@ -12,7 +12,6 @@ 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:flutter_svg/svg.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:tuple/tuple.dart';
import '../../app_config.dart'; import '../../app_config.dart';
import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart';
@ -20,6 +19,8 @@ import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart'; import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart';
import '../../providers/db/main_db_provider.dart'; import '../../providers/db/main_db_provider.dart';
import '../../providers/providers.dart'; import '../../providers/providers.dart';
import '../../services/event_bus/events/wallet_added_event.dart';
import '../../services/event_bus/global_event_bus.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';
@ -58,6 +59,8 @@ class WalletsOverview extends ConsumerStatefulWidget {
ConsumerState<WalletsOverview> createState() => _EthWalletsOverviewState(); ConsumerState<WalletsOverview> createState() => _EthWalletsOverviewState();
} }
typedef WalletListItemData = ({Wallet wallet, List<EthContract> contracts});
class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
final isDesktop = Util.isDesktop; final isDesktop = Util.isDesktop;
@ -66,28 +69,29 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
String _searchString = ""; String _searchString = "";
final List<Tuple2<Wallet, List<EthContract>>> wallets = []; final Map<String, WalletListItemData> wallets = {};
List<Tuple2<Wallet, List<EthContract>>> _filter(String searchTerm) { List<WalletListItemData> _filter(String searchTerm) {
if (searchTerm.isEmpty) { if (searchTerm.isEmpty) {
return wallets; return wallets.values.toList()
..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name));
} }
final List<Tuple2<Wallet, List<EthContract>>> results = []; final Map<String, WalletListItemData> results = {};
final term = searchTerm.toLowerCase(); final term = searchTerm.toLowerCase();
for (final tuple in wallets) { for (final entry in wallets.entries) {
bool includeManager = false; bool includeManager = false;
// search wallet name and total balance // search wallet name and total balance
includeManager |= _elementContains(tuple.item1.info.name, term); includeManager |= _elementContains(entry.value.wallet.info.name, term);
includeManager |= _elementContains( includeManager |= _elementContains(
tuple.item1.info.cachedBalance.total.decimal.toString(), entry.value.wallet.info.cachedBalance.total.decimal.toString(),
term, term,
); );
final List<EthContract> contracts = []; final List<EthContract> contracts = [];
for (final contract in tuple.item2) { for (final contract in entry.value.contracts) {
if (_elementContains(contract.name, term)) { if (_elementContains(contract.name, term)) {
contracts.add(contract); contracts.add(contract);
} else if (_elementContains(contract.symbol, term)) { } else if (_elementContains(contract.symbol, term)) {
@ -100,22 +104,19 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
} }
if (includeManager || contracts.isNotEmpty) { if (includeManager || contracts.isNotEmpty) {
results.add(Tuple2(tuple.item1, contracts)); results.addEntries([entry]);
} }
} }
return results; return results.values.toList()
..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name));
} }
bool _elementContains(String element, String term) { bool _elementContains(String element, String term) {
return element.toLowerCase().contains(term); return element.toLowerCase().contains(term);
} }
@override void updateWallets() {
void initState() {
_searchController = TextEditingController();
searchFieldFocusNode = FocusNode();
final walletsData = final walletsData =
ref.read(mainDBProvider).isar.walletInfo.where().findAllSync(); ref.read(mainDBProvider).isar.walletInfo.where().findAllSync();
walletsData.removeWhere((e) => e.coin != widget.coin); walletsData.removeWhere((e) => e.coin != widget.coin);
@ -143,28 +144,48 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
} }
// add tuple to list // add tuple to list
wallets.add( wallets[data.walletId] = (
Tuple2( wallet: ref.read(pWallets).getWallet(
ref.read(pWallets).getWallet( data.walletId,
data.walletId, ),
), contracts: contracts,
contracts,
),
); );
} }
} else { } else {
// add non token wallet tuple to list // add non token wallet tuple to list
for (final data in walletsData) { for (final data in walletsData) {
wallets.add( // desktop single coin apps may cause issues so lets just ignore the error and move on
Tuple2( try {
ref.read(pWallets).getWallet( wallets[data.walletId] = (
wallet: ref.read(pWallets).getWallet(
data.walletId, data.walletId,
), ),
[], contracts: [],
), );
); } catch (_) {
// lol bandaid for single coin based apps
}
} }
} }
}
@override
void initState() {
_searchController = TextEditingController();
searchFieldFocusNode = FocusNode();
updateWallets();
if (AppConfig.isSingleCoinApp) {
GlobalEventBus.instance.on<WalletAddedEvent>().listen((_) {
updateWallets();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
});
});
}
super.initState(); super.initState();
} }
@ -293,24 +314,27 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
final data = _filter(_searchString); final data = _filter(_searchString);
return ListView.separated( return ListView.separated(
itemBuilder: (_, index) { itemBuilder: (_, index) {
final element = data[index]; final entry = data[index];
final wallet = entry.wallet;
if (element.item1.cryptoCurrency.hasTokenSupport) { if (wallet.cryptoCurrency.hasTokenSupport) {
if (isDesktop) { if (isDesktop) {
return DesktopExpandingWalletCard( return DesktopExpandingWalletCard(
key: Key( key: Key(
"${element.item1.info.name}_${element.item2.map((e) => e.address).join()}", "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}",
), ),
data: element, data: entry,
navigatorState: widget.navigatorState!, navigatorState: widget.navigatorState!,
); );
} else { } else {
return MasterWalletCard( return MasterWalletCard(
walletId: element.item1.walletId, key: Key(wallet.walletId),
walletId: wallet.walletId,
); );
} }
} else { } else {
return ConditionalParent( return ConditionalParent(
key: Key(wallet.walletId),
condition: isDesktop, condition: isDesktop,
builder: (child) => RoundedWhiteContainer( builder: (child) => RoundedWhiteContainer(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -323,7 +347,7 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
child: child, child: child,
), ),
child: SimpleWalletCard( child: SimpleWalletCard(
walletId: element.item1.walletId, walletId: wallet.walletId,
popPrevious: widget popPrevious: widget
.overrideSimpleWalletCardPopPreviousValueWith == .overrideSimpleWalletCardPopPreviousValueWith ==
null null

View file

@ -206,7 +206,8 @@ class _StepScaffoldState extends ConsumerState<StepScaffold> {
void sendFromStack() { void sendFromStack() {
final trade = ref.read(desktopExchangeModelProvider)!.trade!; final trade = ref.read(desktopExchangeModelProvider)!.trade!;
final address = trade.payInAddress; final address = trade.payInAddress;
final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency)!; final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ??
AppConfig.getCryptoCurrencyByPrettyName(trade.payInCurrency);
final amount = Decimal.parse(trade.payInAmount).toAmount( final amount = Decimal.parse(trade.payInAmount).toAmount(
fractionDigits: coin.fractionDigits, fractionDigits: coin.fractionDigits,
); );

View file

@ -326,7 +326,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
), ),
)) ))
CustomTextButton( CustomTextButton(
text: "Choose from Stack", text: "Choose from ${AppConfig.prefix}",
onTap: selectRecipientAddressFromStack, onTap: selectRecipientAddressFromStack,
), ),
], ],
@ -472,7 +472,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
), ),
)) ))
CustomTextButton( CustomTextButton(
text: "Choose from Stack", text: "Choose from ${AppConfig.prefix}",
onTap: selectRefundAddressFromStack, onTap: selectRefundAddressFromStack,
), ),
], ],

View file

@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import '../../../app_config.dart';
import '../../../providers/providers.dart'; import '../../../providers/providers.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
import '../../../utilities/amount/amount.dart'; import '../../../utilities/amount/amount.dart';
@ -87,7 +88,7 @@ class _DesktopChooseFromStackState
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Choose from Stack", "Choose from ${AppConfig.prefix}",
style: STextStyles.desktopH3(context), style: STextStyles.desktopH3(context),
), ),
const SizedBox( const SizedBox(

View file

@ -11,7 +11,6 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@ -21,7 +20,6 @@ import '../providers/providers.dart';
import '../themes/stack_colors.dart'; import '../themes/stack_colors.dart';
import '../utilities/assets.dart'; import '../utilities/assets.dart';
import '../utilities/text_styles.dart'; import '../utilities/text_styles.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../widgets/desktop/desktop_tor_status_button.dart'; import '../widgets/desktop/desktop_tor_status_button.dart';
import '../widgets/desktop/living_stack_icon.dart'; import '../widgets/desktop/living_stack_icon.dart';
import 'desktop_menu_item.dart'; import 'desktop_menu_item.dart';
@ -296,15 +294,15 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
label: "Exit", label: "Exit",
value: 7, value: 7,
onChanged: (_) { onChanged: (_) {
// todo: save stuff/ notify before exit? // // todo: save stuff/ notify before exit?
if (AppConfig.coins // if (AppConfig.coins
.where((e) => e is Monero || e is Wownero) // .where((e) => e is Monero || e is Wownero)
.isNotEmpty) { // .isNotEmpty) {
// hack to insta kill because xmr/wow native lib code sucks // // hack to insta kill because xmr/wow native lib code sucks
exit(0); exit(0);
} else { // } else {
SystemNavigator.pop(); // SystemNavigator.pop();
} // }
}, },
controller: controllers[8], controller: controllers[8],
), ),

View file

@ -11,19 +11,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import '../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../pages/wallets_view/wallets_overview.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/text_styles.dart'; import '../../../utilities/text_styles.dart';
import '../../../wallets/wallet/wallet.dart';
import '../../../widgets/animated_widgets/rotate_icon.dart'; import '../../../widgets/animated_widgets/rotate_icon.dart';
import '../../../widgets/expandable.dart'; import '../../../widgets/expandable.dart';
import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/wallet_card.dart'; import '../../../widgets/wallet_card.dart';
import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart'; import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart';
import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart';
import 'package:tuple/tuple.dart';
class DesktopExpandingWalletCard extends StatefulWidget { class DesktopExpandingWalletCard extends StatefulWidget {
const DesktopExpandingWalletCard({ const DesktopExpandingWalletCard({
@ -32,7 +31,7 @@ class DesktopExpandingWalletCard extends StatefulWidget {
required this.navigatorState, required this.navigatorState,
}); });
final Tuple2<Wallet, List<EthContract>> data; final WalletListItemData data;
final NavigatorState navigatorState; final NavigatorState navigatorState;
@override @override
@ -48,9 +47,9 @@ class _DesktopExpandingWalletCardState
@override @override
void initState() { void initState() {
if (widget.data.item1.cryptoCurrency.hasTokenSupport) { if (widget.data.wallet.cryptoCurrency.hasTokenSupport) {
tokenContractAddresses.addAll( tokenContractAddresses.addAll(
widget.data.item2.map((e) => e.address), widget.data.contracts.map((e) => e.address),
); );
} }
@ -63,7 +62,7 @@ class _DesktopExpandingWalletCardState
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
borderColor: Theme.of(context).extension<StackColors>()!.backgroundAppBar, borderColor: Theme.of(context).extension<StackColors>()!.backgroundAppBar,
child: Expandable( child: Expandable(
initialState: widget.data.item1.cryptoCurrency.hasTokenSupport initialState: widget.data.wallet.cryptoCurrency.hasTokenSupport
? ExpandableState.expanded ? ExpandableState.expanded
: ExpandableState.collapsed, : ExpandableState.collapsed,
controller: expandableController, controller: expandableController,
@ -89,13 +88,13 @@ class _DesktopExpandingWalletCardState
child: Row( child: Row(
children: [ children: [
WalletInfoCoinIcon( WalletInfoCoinIcon(
coin: widget.data.item1.info.coin, coin: widget.data.wallet.info.coin,
), ),
const SizedBox( const SizedBox(
width: 12, width: 12,
), ),
Text( Text(
widget.data.item1.info.name, widget.data.wallet.info.name,
style: STextStyles.desktopTextExtraSmall(context) style: STextStyles.desktopTextExtraSmall(context)
.copyWith( .copyWith(
color: Theme.of(context) color: Theme.of(context)
@ -109,7 +108,7 @@ class _DesktopExpandingWalletCardState
Expanded( Expanded(
flex: 4, flex: 4,
child: WalletInfoRowBalance( child: WalletInfoRowBalance(
walletId: widget.data.item1.walletId, walletId: widget.data.wallet.walletId,
), ),
), ),
], ],
@ -173,7 +172,7 @@ class _DesktopExpandingWalletCardState
bottom: 14, bottom: 14,
), ),
child: SimpleWalletCard( child: SimpleWalletCard(
walletId: widget.data.item1.walletId, walletId: widget.data.wallet.walletId,
popPrevious: true, popPrevious: true,
desktopNavigatorState: widget.navigatorState, desktopNavigatorState: widget.navigatorState,
), ),
@ -187,7 +186,7 @@ class _DesktopExpandingWalletCardState
bottom: 14, bottom: 14,
), ),
child: SimpleWalletCard( child: SimpleWalletCard(
walletId: widget.data.item1.walletId, walletId: widget.data.wallet.walletId,
contractAddress: e, contractAddress: e,
popPrevious: true, popPrevious: true,
desktopNavigatorState: widget.navigatorState, desktopNavigatorState: widget.navigatorState,

View file

@ -182,7 +182,9 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
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 || showMultiType = supportsSpark ||
ref.read(pWallets).getWallet(walletId) is MultiAddressInterface; (wallet is! BCashInterface &&
wallet is Bip39HDWallet &&
wallet.supportedAddressTypes.length > 1);
_walletAddressTypes.add(wallet.info.mainAddressType); _walletAddressTypes.add(wallet.info.mainAddressType);

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import '../../../../../app_config.dart';
import '../../../../../db/sqlite/firo_cache.dart'; import '../../../../../db/sqlite/firo_cache.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';
@ -36,7 +37,6 @@ import '../../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../../widgets/desktop/primary_button.dart'; import '../../../../../widgets/desktop/primary_button.dart';
import '../../../../../widgets/desktop/secondary_button.dart'; import '../../../../../widgets/desktop/secondary_button.dart';
import '../../../../../widgets/rounded_container.dart'; import '../../../../../widgets/rounded_container.dart';
import '../../../../../widgets/stack_dialog.dart';
class MoreFeaturesDialog extends ConsumerStatefulWidget { class MoreFeaturesDialog extends ConsumerStatefulWidget {
const MoreFeaturesDialog({ const MoreFeaturesDialog({
@ -113,102 +113,72 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
context: context, context: context,
builder: (context) { builder: (context) {
final isDesktop = Util.isDesktop; final isDesktop = Util.isDesktop;
return isDesktop return DesktopDialog(
? DesktopDialog( maxWidth: 576,
maxWidth: 576, child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Warning!",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Padding(
padding: const EdgeInsets.only(
top: 8,
left: 32,
right: 32,
bottom: 32,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(
height: 43,
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Padding( Expanded(
padding: const EdgeInsets.only(left: 32), child: SecondaryButton(
child: Text( buttonHeight: ButtonHeight.l,
"Warning!", onPressed: () {
style: STextStyles.desktopH3(context), Navigator.of(context).pop(false);
},
label: "Cancel",
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
onPressed: () {
Navigator.of(context).pop(true);
},
label: "Continue",
), ),
), ),
const DesktopDialogCloseButton(),
], ],
), ),
Padding(
padding: const EdgeInsets.only(
top: 8,
left: 32,
right: 32,
bottom: 32,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
style: STextStyles.desktopTextSmall(context),
),
const SizedBox(
height: 43,
),
Row(
children: [
Expanded(
child: SecondaryButton(
buttonHeight: ButtonHeight.l,
onPressed: () {
Navigator.of(context).pop(false);
},
label: "Cancel",
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
buttonHeight: ButtonHeight.l,
onPressed: () {
Navigator.of(context).pop(true);
},
label: "Continue",
),
),
],
),
],
),
),
], ],
), ),
) ),
: StackDialog( ],
title: "Warning!", ),
message: );
"Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?",
leftButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(context),
child: Text(
"Cancel",
style: STextStyles.itemSubtitle12(context),
),
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
child: Text(
"Continue",
style: STextStyles.button(context),
),
onPressed: () {
Navigator.of(context).pop(true);
},
),
);
}, },
).then((confirmed) async { ).then((confirmed) async {
if (_switchReuseAddressToggledLock) { if (_switchReuseAddressToggledLock) {
@ -312,7 +282,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
if (wallet is OrdinalsInterface) if (wallet is OrdinalsInterface)
_MoreFeaturesItem( _MoreFeaturesItem(
label: "Ordinals", label: "Ordinals",
detail: "View and control your ordinals in Stack", detail: "View and control your ordinals in ${AppConfig.prefix}",
iconAsset: Assets.svg.ordinal, iconAsset: Assets.svg.ordinal,
onPressed: () async => widget.onOrdinalsPressed?.call(), onPressed: () async => widget.onOrdinalsPressed?.call(),
), ),
@ -422,7 +392,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Reuse receiving address by default", "Reuse receiving address",
style: STextStyles.w600_20(context), style: STextStyles.w600_20(context),
), ),
], ],

View file

@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../../app_config.dart';
import '../../db/hive/db.dart'; import '../../db/hive/db.dart';
import '../../notifications/show_flush_bar.dart'; import '../../notifications/show_flush_bar.dart';
import '../../pages/intro_view.dart'; import '../../pages/intro_view.dart';
@ -143,7 +144,7 @@ class _ForgotPasswordDesktopViewState
), ),
TextSpan( TextSpan(
text: widget.shouldCreateNew text: widget.shouldCreateNew
? "create a new Stack" ? "create a new ${AppConfig.prefix}"
: "restore from backup", : "restore from backup",
style: STextStyles.desktopTextSmallBold(context), style: STextStyles.desktopTextSmallBold(context),
), ),

View file

@ -74,7 +74,9 @@ class _ForgotPasswordDesktopViewState
SizedBox( SizedBox(
width: 400, width: 400,
child: Text( child: Text(
"${AppConfig.appName} does not store your password. Create new wallet or use a Stack backup file to restore your wallet.", "${AppConfig.appName} does not store your password. "
"Create new wallet or use a ${AppConfig.prefix} "
"backup file to restore your wallet.",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: STextStyles.desktopTextSmall(context).copyWith( style: STextStyles.desktopTextSmall(context).copyWith(
color: Theme.of(context) color: Theme.of(context)
@ -87,7 +89,7 @@ class _ForgotPasswordDesktopViewState
height: 48, height: 48,
), ),
PrimaryButton( PrimaryButton(
label: "Create new Stack", label: "Create new ${AppConfig.prefix}",
onPressed: () { onPressed: () {
const shouldCreateNew = true; const shouldCreateNew = true;
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
@ -100,7 +102,7 @@ class _ForgotPasswordDesktopViewState
height: 24, height: 24,
), ),
SecondaryButton( SecondaryButton(
label: "Restore from Stack backup", label: "Restore from ${AppConfig.prefix} backup",
onPressed: () { onPressed: () {
const shouldCreateNew = false; const shouldCreateNew = false;
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(

View file

@ -94,7 +94,7 @@ class _ForgottenPassphraseRestoreFromSWBState
color: Colors.transparent, color: Colors.transparent,
child: Center( child: Center(
child: Text( child: Text(
"Decrypting Stack backup file", "Decrypting ${AppConfig.prefix} backup file",
style: STextStyles.pageTitleH2(context).copyWith( style: STextStyles.pageTitleH2(context).copyWith(
color: color:
Theme.of(context).extension<StackColors>()!.textWhite, Theme.of(context).extension<StackColors>()!.textWhite,
@ -245,7 +245,7 @@ class _ForgottenPassphraseRestoreFromSWBState
height: 32, height: 32,
), ),
Text( Text(
"Use your Stack wallet backup file to restore your wallets, address book, and wallet preferences.", "Use your ${AppConfig.prefix} backup file to restore your wallets, address book, and wallet preferences.",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: STextStyles.desktopTextSmall(context).copyWith( style: STextStyles.desktopTextSmall(context).copyWith(
color: Theme.of(context) color: Theme.of(context)

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import '../../../../app_config.dart';
import '../../../../pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart'; import '../../../../pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart';
import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/prefs_provider.dart';
import '../../../../themes/stack_colors.dart'; import '../../../../themes/stack_colors.dart';
@ -225,7 +226,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
"Stack Experience", "${AppConfig.prefix} Experience",
style: STextStyles.desktopTextExtraSmall( style: STextStyles.desktopTextExtraSmall(
context, context,
).copyWith( ).copyWith(

View file

@ -226,9 +226,9 @@ class _DesktopEditBlockExplorerDialogState
" every block explorer has a slightly different URL scheme." " every block explorer has a slightly different URL scheme."
"\n\n" "\n\n"
"Paste in your block explorer of choice, then edit in" "Paste in your block explorer of choice, then edit in"
" [TXID] where the transaction ID should go, and Stack" " [TXID] where the transaction ID should go, and "
" Wallet will auto fill the transaction ID in that place" "${AppConfig.appName} will auto fill the transaction"
" of the URL.", " ID in that place of the URL.",
style: STextStyles.desktopTextExtraExtraSmall(context), style: STextStyles.desktopTextExtraExtraSmall(context),
), ),
), ),

View file

@ -15,6 +15,7 @@ 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:flutter_svg/svg.dart';
import '../../../../app_config.dart';
import '../../../../db/hive/db.dart'; import '../../../../db/hive/db.dart';
import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/prefs_provider.dart';
import '../../../../providers/global/price_provider.dart'; import '../../../../providers/global/price_provider.dart';
@ -69,7 +70,7 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> {
Padding( Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Text( child: Text(
"Choose Your Stack Experience", "Choose Your ${AppConfig.prefix} Experience",
style: STextStyles.desktopH3(context), style: STextStyles.desktopH3(context),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -192,9 +193,11 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> {
) )
.then((_) { .then((_) {
if (isEasy) { if (isEasy) {
unawaited( if (AppConfig.hasFeature(AppFeature.swap)) {
ExchangeDataLoadingService.instance.loadAll(), unawaited(
); ExchangeDataLoadingService.instance.loadAll(),
);
}
ref ref
.read(priceAnd24hChangeNotifierProvider) .read(priceAnd24hChangeNotifierProvider)
.start(true); .start(true);

View file

@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart';
import 'package:zxcvbn/zxcvbn.dart'; import 'package:zxcvbn/zxcvbn.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart'; import '../../../../notifications/show_flush_bar.dart';
import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart';
@ -776,7 +777,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return StackOkDialog( return StackOkDialog(
title: title:
"Stack Auto Backup enabled and saved to:", "${AppConfig.prefix} Auto Backup enabled and saved to:",
message: fileToSave, message: fileToSave,
); );
} else if (Util.isDesktop) { } else if (Util.isDesktop) {
@ -800,7 +801,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
.spaceBetween, .spaceBetween,
children: [ children: [
Text( Text(
"Stack Auto Backup enabled!", "${AppConfig.prefix} Auto Backup enabled!",
style: style:
STextStyles.desktopH3( STextStyles.desktopH3(
context, context,
@ -834,7 +835,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
); );
} else { } else {
return const StackOkDialog( return const StackOkDialog(
title: "Stack Auto Backup enabled!", title:
"${AppConfig.prefix} Auto Backup enabled!",
); );
} }
}, },

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import '../../../app_config.dart';
import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
import '../../../providers/global/prefs_provider.dart'; import '../../../providers/global/prefs_provider.dart';
import '../../../themes/stack_colors.dart'; import '../../../themes/stack_colors.dart';
@ -115,7 +116,7 @@ class _SyncingPreferencesSettings
), ),
TextSpan( TextSpan(
text: text:
"\n\nSet up your syncing preferences for all wallets in your Stack.", "\n\nSet up your syncing preferences for all wallets in your ${AppConfig.prefix}.",
style: STextStyles.desktopTextExtraExtraSmall( style: STextStyles.desktopTextExtraExtraSmall(
context, context,
), ),

View file

@ -15,6 +15,7 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../app_config.dart';
import '../models/isar/models/log.dart'; import '../models/isar/models/log.dart';
import '../utilities/logger.dart'; import '../utilities/logger.dart';
@ -98,7 +99,7 @@ class DebugService extends ChangeNotifier {
Future<String> exportToFile(String directory, EventBus eventBus) async { Future<String> exportToFile(String directory, EventBus eventBus) async {
final now = DateTime.now(); final now = DateTime.now();
final filename = final filename =
"Stack_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt"; "${AppConfig.prefix}_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt";
final filepath = "$directory/$filename"; final filepath = "$directory/$filename";
final File file = await File(filepath).create(); final File file = await File(filepath).create();

View file

@ -0,0 +1 @@
class WalletAddedEvent {}

View file

@ -284,7 +284,13 @@ abstract class Frost {
static String createSignConfig({ static String createSignConfig({
required int network, required int network,
required List<({UTXO utxo, Uint8List scriptPubKey})> inputs, required List<
({
UTXO utxo,
Uint8List scriptPubKey,
AddressDerivationData addressDerivationData
})>
inputs,
required List<({String address, Amount amount, bool isChange})> outputs, required List<({String address, Amount amount, bool isChange})> outputs,
required String changeAddress, required String changeAddress,
required int feePerWeight, required int feePerWeight,
@ -299,6 +305,7 @@ abstract class Frost {
vout: e.utxo.vout, vout: e.utxo.vout,
value: e.utxo.value, value: e.utxo.value,
scriptPubKey: e.scriptPubKey, scriptPubKey: e.scriptPubKey,
addressDerivationData: e.addressDerivationData,
), ),
) )
.toList(), .toList(),

View file

@ -8,13 +8,16 @@
* *
*/ */
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../models/notification_model.dart'; import '../models/notification_model.dart';
import '../utilities/prefs.dart'; import '../utilities/prefs.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
class NotificationApi { abstract final class NotificationApi {
static Completer<void>? _initCalledCompleter;
static final _notifications = FlutterLocalNotificationsPlugin(); static final _notifications = FlutterLocalNotificationsPlugin();
// static final onNotifications = BehaviorSubject<String?>(); // static final onNotifications = BehaviorSubject<String?>();
@ -33,6 +36,16 @@ class NotificationApi {
} }
static Future<void> init({bool initScheduled = false}) async { static Future<void> init({bool initScheduled = false}) async {
if (_initCalledCompleter == null) {
_initCalledCompleter = Completer<void>();
} else {
if (_initCalledCompleter!.isCompleted) {
return;
} else {
return await _initCalledCompleter!.future;
}
}
const android = AndroidInitializationSettings('app_icon_alpha'); const android = AndroidInitializationSettings('app_icon_alpha');
const iOS = DarwinInitializationSettings(); const iOS = DarwinInitializationSettings();
const linux = LinuxInitializationSettings( const linux = LinuxInitializationSettings(
@ -54,12 +67,18 @@ class NotificationApi {
// onNotifications.add(payload.payload); // onNotifications.add(payload.payload);
// }, // },
); );
_initCalledCompleter!.complete();
} }
static Future<void> clearNotifications() async => _notifications.cancelAll(); static Future<void> clearNotifications() async {
await init();
await _notifications.cancelAll();
}
static Future<void> clearNotification(int id) async => static Future<void> clearNotification(int id) async {
_notifications.cancel(id); await init();
await _notifications.cancel(id);
}
//=================================== //===================================
static late Prefs prefs; static late Prefs prefs;
@ -79,6 +98,7 @@ class NotificationApi {
String? changeNowId, String? changeNowId,
String? payload, String? payload,
}) async { }) async {
await init();
await prefs.incrementCurrentNotificationIndex(); await prefs.incrementCurrentNotificationIndex();
final id = prefs.currentNotificationId; final id = prefs.currentNotificationId;

View file

@ -13,12 +13,10 @@ import 'dart:async';
import 'package:flutter_libmonero/monero/monero.dart' as monero; import 'package:flutter_libmonero/monero/monero.dart' as monero;
import 'package:flutter_libmonero/wownero/wownero.dart' as wownero; import 'package:flutter_libmonero/wownero/wownero.dart' as wownero;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../app_config.dart';
import '../db/hive/db.dart'; import '../db/hive/db.dart';
import '../db/isar/main_db.dart'; import '../db/isar/main_db.dart';
import 'node_service.dart';
import 'notifications_service.dart';
import 'trade_sent_from_stack_service.dart';
import '../app_config.dart';
import '../utilities/enums/sync_type_enum.dart'; import '../utilities/enums/sync_type_enum.dart';
import '../utilities/flutter_secure_storage_interface.dart'; import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart'; import '../utilities/logger.dart';
@ -28,6 +26,11 @@ import '../wallets/isar/models/wallet_info.dart';
import '../wallets/wallet/impl/epiccash_wallet.dart'; import '../wallets/wallet/impl/epiccash_wallet.dart';
import '../wallets/wallet/wallet.dart'; import '../wallets/wallet/wallet.dart';
import '../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import 'event_bus/events/wallet_added_event.dart';
import 'event_bus/global_event_bus.dart';
import 'node_service.dart';
import 'notifications_service.dart';
import 'trade_sent_from_stack_service.dart';
class Wallets { class Wallets {
Wallets._private(); Wallets._private();
@ -59,6 +62,7 @@ class Wallets {
); );
} }
_wallets[wallet.walletId] = wallet; _wallets[wallet.walletId] = wallet;
GlobalEventBus.instance.fire(WalletAddedEvent());
} }
Future<void> deleteWallet( Future<void> deleteWallet(

View file

@ -31,7 +31,9 @@ final pThemeService = Provider<ThemeService>((ref) {
}); });
class ThemeService { class ThemeService {
static const _currentDefaultThemeVersion = 10; // dumb quick conditional based on name. Should really be done better
static const _currentDefaultThemeVersion =
AppConfig.appName == "Campfire" ? 17 : 15;
ThemeService._(); ThemeService._();
static ThemeService? _instance; static ThemeService? _instance;
static ThemeService get instance => _instance ??= ThemeService._(); static ThemeService get instance => _instance ??= ThemeService._();

View file

@ -8,11 +8,12 @@
* *
*/ */
import '../app_config.dart';
import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/crypto_currency/crypto_currency.dart';
abstract class DefaultNodes { abstract class DefaultNodes {
static const String defaultNodeIdPrefix = "default_"; static const String defaultNodeIdPrefix = "default_";
static String buildId(CryptoCurrency cryptoCurrency) => static String buildId(CryptoCurrency cryptoCurrency) =>
"$defaultNodeIdPrefix${cryptoCurrency.identifier}"; "$defaultNodeIdPrefix${cryptoCurrency.identifier}";
static const String defaultName = "Stack Default"; static const String defaultName = "${AppConfig.prefix} Default";
} }

View file

@ -127,9 +127,21 @@ class BitcoinFrost extends FrostCurrency {
); );
@override @override
String pubKeyToScriptHash({required Uint8List pubKey}) { Uint8List addressToPubkey({required String address}) {
try { try {
return Bip39HDCurrency.convertBytesToScriptHash(pubKey); final addr = coinlib.Address.fromString(address, networkParams);
return addr.program.script.compiled;
} catch (e) {
rethrow;
}
}
@override
String addressToScriptHash({required String address}) {
try {
return Bip39HDCurrency.convertBytesToScriptHash(
addressToPubkey(address: address),
);
} catch (e) { } catch (e) {
rethrow; rethrow;
} }

View file

@ -6,7 +6,11 @@ import '../crypto_currency.dart';
abstract class FrostCurrency extends CryptoCurrency { abstract class FrostCurrency extends CryptoCurrency {
FrostCurrency(super.network); FrostCurrency(super.network);
String pubKeyToScriptHash({required Uint8List pubKey}); // String pubKeyToScriptHash({required Uint8List pubKey});
String addressToScriptHash({required String address});
Uint8List addressToPubkey({required String address});
Amount get dustLimit; Amount get dustLimit;
} }

View file

@ -1,10 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:frostdart/frostdart.dart' as frost; import 'package:frostdart/frostdart.dart' as frost;
import 'package:frostdart/frostdart_bindings_generated.dart'; import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:frostdart/util.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart';
import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart';
import '../../../models/balance.dart'; import '../../../models/balance.dart';
@ -24,10 +27,13 @@ import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/intermediate/frost_currency.dart'; import '../../crypto_currency/intermediate/frost_currency.dart';
import '../../isar/models/frost_wallet_info.dart'; import '../../isar/models/frost_wallet_info.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.dart'; import '../../models/tx_data.dart';
import '../wallet.dart'; import '../wallet.dart';
import '../wallet_mixin_interfaces/multi_address_interface.dart';
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
with MultiAddressInterface {
BitcoinFrostWallet(CryptoCurrencyNetwork network) BitcoinFrostWallet(CryptoCurrencyNetwork network)
: super(BitcoinFrost(network) as T); : super(BitcoinFrost(network) as T);
@ -77,25 +83,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
await mainDB.isar.frostWalletInfo.put(frostWalletInfo); await mainDB.isar.frostWalletInfo.put(frostWalletInfo);
}); });
final keys = frost.deserializeKeys(keys: serializedKeys); final address = await _generateAddress(
change: 0,
final addressString = frost.addressForKeys( index: 0,
network: cryptoCurrency.network == CryptoCurrencyNetwork.main serializedKeys: serializedKeys,
? Network.Mainnet
: Network.Testnet,
keys: keys,
);
final publicKey = frost.scriptPubKeyForKeys(keys: keys);
final address = Address(
walletId: info.walletId,
value: addressString,
publicKey: publicKey.toUint8ListFromHex,
derivationIndex: 0,
derivationPath: null,
subType: AddressSubType.receiving,
type: AddressType.unknown,
); );
await mainDB.putAddresses([address]); await mainDB.putAddresses([address]);
@ -110,7 +101,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
Future<TxData> frostCreateSignConfig({ Future<TxData> frostCreateSignConfig({
required TxData txData, required TxData txData,
required String changeAddress,
required int feePerWeight, required int feePerWeight,
}) async { }) async {
try { try {
@ -152,44 +142,86 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
); );
final Set<UTXO> utxosToUse = {}; final Set<UTXO> utxosToUse = {};
for (final utxo in utxos) { final Set<UTXO> utxosRemaining = {};
for (int i = 0; i < utxos.length; i++) {
final utxo = utxos[i];
sum += Amount( sum += Amount(
rawValue: BigInt.from(utxo.value), rawValue: BigInt.from(utxo.value),
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
); );
utxosToUse.add(utxo); utxosToUse.add(utxo);
if (sum > total) { if (sum > total) {
if (i + 1 < utxos.length) {
utxosRemaining.addAll(utxos.sublist(i));
}
break; break;
} }
} }
final serializedKeys = await getSerializedKeys();
final keys = frost.deserializeKeys(keys: serializedKeys!);
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet ? Network.Mainnet
: Network.Testnet; : Network.Testnet;
final publicKey = frost final List<
.scriptPubKeyForKeys( ({
keys: keys, UTXO utxo,
) Uint8List scriptPubKey,
.toUint8ListFromHex; ({int account, int index, bool change}) addressDerivationData
})> inputs = [];
final config = Frost.createSignConfig( for (final utxo in utxosToUse) {
network: network, final dData = await getDerivationData(
inputs: utxosToUse utxo.address,
.map( );
(e) => ( final publicKey = cryptoCurrency.addressToPubkey(
utxo: e, address: utxo.address!,
);
inputs.add(
(
utxo: utxo,
scriptPubKey: publicKey,
addressDerivationData: dData,
),
);
}
await checkChangeAddressForTransactions();
final changeAddress = await getCurrentChangeAddress();
String? config;
while (config == null) {
try {
config = Frost.createSignConfig(
network: network,
inputs: inputs,
outputs: txData.recipients!,
changeAddress: changeAddress!.value,
feePerWeight: feePerWeight,
);
} on FrostdartException catch (e) {
if (e.errorCode == NOT_ENOUGH_FUNDS_ERROR &&
utxosRemaining.isNotEmpty) {
// add extra utxo
final utxo = utxosRemaining.take(1).first;
final dData = await getDerivationData(
utxo.address,
);
final publicKey = cryptoCurrency.addressToPubkey(
address: utxo.address!,
);
inputs.add(
(
utxo: utxo,
scriptPubKey: publicKey, scriptPubKey: publicKey,
addressDerivationData: dData,
), ),
) );
.toList(), } else {
outputs: txData.recipients!, rethrow;
changeAddress: (await getCurrentReceivingAddress())!.value, }
feePerWeight: feePerWeight, }
); }
return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); return txData.copyWith(frostMSConfig: config, utxos: utxosToUse);
} catch (_) { } catch (_) {
@ -197,6 +229,44 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
} }
} }
Future<({int account, int index, bool change})> getDerivationData(
String? address,
) async {
if (address == null) {
throw Exception("Missing address required for FROST signing");
}
final addr = await mainDB.getAddress(walletId, address);
if (addr == null) {
throw Exception("Missing address in DB required for FROST signing");
}
final dPath = addr.derivationPath?.value ?? "0/0/0";
try {
final components = dPath.split("/").map((e) => int.parse(e)).toList();
if (components.length != 3) {
throw Exception(
"Unexpected derivation data `$components` for FROST signing",
);
}
if (components[1] != 0 && components[1] != 1) {
throw Exception(
"${components[1]} must be 1 or 0 for change",
);
}
return (
account: components[0],
change: components[1] == 1,
index: components[2],
);
} catch (_) {
rethrow;
}
}
Future< Future<
({ ({
Pointer<TransactionSignMachineWrapper> machinePtr, Pointer<TransactionSignMachineWrapper> machinePtr,
@ -324,16 +394,28 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
@override @override
Future<void> updateTransactions() async { Future<void> updateTransactions() async {
final myAddress = (await getCurrentReceivingAddress())!; // Get all addresses.
final List<Address> allAddressesOld =
await _fetchAddressesForElectrumXScan();
final scriptHash = cryptoCurrency.pubKeyToScriptHash( // Separate receiving and change addresses.
pubKey: Uint8List.fromList(myAddress.publicKey), final Set<String> receivingAddresses = allAddressesOld
); .where((e) => e.subType == AddressSubType.receiving)
final allTxHashes = .map((e) => e.value)
(await electrumXClient.getHistory(scripthash: scriptHash)).toSet(); .toSet();
final Set<String> changeAddresses = allAddressesOld
.where((e) => e.subType == AddressSubType.change)
.map((e) => e.value)
.toSet();
// Remove duplicates.
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
final currentHeight = await chainHeight; final currentHeight = await chainHeight;
final coin = info.coin;
// Fetch history from ElectrumX.
final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddressesSet);
final List<Map<String, dynamic>> allTransactions = []; final List<Map<String, dynamic>> allTransactions = [];
@ -350,7 +432,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
final tx = await electrumXCachedClient.getTransaction( final tx = await electrumXCachedClient.getTransaction(
txHash: txHash["tx_hash"] as String, txHash: txHash["tx_hash"] as String,
verbose: true, verbose: true,
cryptoCurrency: coin, cryptoCurrency: cryptoCurrency,
); );
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
@ -371,6 +453,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
// Parse inputs. // Parse inputs.
BigInt amountReceivedInThisWallet = BigInt.zero; BigInt amountReceivedInThisWallet = BigInt.zero;
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
final List<InputV2> inputs = []; final List<InputV2> inputs = [];
for (final jsonInput in txData["vin"] as List) { for (final jsonInput in txData["vin"] as List) {
final map = Map<String, dynamic>.from(jsonInput as Map); final map = Map<String, dynamic>.from(jsonInput as Map);
@ -421,7 +504,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
); );
// Check if input was from this wallet. // Check if input was from this wallet.
if (input.addresses.contains(myAddress.value)) { if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
wasSentFromThisWallet = true; wasSentFromThisWallet = true;
input = input.copyWith(walletOwns: true); input = input.copyWith(walletOwns: true);
} }
@ -441,10 +524,18 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
); );
// If output was to my wallet, add value to amount received. // If output was to my wallet, add value to amount received.
if (output.addresses.contains(myAddress.value)) { if (receivingAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true; wasReceivedInThisWallet = true;
amountReceivedInThisWallet += output.value; amountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true); output = output.copyWith(walletOwns: true);
} else if (changeAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
changeAmountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
} }
outputs.add(output); outputs.add(output);
@ -478,7 +569,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
type = TransactionType.outgoing; type = TransactionType.outgoing;
if (wasReceivedInThisWallet) { if (wasReceivedInThisWallet) {
if (amountReceivedInThisWallet == totalOut) { if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
totalOut) {
// Definitely sent all to self. // Definitely sent all to self.
type = TransactionType.sentToSelf; type = TransactionType.sentToSelf;
} else if (amountReceivedInThisWallet == BigInt.zero) { } else if (amountReceivedInThisWallet == BigInt.zero) {
@ -488,6 +580,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
} else if (wasReceivedInThisWallet) { } else if (wasReceivedInThisWallet) {
// Only found outputs owned by this wallet. // Only found outputs owned by this wallet.
type = TransactionType.incoming; type = TransactionType.incoming;
// TODO: [prio=none] Check for special Bitcoin outputs like ordinals.
} else { } else {
Logging.instance.log( Logging.instance.log(
"Unexpected tx found (ignoring it): $txData", "Unexpected tx found (ignoring it): $txData",
@ -524,25 +618,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
if (address == null) { if (address == null) {
final serializedKeys = await getSerializedKeys(); final serializedKeys = await getSerializedKeys();
if (serializedKeys != null) { if (serializedKeys != null) {
final keys = frost.deserializeKeys(keys: serializedKeys); final address = await _generateAddress(
change: 0,
final addressString = frost.addressForKeys( index: 0,
network: cryptoCurrency.network == CryptoCurrencyNetwork.main serializedKeys: serializedKeys,
? Network.Mainnet
: Network.Testnet,
keys: keys,
);
final publicKey = frost.scriptPubKeyForKeys(keys: keys);
final address = Address(
walletId: walletId,
value: addressString,
publicKey: publicKey.toUint8ListFromHex,
derivationIndex: 0,
derivationPath: null,
subType: AddressSubType.receiving,
type: AddressType.frostMS,
); );
await mainDB.updateOrPutAddresses([address]); await mainDB.updateOrPutAddresses([address]);
@ -729,30 +808,79 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
await mainDB.deleteWalletBlockchainData(walletId); await mainDB.deleteWalletBlockchainData(walletId);
} }
final keys = frost.deserializeKeys(keys: serializedKeys!);
await _saveSerializedKeys(serializedKeys!); await _saveSerializedKeys(serializedKeys!);
await _saveMultisigConfig(multisigConfig!); await _saveMultisigConfig(multisigConfig!);
final addressString = frost.addressForKeys( const receiveChain = 0;
network: cryptoCurrency.network == CryptoCurrencyNetwork.main const changeChain = 1;
? Network.Mainnet final List<Future<({int index, List<Address> addresses})>>
: Network.Testnet, receiveFutures = [
keys: keys, _checkGapsLinearly(
serializedKeys,
receiveChain,
),
];
final List<Future<({int index, List<Address> addresses})>>
changeFutures = [
_checkGapsLinearly(
serializedKeys,
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];
final List<Address> addressesToStore = [];
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,
); );
final publicKey = frost.scriptPubKeyForKeys(keys: keys); await mainDB.updateOrPutAddresses(addressesToStore);
final address = Address(
walletId: walletId,
value: addressString,
publicKey: publicKey.toUint8ListFromHex,
derivationIndex: 0,
derivationPath: null,
subType: AddressSubType.receiving,
type: AddressType.frostMS,
);
await mainDB.updateOrPutAddresses([address]);
}); });
GlobalEventBus.instance.fire( GlobalEventBus.instance.fire(
@ -868,23 +996,31 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
@override @override
Future<bool> updateUTXOs() async { Future<bool> updateUTXOs() async {
final address = await getCurrentReceivingAddress(); final allAddresses = await _fetchAddressesForElectrumXScan();
try { try {
final scriptHash = cryptoCurrency.pubKeyToScriptHash( final fetchedUtxoList = <List<Map<String, dynamic>>>[];
pubKey: Uint8List.fromList(address!.publicKey), for (int i = 0; i < allAddresses.length; i++) {
); final scriptHash = cryptoCurrency.addressToScriptHash(
address: allAddresses[i].value,
);
final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash); final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash);
if (utxos.isNotEmpty) {
fetchedUtxoList.add(utxos);
}
}
final List<UTXO> outputArray = []; final List<UTXO> outputArray = [];
for (int i = 0; i < utxos.length; i++) { for (int i = 0; i < fetchedUtxoList.length; i++) {
final utxo = await _parseUTXO( for (int j = 0; j < fetchedUtxoList[i].length; j++) {
jsonUTXO: utxos[i], final utxo = await _parseUTXO(
); jsonUTXO: fetchedUtxoList[i][j],
);
outputArray.add(utxo); outputArray.add(utxo);
}
} }
return await mainDB.updateUTXOs(walletId, outputArray); return await mainDB.updateUTXOs(walletId, outputArray);
@ -1174,4 +1310,389 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
return utxo; return utxo;
} }
@override
Future<void> checkChangeAddressForTransactions() async {
try {
final currentChange = await getCurrentChangeAddress();
final bool needsGenerate;
if (currentChange == null) {
// no addresses in db yet for some reason.
// Should not happen at this point...
needsGenerate = true;
} else {
final txCount = await _fetchTxCount(address: currentChange);
needsGenerate = txCount > 0 || currentChange.derivationIndex < 0;
}
if (needsGenerate) {
await generateNewChangeAddress();
// TODO: get rid of this? Could cause problems (long loading/infinite loop or something)
// keep checking until address with no tx history is set as current
await checkChangeAddressForTransactions();
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkChangeAddressForTransactions"
"($cryptoCurrency): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
@override
Future<void> checkReceivingAddressForTransactions() async {
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
try {
throw Exception();
} catch (_, s) {
Logging.instance.log(
"checkReceivingAddressForTransactions called but reuse address flag set: $s",
level: LogLevel.Error,
);
}
}
try {
final currentReceiving = await getCurrentReceivingAddress();
final bool needsGenerate;
if (currentReceiving == null) {
// no addresses in db yet for some reason.
// Should not happen at this point...
needsGenerate = true;
} else {
final txCount = await _fetchTxCount(address: currentReceiving);
needsGenerate = txCount > 0 || currentReceiving.derivationIndex < 0;
}
if (needsGenerate) {
await generateNewReceivingAddress();
// TODO: [prio=low] Make sure we scan all addresses but only show one.
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
// TODO: get rid of this? Could cause problems (long loading/infinite loop or something)
// keep checking until address with no tx history is set as current
await checkReceivingAddressForTransactions();
}
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from _checkReceivingAddressForTransactions"
"($cryptoCurrency): $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
@override
Future<void> generateNewChangeAddress() async {
final current = await getCurrentChangeAddress();
int index = current == null ? 0 : current.derivationIndex + 1;
const chain = 1; // change address
final serializedKeys = (await getSerializedKeys())!;
Address? address;
while (address == null) {
try {
address = await _generateAddress(
change: chain,
index: index,
serializedKeys: serializedKeys,
);
} on FrostdartException catch (e) {
if (e.errorCode == 72) {
// rust doesn't like the addressDerivationData
index++;
continue;
} else {
rethrow;
}
}
}
await mainDB.updateOrPutAddresses([address]);
}
@override
Future<void> generateNewReceivingAddress() async {
final current = await getCurrentReceivingAddress();
int index = current == null ? 0 : current.derivationIndex + 1;
const chain = 0; // receiving address
final serializedKeys = (await getSerializedKeys())!;
Address? address;
while (address == null) {
try {
address = await _generateAddress(
change: chain,
index: index,
serializedKeys: serializedKeys,
);
} on FrostdartException catch (e) {
if (e.errorCode == 72) {
// rust doesn't like the addressDerivationData
index++;
continue;
} else {
rethrow;
}
}
}
await mainDB.updateOrPutAddresses([address]);
await info.updateReceivingAddress(
newAddress: address.value,
isar: mainDB.isar,
);
}
Future<void> lookAhead() async {
Address? currentReceiving = await getCurrentReceivingAddress();
if (currentReceiving == null) {
await generateNewReceivingAddress();
currentReceiving = await getCurrentReceivingAddress();
}
Address? currentChange = await getCurrentChangeAddress();
if (currentChange == null) {
await generateNewChangeAddress();
currentChange = await getCurrentChangeAddress();
}
final List<Address> nextReceivingAddresses = [];
final List<Address> nextChangeAddresses = [];
int receiveIndex = currentReceiving!.derivationIndex;
int changeIndex = currentChange!.derivationIndex;
for (int i = 0; i < 10; i++) {
final receiveAddress = await _generateAddressSafe(
chain: 0,
startingIndex: receiveIndex + 1,
);
receiveIndex = receiveAddress.derivationIndex;
nextReceivingAddresses.add(receiveAddress);
final changeAddress = await _generateAddressSafe(
chain: 1,
startingIndex: changeIndex + 1,
);
changeIndex = changeAddress.derivationIndex;
nextChangeAddresses.add(changeAddress);
}
int activeReceiveIndex = currentReceiving.derivationIndex;
int activeChangeIndex = currentChange.derivationIndex;
for (final address in nextReceivingAddresses) {
final txCount = await _fetchTxCount(address: address);
if (txCount > 0) {
activeReceiveIndex = max(activeReceiveIndex, address.derivationIndex);
}
}
for (final address in nextChangeAddresses) {
final txCount = await _fetchTxCount(address: address);
if (txCount > 0) {
activeChangeIndex = max(activeChangeIndex, address.derivationIndex);
}
}
nextReceivingAddresses
.removeWhere((e) => e.derivationIndex > activeReceiveIndex);
if (nextReceivingAddresses.isNotEmpty) {
await mainDB.updateOrPutAddresses(nextReceivingAddresses);
await info.updateReceivingAddress(
newAddress: nextReceivingAddresses.last.value,
isar: mainDB.isar,
);
}
nextChangeAddresses
.removeWhere((e) => e.derivationIndex > activeChangeIndex);
if (nextChangeAddresses.isNotEmpty) {
await mainDB.updateOrPutAddresses(nextChangeAddresses);
}
}
Future<Address> _generateAddressSafe({
required final int chain,
required int startingIndex,
}) async {
final serializedKeys = (await getSerializedKeys())!;
Address? address;
while (address == null) {
try {
address = await _generateAddress(
change: chain,
index: startingIndex,
serializedKeys: serializedKeys,
);
} on FrostdartException catch (e) {
if (e.errorCode == 72) {
// rust doesn't like the addressDerivationData
startingIndex++;
continue;
} else {
rethrow;
}
}
}
return address;
}
/// Can and will often throw unless [index], [change], and [account] are zero.
/// Caller MUST handle exception!
Future<Address> _generateAddress({
int account = 0,
required int change,
required int index,
required String serializedKeys,
}) async {
final addressDerivationData = (
account: account,
change: change == 1,
index: index,
);
final keys = frost.deserializeKeys(keys: serializedKeys);
final addressString = frost.addressForKeys(
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet,
keys: keys,
addressDerivationData: addressDerivationData,
);
return Address(
walletId: info.walletId,
value: addressString,
publicKey: cryptoCurrency.addressToPubkey(address: addressString),
derivationIndex: index,
derivationPath: DerivationPath()..value = "$account/$change/$index",
subType: change == 0
? AddressSubType.receiving
: change == 1
? AddressSubType.change
: AddressSubType.unknown,
type: AddressType.frostMS,
);
}
Future<({List<Address> addresses, int index})> _checkGapsLinearly(
String serializedKeys,
int chain,
) async {
final List<Address> addressArray = [];
int gapCounter = 0;
int index = 0;
for (; gapCounter < 20; index++) {
Logging.instance.log(
"Frost index: $index, \t GapCounter chain=$chain: $gapCounter",
level: LogLevel.Info,
);
Address? address;
while (address == null) {
try {
address = await _generateAddress(
change: chain,
index: index,
serializedKeys: serializedKeys,
);
} on FrostdartException catch (e) {
if (e.errorCode == 72) {
// rust doesn't like the addressDerivationData
index++;
continue;
} else {
rethrow;
}
}
}
// get address tx count
final count = await _fetchTxCount(
address: address!,
);
// check and add appropriate addresses
if (count > 0) {
// add address to array
addressArray.add(address!);
// reset counter
gapCounter = 0;
// add info to derivations
} else {
// increase counter when no tx history found
gapCounter++;
}
}
return (addresses: addressArray, index: index);
}
Future<int> _fetchTxCount({required Address address}) async {
final transactions = await electrumXClient.getHistory(
scripthash: cryptoCurrency.addressToScriptHash(
address: address.value,
),
);
return transactions.length;
}
Future<List<Address>> _fetchAddressesForElectrumXScan() async {
final allAddresses = await mainDB
.getAddresses(walletId)
.filter()
.not()
.group(
(q) => q
.typeEqualTo(AddressType.nonWallet)
.or()
.subTypeEqualTo(AddressSubType.nonWallet),
)
.findAll();
return allAddresses;
}
Future<List<Map<String, dynamic>>> _fetchHistory(
Iterable<String> allAddresses,
) async {
try {
final List<Map<String, dynamic>> allTxHashes = [];
for (int i = 0; i < allAddresses.length; i++) {
final addressString = allAddresses.elementAt(i);
final scriptHash = cryptoCurrency.addressToScriptHash(
address: addressString,
);
final response = await electrumXClient.getHistory(
scripthash: scriptHash,
);
for (int j = 0; j < response.length; j++) {
response[j]["address"] = addressString;
if (!allTxHashes.contains(response[j])) {
allTxHashes.add(response[j]);
}
}
}
return allTxHashes;
} catch (e, s) {
Logging.instance.log(
"$runtimeType._fetchHistory: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
} }

View file

@ -609,7 +609,7 @@ class EpiccashWallet extends Bip39Wallet {
wallet: wallet!, wallet: wallet!,
selectionStrategyIsAll: 0, selectionStrategyIsAll: 0,
minimumConfirmations: cryptoCurrency.minConfirms, minimumConfirmations: cryptoCurrency.minConfirms,
message: txData.noteOnChain!, message: txData.noteOnChain ?? "",
amount: txData.recipients!.first.amount.raw.toInt(), amount: txData.recipients!.first.amount.raw.toInt(),
address: txData.recipients!.first.address, address: txData.recipients!.first.address,
); );

View file

@ -518,6 +518,10 @@ abstract class Wallet<T extends CryptoCurrency> {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
await updateChainHeight(); await updateChainHeight();
if (this is BitcoinFrostWallet) {
await (this as BitcoinFrostWallet).lookAhead();
}
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
// 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.

View file

@ -183,14 +183,75 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
} }
Future<Amount> estimateFeeForSpark(Amount amount) async { Future<Amount> estimateFeeForSpark(Amount amount) async {
// int spendAmount = amount.raw.toInt(); final spendAmount = amount.raw.toInt();
// if (spendAmount == 0) { if (spendAmount == 0) {
return Amount( return Amount(
rawValue: BigInt.from(0), rawValue: BigInt.from(0),
fractionDigits: cryptoCurrency.fractionDigits, fractionDigits: cryptoCurrency.fractionDigits,
); );
// } } else {
// TODO actual fee estimation // fetch spendable spark coins
final coins = await mainDB.isar.sparkCoins
.where()
.walletIdEqualToAnyLTagHash(walletId)
.filter()
.isUsedEqualTo(false)
.and()
.heightIsNotNull()
.and()
.not()
.valueIntStringEqualTo("0")
.findAll();
final available =
coins.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e);
if (amount.raw > available) {
return Amount(
rawValue: BigInt.from(0),
fractionDigits: cryptoCurrency.fractionDigits,
);
}
// prepare coin data for ffi
final serializedCoins = coins
.map(
(e) => (
serializedCoin: e.serializedCoinB64!,
serializedCoinContext: e.contextB64!,
groupId: e.groupId,
height: e.height!,
),
)
.toList();
final root = await getRootHDNode();
final String derivationPath;
if (cryptoCurrency.network.isTestNet) {
derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex";
} else {
derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex";
}
final privateKey = root.derivePath(derivationPath).privateKey.data;
int estimate = await _asyncSparkFeesWrapper(
privateKeyHex: privateKey.toHex,
index: kDefaultSparkIndex,
sendAmount: spendAmount,
subtractFeeFromAmount: true,
serializedCoins: serializedCoins,
// privateRecipientsCount: (txData.sparkRecipients?.length ?? 0),
privateRecipientsCount: 1, // ROUGHLY!
);
if (estimate < 0) {
estimate = 0;
}
return Amount(
rawValue: BigInt.from(estimate),
fractionDigits: cryptoCurrency.fractionDigits,
);
}
} }
/// Spark to Spark/Transparent (spend) creation /// Spark to Spark/Transparent (spend) creation
@ -374,7 +435,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
recipientCount + (txData.sparkRecipients?.length ?? 0); recipientCount + (txData.sparkRecipients?.length ?? 0);
final BigInt estimatedFee; final BigInt estimatedFee;
if (isSendAll) { if (isSendAll) {
final estFee = LibSpark.estimateSparkFee( final estFee = await _asyncSparkFeesWrapper(
privateKeyHex: privateKey.toHex, privateKeyHex: privateKey.toHex,
index: kDefaultSparkIndex, index: kDefaultSparkIndex,
sendAmount: txAmount.raw.toInt(), sendAmount: txAmount.raw.toInt(),
@ -2002,3 +2063,53 @@ class MutableSparkRecipient {
return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }'; return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }';
} }
} }
typedef SerializedCoinData = ({
int groupId,
int height,
String serializedCoin,
String serializedCoinContext
});
Future<int> _asyncSparkFeesWrapper({
required String privateKeyHex,
int index = 1,
required int sendAmount,
required bool subtractFeeFromAmount,
required List<SerializedCoinData> serializedCoins,
required int privateRecipientsCount,
}) async {
return await compute(
_estSparkFeeComputeFunc,
(
privateKeyHex: privateKeyHex,
index: index,
sendAmount: sendAmount,
subtractFeeFromAmount: subtractFeeFromAmount,
serializedCoins: serializedCoins,
privateRecipientsCount: privateRecipientsCount,
),
);
}
int _estSparkFeeComputeFunc(
({
String privateKeyHex,
int index,
int sendAmount,
bool subtractFeeFromAmount,
List<SerializedCoinData> serializedCoins,
int privateRecipientsCount,
}) args,
) {
final est = LibSpark.estimateSparkFee(
privateKeyHex: args.privateKeyHex,
index: args.index,
sendAmount: args.sendAmount,
subtractFeeFromAmount: args.subtractFeeFromAmount,
serializedCoins: args.serializedCoins,
privateRecipientsCount: args.privateRecipientsCount,
);
return est;
}

View file

@ -181,8 +181,7 @@ class _TransactionCardState extends ConsumerState<TransactionCard> {
unawaited( unawaited(
showFloatingFlushBar( showFloatingFlushBar(
context: context, context: context,
message: message: "Restored Epic funds from your Seed have no Data.",
"Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.",
type: FlushBarType.warning, type: FlushBarType.warning,
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
), ),

View file

@ -287,21 +287,23 @@ packages:
source: hosted source: hosted
version: "4.6.0" version: "4.6.0"
coinlib: coinlib:
dependency: transitive dependency: "direct overridden"
description: description:
name: coinlib path: coinlib
sha256: "44aa3f7b07d3b03d58353e7657f43cdaf76a70ad2cce5bdac9306208099d8df5" ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f
url: "https://pub.dev" resolved-ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f
source: hosted url: "https://github.com/peercoin/coinlib.git"
version: "2.0.0" source: git
version: "2.1.0-rc.1"
coinlib_flutter: coinlib_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: coinlib_flutter path: coinlib_flutter
sha256: b352378773158dbaec37bd542c297682f3812f9881acb676971f0f4c5893631f ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f
url: "https://pub.dev" resolved-ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f
source: hosted url: "https://github.com/peercoin/coinlib.git"
version: "2.0.0" source: git
version: "2.1.0-rc.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -1062,26 +1064,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.0" version: "10.0.4"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.3"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "3.0.1"
lelantus: lelantus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1149,10 +1151,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: meta name: meta
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" version: "1.12.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -1588,8 +1590,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "packages/solana" path: "packages/solana"
ref: a83e375678eb22fe544dc125d29bbec0fb833882 ref: "706be5f166d31736c443cca4cd311b7fcfc2a9bf"
resolved-ref: a83e375678eb22fe544dc125d29bbec0fb833882 resolved-ref: "706be5f166d31736c443cca4cd311b7fcfc2a9bf"
url: "https://github.com/cypherstack/espresso-cash-public.git" url: "https://github.com/cypherstack/espresso-cash-public.git"
source: git source: git
version: "0.30.4" version: "0.30.4"
@ -1734,26 +1736,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.24.9" version: "1.25.2"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.1" version: "0.7.0"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.9" version: "0.6.0"
tezart: tezart:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1952,10 +1954,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "13.0.0" version: "14.2.1"
wakelock: wakelock:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -43,6 +43,8 @@ part of 'app_config.dart';
const _prefix = "Campfire"; const _prefix = "Campfire";
const _separator = ""; const _separator = "";
const _suffix = ""; const _suffix = "";
const _emptyWalletsMessage =
"Join us around the Campfire and create a wallet!";
const _appDataDirName = "campfire"; const _appDataDirName = "campfire";
const _shortDescriptionText = "Your privacy. Your wallet. Your Firo."; const _shortDescriptionText = "Your privacy. Your wallet. Your Firo.";
const _commitHash = "$BUILT_COMMIT_HASH"; const _commitHash = "$BUILT_COMMIT_HASH";

View file

@ -37,6 +37,8 @@ part of 'app_config.dart';
const _prefix = "Stack"; const _prefix = "Stack";
const _separator = " "; const _separator = " ";
const _suffix = "Duo"; const _suffix = "Duo";
const _emptyWalletsMessage =
"You do not have any wallets yet. Start building your crypto Stack!";
const _appDataDirName = "stackduo"; const _appDataDirName = "stackduo";
const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone";
const _commitHash = "$BUILT_COMMIT_HASH"; const _commitHash = "$BUILT_COMMIT_HASH";

View file

@ -37,6 +37,8 @@ part of 'app_config.dart';
const _prefix = "Stack"; const _prefix = "Stack";
const _separator = " "; const _separator = " ";
const _suffix = "Wallet"; const _suffix = "Wallet";
const _emptyWalletsMessage =
"You do not have any wallets yet. Start building your crypto Stack!";
const _appDataDirName = "stackwallet"; const _appDataDirName = "stackwallet";
const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone";
const _commitHash = "$BUILT_COMMIT_HASH"; const _commitHash = "$BUILT_COMMIT_HASH";

View file

@ -15,3 +15,22 @@ done
# Configure ios for Duo. # Configure ios for Duo.
sed -i '' "s/${APP_NAME_PLACEHOLDER}/${NEW_NAME}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_0}" sed -i '' "s/${APP_NAME_PLACEHOLDER}/${NEW_NAME}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_0}"
sed -i '' "s/${APP_ID_PLACEHOLDER}/${NEW_APP_ID}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_1}" sed -i '' "s/${APP_ID_PLACEHOLDER}/${NEW_APP_ID}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_1}"
# use app specific launch images
LAUNCH_IMAGES_DIR="${APP_PROJECT_ROOT_DIR}/ios/Runner/Assets.xcassets/LaunchImage.imageset"
for file in "${LAUNCH_IMAGES_DIR}"/*.png;
do
# Check if the file exists to avoid errors if no PNG files are found
if [ -f "${file}" ]; then
rm "${file}"
fi
done
LAUNCH_IMAGES_TEMPLATES_DIR="${APP_PROJECT_ROOT_DIR}/asset_sources/other/ios_launch_image/${NEW_BASIC_NAME}"
for file in "${LAUNCH_IMAGES_TEMPLATES_DIR}"/*.png;
do
# Check if the file exists to avoid errors if no PNG files are found
if [ -f "${file}" ]; then
cp "${file}" "${LAUNCH_IMAGES_DIR}/"
fi
done

View file

@ -147,6 +147,15 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient {
returnValueForMissingStub: _i7.Future<void>.value(), returnValueForMissingStub: _i7.Future<void>.value(),
) as _i7.Future<void>); ) as _i7.Future<void>);
@override @override
_i7.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i7.Future<void>.value(),
returnValueForMissingStub: _i7.Future<void>.value(),
) as _i7.Future<void>);
@override
_i7.Future<dynamic> request({ _i7.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -465,7 +474,14 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient {
returnValue: _i7.Future<Set<String>>.value(<String>{}), returnValue: _i7.Future<Set<String>>.value(<String>{}),
) as _i7.Future<Set<String>>); ) as _i7.Future<Set<String>>);
@override @override
_i7.Future<Map<String, dynamic>> getMempoolSparkData({ _i7.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -478,9 +494,27 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i7.Future<
_i7.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i7.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i7.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i7.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i7.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -498,6 +532,24 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient {
returnValue: _i7.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i7.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i7.Future<List<List<dynamic>>>); ) as _i7.Future<List<List<dynamic>>>);
@override @override
_i7.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i7.Future<bool>.value(false),
) as _i7.Future<bool>);
@override
_i7.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i7.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(
@ -941,6 +993,19 @@ class MockPrefs extends _i1.Mock implements _i8.Prefs {
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
bool get autoPin => (super.noSuchMethod(
Invocation.getter(#autoPin),
returnValue: false,
) as bool);
@override
set autoPin(bool? autoPin) => super.noSuchMethod(
Invocation.setter(
#autoPin,
autoPin,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod( bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners), Invocation.getter(#hasListeners),
returnValue: false, returnValue: false,

View file

@ -1000,6 +1000,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
bool get autoPin => (super.noSuchMethod(
Invocation.getter(#autoPin),
returnValue: false,
) as bool);
@override
set autoPin(bool? autoPin) => super.noSuchMethod(
Invocation.setter(
#autoPin,
autoPin,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod( bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners), Invocation.getter(#hasListeners),
returnValue: false, returnValue: false,

View file

@ -473,6 +473,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs {
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
bool get autoPin => (super.noSuchMethod(
Invocation.getter(#autoPin),
returnValue: false,
) as bool);
@override
set autoPin(bool? autoPin) => super.noSuchMethod(
Invocation.setter(
#autoPin,
autoPin,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod( bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners), Invocation.getter(#hasListeners),
returnValue: false, returnValue: false,

View file

@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValueForMissingStub: _i6.Future<void>.value(), returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>); ) as _i6.Future<void>);
@override @override
_i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<dynamic> request({ _i6.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<Set<String>>.value(<String>{}), returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>); ) as _i6.Future<Set<String>>);
@override @override
_i6.Future<Map<String, dynamic>> getMempoolSparkData({ _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i6.Future<
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i6.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i6.Future<List<List<dynamic>>>); ) as _i6.Future<List<List<dynamic>>>);
@override @override
_i6.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i6.Future<bool>.value(false),
) as _i6.Future<bool>);
@override
_i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValueForMissingStub: _i6.Future<void>.value(), returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>); ) as _i6.Future<void>);
@override @override
_i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<dynamic> request({ _i6.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<Set<String>>.value(<String>{}), returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>); ) as _i6.Future<Set<String>>);
@override @override
_i6.Future<Map<String, dynamic>> getMempoolSparkData({ _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i6.Future<
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i6.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i6.Future<List<List<dynamic>>>); ) as _i6.Future<List<List<dynamic>>>);
@override @override
_i6.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i6.Future<bool>.value(false),
) as _i6.Future<bool>);
@override
_i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValueForMissingStub: _i6.Future<void>.value(), returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>); ) as _i6.Future<void>);
@override @override
_i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<dynamic> request({ _i6.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<Set<String>>.value(<String>{}), returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>); ) as _i6.Future<Set<String>>);
@override @override
_i6.Future<Map<String, dynamic>> getMempoolSparkData({ _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i6.Future<
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i6.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i6.Future<List<List<dynamic>>>); ) as _i6.Future<List<List<dynamic>>>);
@override @override
_i6.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i6.Future<bool>.value(false),
) as _i6.Future<bool>);
@override
_i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValueForMissingStub: _i6.Future<void>.value(), returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>); ) as _i6.Future<void>);
@override @override
_i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<dynamic> request({ _i6.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<Set<String>>.value(<String>{}), returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>); ) as _i6.Future<Set<String>>);
@override @override
_i6.Future<Map<String, dynamic>> getMempoolSparkData({ _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i6.Future<
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i6.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i6.Future<List<List<dynamic>>>); ) as _i6.Future<List<List<dynamic>>>);
@override @override
_i6.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i6.Future<bool>.value(false),
) as _i6.Future<bool>);
@override
_i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValueForMissingStub: _i6.Future<void>.value(), returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>); ) as _i6.Future<void>);
@override @override
_i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod(
Invocation.method(
#checkElectrumAdapter,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<dynamic> request({ _i6.Future<dynamic> request({
required String? command, required String? command,
List<dynamic>? args = const [], List<dynamic>? args = const [],
@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<Set<String>>.value(<String>{}), returnValue: _i6.Future<Set<String>>.value(<String>{}),
) as _i6.Future<Set<String>>); ) as _i6.Future<Set<String>>);
@override @override
_i6.Future<Map<String, dynamic>> getMempoolSparkData({ _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>> getMempoolSparkData({
String? requestID, String? requestID,
required List<String>? txids, required List<String>? txids,
}) => }) =>
@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
#txids: txids, #txids: txids,
}, },
), ),
returnValue: returnValue: _i6.Future<
_i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), List<
) as _i6.Future<Map<String, dynamic>>); ({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>.value(<({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>[]),
) as _i6.Future<
List<
({
List<String> coins,
List<String> lTags,
List<String> serialContext,
String txid
})>>);
@override @override
_i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({
String? requestID, String? requestID,
@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient {
returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]),
) as _i6.Future<List<List<dynamic>>>); ) as _i6.Future<List<List<dynamic>>>);
@override @override
_i6.Future<bool> isMasterNodeCollateral({
String? requestID,
required String? txid,
required int? index,
}) =>
(super.noSuchMethod(
Invocation.method(
#isMasterNodeCollateral,
[],
{
#requestID: requestID,
#txid: txid,
#index: index,
},
),
returnValue: _i6.Future<bool>.value(false),
) as _i6.Future<bool>);
@override
_i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) =>
(super.noSuchMethod( (super.noSuchMethod(
Invocation.method( Invocation.method(

View file

@ -729,6 +729,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
bool get autoPin => (super.noSuchMethod(
Invocation.getter(#autoPin),
returnValue: false,
) as bool);
@override
set autoPin(bool? autoPin) => super.noSuchMethod(
Invocation.setter(
#autoPin,
autoPin,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod( bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners), Invocation.getter(#hasListeners),
returnValue: false, returnValue: false,

View file

@ -615,6 +615,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null, returnValueForMissingStub: null,
); );
@override @override
bool get autoPin => (super.noSuchMethod(
Invocation.getter(#autoPin),
returnValue: false,
) as bool);
@override
set autoPin(bool? autoPin) => super.noSuchMethod(
Invocation.setter(
#autoPin,
autoPin,
),
returnValueForMissingStub: null,
);
@override
bool get hasListeners => (super.noSuchMethod( bool get hasListeners => (super.noSuchMethod(
Invocation.getter(#hasListeners), Invocation.getter(#hasListeners),
returnValue: false, returnValue: false,

Some files were not shown because too many files have changed in this diff Show more