Merge branch 'staging' into uri

This commit is contained in:
sneurlax 2024-08-27 13:09:50 -05:00
commit dd11214b3c
72 changed files with 3943 additions and 743 deletions
asset_sources/default_themes/campfire
crypto_plugins
docs
lib
main.dart
models/isar/models
pages
address_book_views/subviews
exchange_view
home_view
receive_view/addresses
send_view
settings_views
global_settings_view
wallet_settings_view/wallet_settings_wallet_settings
stack_privacy_calls.dart
token_view/sub_widgets
wallet_view
wallets_view
pages_desktop_specific
desktop_exchange/exchange_steps
desktop_menu.dartdesktop_menu_item.dart
my_stack_view/wallet_view/sub_widgets
settings/settings_menu/advanced_settings
services
themes
utilities
wallets
widgets/custom_buttons
linux/flutter
macos
pubspec.lock
scripts/app_config/templates
test
windows/flutter

@ -1 +1 @@
Subproject commit b654bf4488357c8a104900e11f9468d54a39f22b
Subproject commit f8746dbef5c5ad5ed2dad12f615723d087083e9c

@ -1 +1 @@
Subproject commit 982f5ab19fe0dd3dd3f6be2c46f8dff13d49027c
Subproject commit db7585d8cd493b143e0a0652c618904d1f636d1d

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

View file

@ -53,7 +53,7 @@ sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2-
### Build dependencies
Install basic dependencies
```
sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils g++ gcc gperf
sudo apt-get install libssl-dev curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake libgit2-dev clang libncurses5-dev libncursesw5-dev zlib1g-dev llvm python3-distutils g++ gcc gperf libopencv-dev
```
Install [Rust](https://www.rust-lang.org/tools/install) with command:

View file

@ -242,7 +242,7 @@ void main(List<String> args) async {
// SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
// overlays: [SystemUiOverlay.bottom]);
await NotificationApi.init();
unawaited(NotificationApi.init());
await loadCoinlibFuture;
@ -378,7 +378,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
// TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet
// unawaited(_nodeService.updateCommunityNodes());
if (AppConfig.hasFeature(AppFeature.swap)) {
if (AppConfig.hasFeature(AppFeature.swap) &&
ref.read(prefsChangeNotifierProvider).enableExchange) {
await ExchangeDataLoadingService.instance.initDB();
// run without awaiting
if (ref.read(prefsChangeNotifierProvider).externalCalls &&

View file

@ -87,7 +87,10 @@ class TransactionV2 {
);
}
@ignore
int? get size => _getFromOtherData(key: TxV2OdKeys.size) as int?;
@ignore
int? get vSize => _getFromOtherData(key: TxV2OdKeys.vSize) as int?;
bool get isEpiccashTransaction =>

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

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

View file

@ -28,6 +28,7 @@ import '../../../utilities/assets.dart';
import '../../../utilities/clipboard_interface.dart';
import '../../../utilities/constants.dart';
import '../../../utilities/enums/fee_rate_type_enum.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/text_styles.dart';
import '../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
@ -315,7 +316,8 @@ class _Step4ViewState extends ConsumerState<Step4View> {
);
}
}
} catch (e) {
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
if (mounted && !wasCancelled) {
// pop building dialog
Navigator.of(context).pop();

View file

@ -28,12 +28,14 @@ import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/enums/fee_rate_type_enum.dart';
import '../../utilities/logger.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/crypto_currency/crypto_currency.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/models/tx_data.dart';
import '../../wallets/wallet/impl/firo_wallet.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -271,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(
const Duration(
milliseconds: 2500,
@ -375,7 +386,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
);
}
}
} catch (e) {
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
if (mounted) {
// pop building dialog
Navigator.of(context).pop();

View file

@ -190,6 +190,15 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
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(
condition: !isDesktop,
builder: (child) => Background(
@ -248,21 +257,11 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
children: children,
),
),
if (!hasTx &&
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
if (showSendFromStackButton)
const SizedBox(
height: 32,
),
if (!hasTx &&
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
if (showSendFromStackButton)
SecondaryButton(
label: "Send from ${AppConfig.prefix}",
buttonHeight: ButtonHeight.l,
@ -1371,13 +1370,7 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
const SizedBox(
height: 12,
),
if (!isDesktop &&
!hasTx &&
AppConfig.isStackCoin(trade.payInCurrency) &&
(trade.status == "New" ||
trade.status == "new" ||
trade.status == "waiting" ||
trade.status == "Waiting"))
if (!isDesktop && showSendFromStackButton)
SecondaryButton(
label: "Send from ${AppConfig.prefix}",
onPressed: () {

View file

@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart';
import '../../app_config.dart';
import '../../providers/global/notifications_provider.dart';
import '../../providers/global/prefs_provider.dart';
import '../../providers/ui/home_view_index_provider.dart';
import '../../providers/ui/unread_notifications_provider.dart';
import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart';
@ -172,6 +173,20 @@ class _HomeViewState extends ConsumerState<HomeView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
// dirty hack
ref.listen(
prefsChangeNotifierProvider.select((value) => value.enableExchange),
(prev, next) {
if (next == false &&
mounted &&
ref.read(homeViewPageIndexStateProvider) != 0) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => ref.read(homeViewPageIndexStateProvider.state).state = 0,
);
}
});
return WillPopScope(
onWillPop: _onWillPop,
child: Background(
@ -345,7 +360,8 @@ class _HomeViewState extends ConsumerState<HomeView> {
),
body: Column(
children: [
if (_children.length > 1)
if (_children.length > 1 &&
ref.watch(prefsChangeNotifierProvider).enableExchange)
Container(
decoration: BoxDecoration(
color: Theme.of(context)

View file

@ -44,8 +44,6 @@ class _HomeViewButtonBarState extends ConsumerState<HomeViewButtonBar> {
@override
Widget build(BuildContext context) {
//todo: check if print needed
// debugPrint("BUILD: HomeViewButtonBar");
final selectedIndex = ref.watch(homeViewPageIndexStateProvider.state).state;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,

View file

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

View file

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

View file

@ -829,7 +829,8 @@ class _SendViewState extends ConsumerState<SendView> {
),
);
}
} catch (e) {
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
if (mounted) {
// pop building dialog
Navigator.of(context).pop();
@ -942,6 +943,9 @@ class _SendViewState extends ConsumerState<SendView> {
if (isPaynymSend) {
sendToController.text = widget.accountLite!.nymName;
noteController.text = "PayNym send";
WidgetsBinding.instance.addPostFrameCallback(
(_) => _setValidAddressProviders(sendToController.text),
);
}
// if (coin is! Epiccash) {

View file

@ -15,12 +15,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../models/isar/models/isar_models.dart';
import '../../models/send_view_auto_fill_data.dart';
import '../address_book_views/address_book_view.dart';
import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/transaction_fee_selection_sheet.dart';
import '../../providers/providers.dart';
import '../../providers/ui/fee_rate_type_state_provider.dart';
import '../../providers/ui/preview_tx_button_state_provider.dart';
@ -55,6 +52,10 @@ import '../../widgets/icon_widgets/x_icon.dart';
import '../../widgets/stack_dialog.dart';
import '../../widgets/stack_text_field.dart';
import '../../widgets/textfield_icon_button.dart';
import '../address_book_views/address_book_view.dart';
import 'confirm_transaction_view.dart';
import 'sub_widgets/building_transaction_dialog.dart';
import 'sub_widgets/transaction_fee_selection_sheet.dart';
class TokenSendView extends ConsumerStatefulWidget {
const TokenSendView({
@ -529,7 +530,8 @@ class _TokenSendViewState extends ConsumerState<TokenSendView> {
),
);
}
} catch (e) {
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Error);
if (mounted) {
// pop building dialog
Navigator.of(context).pop();

View file

@ -183,6 +183,55 @@ class AdvancedSettingsView extends StatelessWidget {
},
),
),
// showExchange pref.
const SizedBox(
height: 8,
),
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(
"Enable exchange features",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableExchange,
),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.enableExchange = newValue;
},
),
),
],
),
),
);
},
),
),
const SizedBox(
height: 8,
),

View file

@ -52,6 +52,8 @@ import '../../../../../wallets/isar/models/frost_wallet_info.dart';
import '../../../../../wallets/isar/models/wallet_info.dart';
import '../../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../../../wallets/wallet/impl/monero_wallet.dart';
import '../../../../../wallets/wallet/impl/wownero_wallet.dart';
import '../../../../../wallets/wallet/wallet.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import '../../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
@ -486,7 +488,13 @@ abstract class SWB {
privateKey: privateKey,
);
await wallet.init();
if (wallet is MoneroWallet /*|| wallet is WowneroWallet doesn't work.*/) {
await wallet.init(isRestore: true);
} else if (wallet is WowneroWallet) {
await wallet.init(isRestore: true);
} else {
await wallet.init();
}
int restoreHeight = walletbackup['restoreHeight'] as int? ?? 0;
if (restoreHeight <= 0) {

View file

@ -82,17 +82,20 @@ class SWBFileSystem {
}
Future<void> pickDir(BuildContext context) async {
final String? path;
final String? chosenPath;
if (Platform.isIOS) {
path = startPath?.path;
chosenPath = startPath?.path;
} 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",
initialDirectory: startPath!.path,
initialDirectory: path,
lockParentWindow: true,
);
}
dirPath = path;
dirPath = chosenPath;
}
Future<void> openFile(BuildContext context) async {

View file

@ -17,7 +17,6 @@ import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/constants.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/wallet/wallet_mixin_interfaces/lelantus_interface.dart';
@ -53,78 +52,83 @@ class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget {
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.
late final DSBController _switchController;
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,
);
bool _switchReuseAddressToggledLock = false; // Mutex.
Future<void> _switchReuseAddressToggled() async {
if (_switchReuseAddressToggledLock) {
return;
}
_switchReuseAddressToggledLock = true; // Lock mutex.
try {
if (_switchController.isOn?.call() != true) {
final canContinue = await showDialog<bool?>(
context: context,
builder: (context) {
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);
},
),
);
},
);
if (canContinue == true) {
await _updateAddressReuse(true);
}
} else {
await _updateAddressReuse(false);
}
} finally {
// ensure _switchReuseAddressToggledLock is set to false no matter what.
_switchReuseAddressToggledLock = false;
}
}
Future<void> _updateAddressReuse(bool shouldReuse) async {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: shouldReuse,
},
isar: ref.read(mainDBProvider).isar,
);
if (_switchController.isOn != null) {
if (_switchController.isOn!.call() != shouldReuse) {
_switchController.activate?.call();
}
}
}
@override
void initState() {
_switchController = DSBController();
super.initState();
}
@override
@ -185,9 +189,11 @@ class _WalletSettingsWalletSettingsViewState
),
),
),
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
RoundedWhiteContainer(
@ -222,62 +228,56 @@ class _WalletSettingsWalletSettingsViewState
),
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
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,
padding: const EdgeInsets.all(0),
child: RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: _switchReuseAddressToggled,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 20,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Reuse receiving address",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
),
onPressed: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Reuse receiving address by default",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
SizedBox(
height: 20,
width: 40,
child: IgnorePointer(
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId).select(
(value) => value.otherData,
),
)[WalletInfoKeys.reuseAddress] as bool? ??
false,
controller: _switchController,
),
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 MultiAddressInterface)
const SizedBox(
height: 8,
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is LelantusInterface)
const SizedBox(
@ -354,11 +354,9 @@ class _WalletSettingsWalletSettingsViewState
),
),
),
if (ref.watch(pWallets).getWallet(widget.walletId)
is RbfInterface)
const SizedBox(
height: 8,
),
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: RawMaterialButton(

View file

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

View file

@ -218,6 +218,9 @@ class TokenWalletOptions extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(prefsChangeNotifierProvider);
final showExchange = prefs.enableExchange;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -251,11 +254,11 @@ class TokenWalletOptions extends ConsumerWidget {
subLabel: "Send",
iconAssetPathSVG: Assets.svg.arrowUpRight,
),
if (AppConfig.hasFeature(AppFeature.swap))
if (AppConfig.hasFeature(AppFeature.swap) && showExchange)
const SizedBox(
width: 16,
),
if (AppConfig.hasFeature(AppFeature.swap))
if (AppConfig.hasFeature(AppFeature.swap) && showExchange)
TokenOptionsButton(
onPressed: () => _onExchangePressed(context),
subLabel: "Swap",
@ -265,11 +268,11 @@ class TokenWalletOptions extends ConsumerWidget {
),
),
),
if (AppConfig.hasFeature(AppFeature.buy))
if (AppConfig.hasFeature(AppFeature.buy) && showExchange)
const SizedBox(
width: 16,
),
if (AppConfig.hasFeature(AppFeature.buy))
if (AppConfig.hasFeature(AppFeature.buy) && showExchange)
TokenOptionsButton(
onPressed: () => _onBuyPressed(context),
subLabel: "Buy",

View file

@ -81,14 +81,16 @@ class WalletSummaryInfo extends ConsumerWidget {
priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin)),
);
final _showAvailable =
ref.watch(walletBalanceToggleStateProvider.state).state ==
WalletBalanceToggleState.available;
final _showAvailable = ref.watch(walletBalanceToggleStateProvider) ==
WalletBalanceToggleState.available;
final Amount balanceToShow;
final String title;
final bool toggleBalance;
if (coin is Firo) {
toggleBalance = false;
final type = ref.watch(publicPrivateBalanceStateProvider.state).state;
title =
"${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance";
@ -109,6 +111,7 @@ class WalletSummaryInfo extends ConsumerWidget {
break;
}
} else {
toggleBalance = true;
balanceToShow = _showAvailable ? balance.spendable : balance.total;
title = _showAvailable ? "Available balance" : "Full balance";
}
@ -141,7 +144,20 @@ class WalletSummaryInfo extends ConsumerWidget {
children: [
GestureDetector(
onTap: () {
showSheet(context);
if (toggleBalance) {
if (ref.read(walletBalanceToggleStateProvider) ==
WalletBalanceToggleState.available) {
ref
.read(walletBalanceToggleStateProvider.notifier)
.state = WalletBalanceToggleState.full;
} else {
ref
.read(walletBalanceToggleStateProvider.notifier)
.state = WalletBalanceToggleState.available;
}
} else {
showSheet(context);
}
},
child: Row(
children: [
@ -153,17 +169,19 @@ class WalletSummaryInfo extends ConsumerWidget {
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
if (!toggleBalance) ...[
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
],
],
),
),

View file

@ -518,6 +518,9 @@ class _WalletViewState extends ConsumerState<WalletView> {
final coin = ref.watch(pWalletCoin(walletId));
final prefs = ref.watch(prefsChangeNotifierProvider);
final showExchange = prefs.enableExchange;
return ConditionalParent(
condition: _rescanningOnOpen,
builder: (child) {
@ -1053,7 +1056,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
if (Constants.enableExchange &&
ref.watch(pWalletCoin(walletId)) is! FrostCurrency &&
AppConfig.hasFeature(AppFeature.swap))
AppConfig.hasFeature(AppFeature.swap) &&
showExchange)
WalletNavigationBarItemData(
label: "Swap",
icon: const ExchangeNavIcon(),
@ -1061,7 +1065,8 @@ class _WalletViewState extends ConsumerState<WalletView> {
),
if (Constants.enableExchange &&
ref.watch(pWalletCoin(walletId)) is! FrostCurrency &&
AppConfig.hasFeature(AppFeature.buy))
AppConfig.hasFeature(AppFeature.buy) &&
showExchange)
WalletNavigationBarItemData(
label: "Buy",
icon: const BuyNavIcon(),

View file

@ -72,6 +72,16 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> {
final Map<String, WalletListItemData> wallets = {};
List<WalletListItemData> _filter(String searchTerm) {
// clean out deleted wallets
final existingWalletIds = ref
.read(mainDBProvider)
.isar
.walletInfo
.where()
.walletIdProperty()
.findAllSync();
wallets.removeWhere((k, v) => !existingWalletIds.contains(k));
if (searchTerm.isEmpty) {
return wallets.values.toList()
..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name));

View file

@ -206,7 +206,8 @@ class _StepScaffoldState extends ConsumerState<StepScaffold> {
void sendFromStack() {
final trade = ref.read(desktopExchangeModelProvider)!.trade!;
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(
fractionDigits: coin.fractionDigits,
);

View file

@ -11,16 +11,15 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../app_config.dart';
import '../providers/desktop/current_desktop_menu_item.dart';
import '../providers/providers.dart';
import '../themes/stack_colors.dart';
import '../utilities/assets.dart';
import '../utilities/text_styles.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../widgets/desktop/desktop_tor_status_button.dart';
import '../widgets/desktop/living_stack_icon.dart';
import 'desktop_menu_item.dart';
@ -60,6 +59,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
late final DMIController torButtonController;
double _width = expandedWidth;
bool get _isMinimized => _width < expandedWidth;
void updateSelectedMenuItem(DesktopMenuItemId idKey) {
widget.onSelectionWillChange?.call(idKey);
@ -114,6 +114,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
@override
Widget build(BuildContext context) {
final prefs = ref.watch(prefsChangeNotifierProvider);
final showExchange = prefs.enableExchange;
return Material(
color: Theme.of(context).extension<StackColors>()!.popupBG,
child: AnimatedContainer(
@ -163,7 +167,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
onPressed: () {
ref.read(currentDesktopMenuItemProvider.state).state =
DesktopMenuItemId.settings;
ref.watch(selectedSettingsMenuItemStateProvider.state).state =
ref.read(selectedSettingsMenuItemStateProvider.state).state =
4;
},
),
@ -181,114 +185,134 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DesktopMenuItem(
key: const ValueKey('myStack'),
duration: duration,
icon: const DesktopMyStackIcon(),
label: "My ${AppConfig.prefix}",
value: DesktopMenuItemId.myStack,
onChanged: updateSelectedMenuItem,
controller: controllers[0],
isExpandedInitially: !_isMinimized,
),
if (AppConfig.hasFeature(AppFeature.swap))
if (AppConfig.hasFeature(AppFeature.swap) &&
showExchange) ...[
const SizedBox(
height: 2,
),
if (AppConfig.hasFeature(AppFeature.swap))
DesktopMenuItem(
key: const ValueKey('swap'),
duration: duration,
icon: const DesktopExchangeIcon(),
label: "Swap",
value: DesktopMenuItemId.exchange,
onChanged: updateSelectedMenuItem,
controller: controllers[1],
isExpandedInitially: !_isMinimized,
),
if (AppConfig.hasFeature(AppFeature.buy))
],
if (AppConfig.hasFeature(AppFeature.buy) &&
showExchange) ...[
const SizedBox(
height: 2,
),
if (AppConfig.hasFeature(AppFeature.buy))
DesktopMenuItem(
key: const ValueKey('buy'),
duration: duration,
icon: const DesktopBuyIcon(),
label: "Buy crypto",
value: DesktopMenuItemId.buy,
onChanged: updateSelectedMenuItem,
controller: controllers[2],
isExpandedInitially: !_isMinimized,
),
],
const SizedBox(
height: 2,
),
DesktopMenuItem(
key: const ValueKey('notifications'),
duration: duration,
icon: const DesktopNotificationsIcon(),
label: "Notifications",
value: DesktopMenuItemId.notifications,
onChanged: updateSelectedMenuItem,
controller: controllers[3],
isExpandedInitially: !_isMinimized,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
key: const ValueKey('addressBook'),
duration: duration,
icon: const DesktopAddressBookIcon(),
label: "Address Book",
value: DesktopMenuItemId.addressBook,
onChanged: updateSelectedMenuItem,
controller: controllers[4],
isExpandedInitially: !_isMinimized,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
key: const ValueKey('settings'),
duration: duration,
icon: const DesktopSettingsIcon(),
label: "Settings",
value: DesktopMenuItemId.settings,
onChanged: updateSelectedMenuItem,
controller: controllers[5],
isExpandedInitially: !_isMinimized,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
key: const ValueKey('support'),
duration: duration,
icon: const DesktopSupportIcon(),
label: "Support",
value: DesktopMenuItemId.support,
onChanged: updateSelectedMenuItem,
controller: controllers[6],
isExpandedInitially: !_isMinimized,
),
const SizedBox(
height: 2,
),
DesktopMenuItem(
key: const ValueKey('about'),
duration: duration,
icon: const DesktopAboutIcon(),
label: "About",
value: DesktopMenuItemId.about,
onChanged: updateSelectedMenuItem,
controller: controllers[7],
isExpandedInitially: !_isMinimized,
),
const Spacer(),
if (!Platform.isIOS)
DesktopMenuItem(
key: const ValueKey('exit'),
duration: duration,
labelLength: 123,
icon: const DesktopExitIcon(),
label: "Exit",
value: 7,
onChanged: (_) {
// todo: save stuff/ notify before exit?
if (AppConfig.coins
.where((e) => e is Monero || e is Wownero)
.isNotEmpty) {
// hack to insta kill because xmr/wow native lib code sucks
exit(0);
} else {
SystemNavigator.pop();
}
// // todo: save stuff/ notify before exit?
// if (AppConfig.coins
// .where((e) => e is Monero || e is Wownero)
// .isNotEmpty) {
// // hack to insta kill because xmr/wow native lib code sucks
exit(0);
// } else {
// SystemNavigator.pop();
// }
},
controller: controllers[8],
isExpandedInitially: !_isMinimized,
),
],
),

View file

@ -24,6 +24,9 @@ import 'desktop_menu.dart';
class DMIController {
VoidCallback? toggle;
DMIController();
void dispose() {
toggle = null;
}
@ -237,6 +240,7 @@ class DesktopMenuItem<T> extends ConsumerStatefulWidget {
required this.duration,
this.labelLength = 125,
this.controller,
required this.isExpandedInitially,
});
final Widget icon;
@ -246,6 +250,7 @@ class DesktopMenuItem<T> extends ConsumerStatefulWidget {
final Duration duration;
final double labelLength;
final DMIController? controller;
final bool isExpandedInitially;
@override
ConsumerState<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>();
@ -287,11 +292,17 @@ class _DesktopMenuItemState<T> extends ConsumerState<DesktopMenuItem<T>>
labelLength = widget.labelLength;
controller = widget.controller;
_iconOnly = !widget.isExpandedInitially;
controller?.toggle = toggle;
animationController = AnimationController(
vsync: this,
duration: duration,
)..forward();
);
if (_iconOnly) {
animationController.value = 0;
} else {
animationController.value = 1;
}
super.initState();
}

View file

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

View file

@ -9,14 +9,26 @@
*/
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:camera_linux/camera_linux.dart';
import 'package:camera_macos/camera_macos_arguments.dart';
import 'package:camera_macos/camera_macos_controller.dart';
import 'package:camera_macos/camera_macos_device.dart';
import 'package:camera_macos/camera_macos_platform_interface.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:camera_windows/camera_windows.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:decimal/decimal.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image/image.dart' as img;
import 'package:zxing2/qrcode.dart';
import '../../../../models/isar/models/contact_entry.dart';
import '../../../../models/paynym/paynym_account_lite.dart';
@ -64,8 +76,10 @@ import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart';
import '../../../../widgets/fee_slider.dart';
import '../../../../widgets/icon_widgets/addressbook_icon.dart';
import '../../../../widgets/icon_widgets/clipboard_icon.dart';
import '../../../../widgets/icon_widgets/qrcode_icon.dart';
import '../../../../widgets/icon_widgets/x_icon.dart';
import '../../../../widgets/rounded_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../../widgets/stack_text_field.dart';
import '../../../../widgets/textfield_icon_button.dart';
import '../../../coin_control/desktop_coin_control_use_dialog.dart';
@ -140,6 +154,30 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
"Calculating...",
];
Future<void> scanWebcam() async {
try {
await showDialog(
context: context,
builder: (context) {
return QrCodeScannerDialog(
walletId: widget.walletId,
onQrCodeDetected: (qrCodeData) {
try {
_processQrCodeData(qrCodeData);
} catch (e, s) {
Logging.instance.log("Error processing QR code data: $e\n$s",
level: LogLevel.Error);
}
},
);
},
);
} catch (e, s) {
Logging.instance.log("Error opening QR code scanner dialog: $e\n$s",
level: LogLevel.Error);
}
}
Future<void> previewSend() async {
final wallet = ref.read(pWallets).getWallet(walletId);
@ -481,7 +519,7 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
);
}
} catch (e, s) {
Logging.instance.log("Desktop send: $e\n$s", level: LogLevel.Warning);
Logging.instance.log("Desktop send: $e\n$s", level: LogLevel.Error);
if (mounted) {
// pop building dialog
Navigator.of(
@ -695,6 +733,40 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
"Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
} catch (e, s) {
Logging.instance.log(
"Failed to scan qr code in SendView: $e\n$s",
level: LogLevel.Warning,
);
}
}
void _processQrCodeData(String qrCodeData) {
try {
var results = AddressUtils.parseUri(qrCodeData);
if (results.isNotEmpty && results["scheme"] == coin.uriScheme) {
_address = (results["address"] ?? "").trim();
sendToController.text = _address!;
if (results["amount"] != null) {
final Amount amount = Decimal.parse(results["amount"]!).toAmount(
fractionDigits: coin.fractionDigits,
);
cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format(
amount,
withUnitName: false,
);
ref.read(pSendAmount.notifier).state = amount;
}
_setValidAddressProviders(_address);
setState(() {
_addressToggleFlag = sendToController.text.isNotEmpty;
});
}
} catch (e, s) {
Logging.instance
.log("Error processing QR code data: $e\n$s", level: LogLevel.Error);
}
}
@ -1516,12 +1588,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
},
child: const AddressBookIcon(),
),
// if (sendToController.text.isEmpty)
// TextFieldIconButton(
// key: const Key("sendViewScanQrButtonKey"),
// onTap: scanQr,
// child: const QrCodeIcon(),
// )
if (sendToController.text.isEmpty)
TextFieldIconButton(
semanticsLabel:
"Scan QR Button. Opens Camera For Scanning QR Code.",
key: const Key(
"sendViewScanQrButtonKey",
),
onTap: scanWebcam,
child: const QrCodeIcon(),
),
],
),
),
@ -1932,3 +2008,389 @@ String formatAddress(String epicAddress) {
}
return epicAddress;
}
class QrCodeScannerDialog extends StatefulWidget {
final String walletId;
final Function(String) onQrCodeDetected;
QrCodeScannerDialog({
required this.walletId,
required this.onQrCodeDetected,
});
@override
_QrCodeScannerDialogState createState() => _QrCodeScannerDialogState();
}
class _QrCodeScannerDialogState extends State<QrCodeScannerDialog> {
final CameraLinux? _cameraLinuxPlugin =
Platform.isLinux ? CameraLinux() : null;
final CameraWindows? _cameraWindowsPlugin =
Platform.isWindows ? CameraWindows() : null;
CameraMacOSController? _macOSController;
bool _isCameraOpen = false;
Image? _image;
bool _isScanning = false;
int _cameraId = -1;
String? _macOSDeviceId;
int _imageDelayInMs = 250;
@override
void initState() {
super.initState();
_isCameraOpen = false;
_isScanning = false;
_initializeCamera();
}
@override
void dispose() {
_stopCamera();
super.dispose();
}
Future<void> _initializeCamera() async {
try {
setState(() {
_isScanning = true; // Show the progress indicator
});
if (Platform.isLinux && _cameraLinuxPlugin != null) {
await _cameraLinuxPlugin!.initializeCamera();
Logging.instance.log("Linux Camera initialized", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
final List<CameraDescription> cameras =
await _cameraWindowsPlugin!.availableCameras();
if (cameras.isEmpty) {
throw CameraException('No cameras available', 'No cameras found.');
}
final CameraDescription camera = cameras[0]; // Could be user-selected.
_cameraId = await _cameraWindowsPlugin!.createCameraWithSettings(
camera,
const MediaSettings(
resolutionPreset: ResolutionPreset.low,
fps: 4,
videoBitrate: 200000,
enableAudio: false,
),
);
await _cameraWindowsPlugin!.initializeCamera(_cameraId);
// await _cameraWindowsPlugin!.onCameraInitialized(_cameraId).first;
// TODO [prio=low]: Make this work. ^^^
Logging.instance.log("Windows Camera initialized with ID: $_cameraId",
level: LogLevel.Info);
} else if (Platform.isMacOS) {
final List<CameraMacOSDevice> videoDevices = await CameraMacOS.instance
.listDevices(deviceType: CameraMacOSDeviceType.video);
if (videoDevices.isEmpty) {
throw Exception('No cameras available');
}
_macOSDeviceId = videoDevices.first.deviceId;
await CameraMacOS.instance
.initialize(cameraMacOSMode: CameraMacOSMode.photo);
setState(() {
_isCameraOpen = true;
});
Logging.instance.log(
"macOS Camera initialized with ID: $_macOSDeviceId",
level: LogLevel.Info);
}
if (mounted) {
setState(() {
_isCameraOpen = true;
_isScanning = true;
});
}
unawaited(_captureAndScanImage()); // Could be awaited.
} catch (e, s) {
Logging.instance
.log("Failed to initialize camera: $e\n$s", level: LogLevel.Error);
if (mounted) {
// widget.onSnackbar("Failed to initialize camera. Please try again.");
setState(() {
_isScanning = false;
});
}
}
}
Future<void> _stopCamera() async {
try {
if (Platform.isLinux && _cameraLinuxPlugin != null) {
_cameraLinuxPlugin!.stopCamera();
Logging.instance.log("Linux Camera stopped", level: LogLevel.Info);
} else if (Platform.isWindows && _cameraWindowsPlugin != null) {
// if (_cameraId >= 0) {
await _cameraWindowsPlugin!.dispose(_cameraId);
Logging.instance.log("Windows Camera stopped with ID: $_cameraId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("Windows Camera ID is null. Cannot dispose.",
// level: LogLevel.Error);
// }
} else if (Platform.isMacOS) {
// if (_macOSDeviceId != null) {
await CameraMacOS.instance.stopImageStream();
Logging.instance.log("macOS Camera stopped with ID: $_macOSDeviceId",
level: LogLevel.Info);
// } else {
// Logging.instance.log("macOS Camera ID is null. Cannot stop.",
// level: LogLevel.Error);
// }
}
} catch (e, s) {
Logging.instance
.log("Failed to stop camera: $e\n$s", level: LogLevel.Error);
} finally {
if (mounted) {
setState(() {
_isScanning = false;
_isCameraOpen = false;
});
}
}
}
Future<void> _captureAndScanImage() async {
while (_isCameraOpen && _isScanning) {
try {
String? base64Image;
if (Platform.isLinux && _cameraLinuxPlugin != null) {
base64Image = await _cameraLinuxPlugin!.captureImage();
} else if (Platform.isWindows) {
final XFile xfile =
await _cameraWindowsPlugin!.takePicture(_cameraId);
final bytes = await xfile.readAsBytes();
base64Image = base64Encode(bytes);
// We could use a Uint8List to optimize for Windows and macOS.
} else if (Platform.isMacOS) {
final macOSimg = await CameraMacOS.instance.takePicture();
if (macOSimg == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(macOSimg.bytes!);
if (image == null) {
Logging.instance
.log("Failed to capture image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
base64Image = base64Encode(Uint8List.fromList(img.encodePng(image)));
}
if (base64Image == null || base64Image.isEmpty) {
// Logging.instance
// .log("Failed to capture image", level: LogLevel.Error);
// Spammy.
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
final img.Image? image = img.decodeImage(base64Decode(base64Image));
// TODO [prio=low]: Optimize this process. Docs say:
// > WARNING Since this will check the image data against all known
// > decoders, it is much slower than using an explicit decoder
if (image == null) {
Logging.instance.log("Failed to decode image", level: LogLevel.Error);
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
continue;
}
if (mounted) {
setState(() {
_image = Image.memory(
base64Decode(base64Image!),
fit: BoxFit.cover,
);
});
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
if (mounted) {
Navigator.of(context).pop();
}
break;
} else {
// Logging.instance.log("No QR code found in the image", level: LogLevel.Info);
// if (mounted) {
// widget.onSnackbar("No QR code found in the image.");
// }
// Spammy.
}
await Future.delayed(Duration(milliseconds: _imageDelayInMs));
} catch (e, s) {
// Logging.instance.log("Failed to capture and scan image: $e\n$s", level: LogLevel.Error);
// Spammy.
// if (mounted) {
// widget.onSnackbar(
// "Error capturing or scanning the image. Please try again.");
// }
}
}
}
Future<String?> _scanImage(img.Image image) async {
try {
final LuminanceSource source = RGBLuminanceSource(
image.width,
image.height,
image
.convert(numChannels: 4)
.getBytes(order: img.ChannelOrder.abgr)
.buffer
.asInt32List(),
);
final BinaryBitmap bitmap =
BinaryBitmap(GlobalHistogramBinarizer(source));
final QRCodeReader reader = QRCodeReader();
final qrDecode = reader.decode(bitmap);
if (qrDecode.text.isEmpty) {
return null;
}
return qrDecode.text;
} catch (e, s) {
// Logging.instance.log("Failed to decode QR code: $e\n$s", level: LogLevel.Error);
// Spammy.
return null;
}
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 696,
maxHeight: 600,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Scan QR code",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
Expanded(
child: _isCameraOpen
? _image != null
? _image!
: const Center(
child: CircularProgressIndicator(),
)
: const Center(
child:
CircularProgressIndicator(), // Show progress indicator immediately
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(child: Container()),
// "Select file" button.
SecondaryButton(
buttonHeight: ButtonHeight.l,
label: "Select file",
width: 200,
onPressed: () async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["png", "jpg", "jpeg"],
);
if (result == null || result.files.single.path == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "No file selected.",
),
);
return;
}
final filePath = result?.files.single.path!;
if (filePath == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "Error selecting file.",
),
);
return;
}
try {
final img.Image? image =
img.decodeImage(File(filePath!).readAsBytesSync());
if (image == null) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "Failed to decode image.",
),
);
return;
}
final String? scanResult = await _scanImage(image);
if (scanResult != null && scanResult.isNotEmpty) {
widget.onQrCodeDetected(scanResult);
Navigator.of(context).pop();
} else {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message: "No QR code found in the image.",
),
);
}
} catch (e, s) {
Logging.instance.log("Failed to decode image: $e\n$s",
level: LogLevel.Error);
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Error scanning QR code",
message:
"Error processing the image. Please try again.",
),
);
}
},
),
const SizedBox(width: 16),
// Close button.
PrimaryButton(
buttonHeight: ButtonHeight.l,
label: "Close",
width: 272.5,
onPressed: () {
_stopCamera();
Navigator.of(context).pop();
},
),
],
),
),
],
),
);
}
}

View file

@ -353,6 +353,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
final wallet = ref.watch(pWallets).getWallet(widget.walletId);
final coin = wallet.info.coin;
final prefs = ref.watch(prefsChangeNotifierProvider);
final showExchange = prefs.enableExchange;
final showMore = wallet is PaynymInterface ||
(wallet is CoinControlInterface &&
ref.watch(
@ -368,7 +371,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
return Row(
children: [
if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.swap))
if (Constants.enableExchange &&
AppConfig.hasFeature(AppFeature.swap) &&
showExchange)
SecondaryButton(
label: "Swap",
width: buttonWidth,
@ -383,11 +388,15 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> {
),
onPressed: () => _onSwapPressed(),
),
if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.buy))
if (Constants.enableExchange &&
AppConfig.hasFeature(AppFeature.buy) &&
showExchange)
const SizedBox(
width: 16,
),
if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.buy))
if (Constants.enableExchange &&
AppConfig.hasFeature(AppFeature.buy) &&
showExchange)
SecondaryButton(
label: "Buy",
width: buttonWidth,

View file

@ -20,7 +20,6 @@ import '../../../../../providers/global/wallets_provider.dart';
import '../../../../../themes/stack_colors.dart';
import '../../../../../utilities/assets.dart';
import '../../../../../utilities/text_styles.dart';
import '../../../../../utilities/util.dart';
import '../../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../../wallets/isar/models/wallet_info.dart';
import '../../../../../wallets/isar/providers/wallet_info_provider.dart';
@ -106,117 +105,122 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
}
}
late final DSBController _switchController;
bool _switchReuseAddressToggledLock = false; // Mutex.
Future<void> _switchReuseAddressToggled(bool newValue) async {
if (newValue) {
await showDialog(
context: context,
builder: (context) {
final isDesktop = Util.isDesktop;
return DesktopDialog(
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(
mainAxisSize: MainAxisSize.min,
Future<void> _switchReuseAddressToggled() async {
if (_switchReuseAddressToggledLock) {
return;
}
_switchReuseAddressToggledLock = true; // Lock mutex.
try {
if (_switchController.isOn?.call() != true) {
final canContinue = await showDialog<bool?>(
context: context,
builder: (context) {
return DesktopDialog(
maxWidth: 576,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
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",
),
),
],
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
"Warning!",
style: STextStyles.desktopH3(context),
),
),
const DesktopDialogCloseButton(),
],
),
),
],
),
);
},
).then((confirmed) async {
if (_switchReuseAddressToggledLock) {
return;
}
_switchReuseAddressToggledLock = true; // Lock mutex.
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",
),
),
],
),
],
),
),
],
),
);
},
);
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;
if (canContinue == true) {
await _updateAddressReuse(true);
}
});
} else {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: false,
},
isar: ref.read(mainDBProvider).isar,
);
} else {
await _updateAddressReuse(false);
}
} finally {
// ensure _switchReuseAddressToggledLock is set to false no matter what.
_switchReuseAddressToggledLock = false;
}
}
Future<void> _updateAddressReuse(bool shouldReuse) async {
await ref.read(pWalletInfo(widget.walletId)).updateOtherData(
newEntries: {
WalletInfoKeys.reuseAddress: shouldReuse,
},
isar: ref.read(mainDBProvider).isar,
);
if (_switchController.isOn != null) {
if (_switchController.isOn!.call() != shouldReuse) {
_switchController.activate?.call();
}
}
}
@override
void initState() {
_switchController = DSBController();
super.initState();
}
@override
Widget build(BuildContext context) {
final wallet = ref.watch(
@ -370,19 +374,22 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
),
// reuseAddress preference.
_MoreFeaturesItemBase(
onPressed: _switchReuseAddressToggled,
child: Row(
children: [
const SizedBox(width: 3),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId)
.select((value) => value.otherData),
)[WalletInfoKeys.reuseAddress] as bool? ??
false,
onValueChanged: _switchReuseAddressToggled,
child: IgnorePointer(
child: DraggableSwitchButton(
isOn: ref.watch(
pWalletInfo(widget.walletId)
.select((value) => value.otherData),
)[WalletInfoKeys.reuseAddress] as bool? ??
false,
controller: _switchController,
),
),
),
const SizedBox(
@ -392,7 +399,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Reuse receiving address by default",
"Reuse receiving address",
style: STextStyles.w600_20(context),
),
],

View file

@ -162,6 +162,47 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
],
),
),
// showExchange pref.
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(
thickness: 0.5,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Enable exchange features",
style: STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider.select(
(value) => value.enableExchange,
),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.enableExchange = newValue;
},
),
),
],
),
),
const Padding(
padding: EdgeInsets.all(10.0),
child: Divider(

View file

@ -284,7 +284,13 @@ abstract class Frost {
static String createSignConfig({
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 String changeAddress,
required int feePerWeight,
@ -299,6 +305,7 @@ abstract class Frost {
vout: e.utxo.vout,
value: e.utxo.value,
scriptPubKey: e.scriptPubKey,
addressDerivationData: e.addressDerivationData,
),
)
.toList(),

View file

@ -8,13 +8,16 @@
*
*/
import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../models/notification_model.dart';
import '../utilities/prefs.dart';
import 'notifications_service.dart';
class NotificationApi {
abstract final class NotificationApi {
static Completer<void>? _initCalledCompleter;
static final _notifications = FlutterLocalNotificationsPlugin();
// static final onNotifications = BehaviorSubject<String?>();
@ -33,6 +36,16 @@ class NotificationApi {
}
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 iOS = DarwinInitializationSettings();
const linux = LinuxInitializationSettings(
@ -54,12 +67,18 @@ class NotificationApi {
// 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 =>
_notifications.cancel(id);
static Future<void> clearNotification(int id) async {
await init();
await _notifications.cancel(id);
}
//===================================
static late Prefs prefs;
@ -79,6 +98,7 @@ class NotificationApi {
String? changeNowId,
String? payload,
}) async {
await init();
await prefs.incrementCurrentNotificationIndex();
final id = prefs.currentNotificationId;

View file

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

View file

@ -71,6 +71,7 @@ class Prefs extends ChangeNotifier {
_useTor = await _getUseTor();
_fusionServerInfo = await _getFusionServerInfo();
_autoPin = await _getAutoPin();
_enableExchange = await _getEnableExchange();
_initialized = true;
}
@ -1131,4 +1132,30 @@ class Prefs extends ChangeNotifier {
) as bool? ??
false;
}
// Show or hide exchange (buy & swap) features.
bool _enableExchange = true;
bool get enableExchange => _enableExchange;
set enableExchange(bool showExchange) {
if (_enableExchange != showExchange) {
DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "showExchange",
value: showExchange,
);
_enableExchange = showExchange;
notifyListeners();
}
}
Future<bool> _getEnableExchange() async {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs,
key: "showExchange",
) as bool? ??
true;
}
}

View file

@ -38,34 +38,93 @@ Future<MoneroNodeConnectionResponse> testMoneroNodeConnection(
int port,
})? proxyInfo,
}) async {
final httpClient = HttpClient();
MoneroNodeConnectionResponse? badCertResponse;
try {
if (proxyInfo != null) {
SocksTCPClient.assignToHttpClient(httpClient, [
ProxySettings(
proxyInfo.host,
proxyInfo.port,
),
]);
if (uri.host.endsWith(".onion")) {
if (proxyInfo == null) {
// If the host ends in .onion, we can't access it without Tor.
return MoneroNodeConnectionResponse(null, null, null, false);
}
httpClient.badCertificateCallback = (cert, url, port) {
if (allowBadX509Certificate) {
return true;
SOCKSSocket? socket;
try {
// An HttpClient cannot be used for onion nodes.
//
// The SOCKSSocket class from the tor_ffi_plugin package can be used to
// connect to .onion addresses. We'll do the same things as above but
// with SOCKSSocket instead of httpClient.
socket = await SOCKSSocket.create(
proxyHost: proxyInfo.host.address,
proxyPort: proxyInfo.port,
sslEnabled: false,
);
await socket.connect();
await socket.connectTo(uri.host, uri.port);
final body = jsonEncode({
"jsonrpc": "2.0",
"id": "0",
"method": "get_info",
});
final request = 'POST /json_rpc HTTP/1.1\r\n'
'Host: ${uri.host}\r\n'
'Content-Type: application/json\r\n'
'Content-Length: ${body.length}\r\n'
'\r\n'
'$body';
socket.write(request);
print("Request sent: $request");
final buffer = StringBuffer();
await for (var response in socket.inputStream) {
buffer.write(utf8.decode(response));
if (buffer.toString().contains("\r\n\r\n")) {
break;
}
}
if (badCertResponse == null) {
badCertResponse = MoneroNodeConnectionResponse(cert, url, port, false);
} else {
final result = buffer.toString();
print("Response received: $result");
// Check if the response contains "results" and does not contain "error"
final success =
result.contains('"result":') && !result.contains('"error"');
return MoneroNodeConnectionResponse(null, null, null, success);
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
return MoneroNodeConnectionResponse(null, null, null, false);
} finally {
await socket?.close();
}
} else {
final httpClient = HttpClient();
MoneroNodeConnectionResponse? badCertResponse;
try {
if (proxyInfo != null) {
SocksTCPClient.assignToHttpClient(httpClient, [
ProxySettings(
proxyInfo.host,
proxyInfo.port,
),
]);
}
httpClient.badCertificateCallback = (cert, url, port) {
if (allowBadX509Certificate) {
return true;
}
if (badCertResponse == null) {
badCertResponse =
MoneroNodeConnectionResponse(cert, url, port, false);
} else {
return false;
}
return false;
}
};
return false;
};
if (!uri.host.endsWith('.onion')) {
final request = await httpClient.postUrl(uri);
final body = utf8.encode(
@ -91,62 +150,22 @@ Future<MoneroNodeConnectionResponse> testMoneroNodeConnection(
final response = await request.close();
final result = await response.transform(utf8.decoder).join();
// TODO: json decoded without error so assume connection exists?
// or we can check for certain values in the response to decide
return MoneroNodeConnectionResponse(null, null, null, true);
} else {
// If the URL ends in .onion, we can't use an httpClient to connect to it.
//
// The SOCKSSocket class from the tor_ffi_plugin package can be used to
// connect to .onion addresses. We'll do the same things as above but
// with SOCKSSocket instead of httpClient.
final socket = await SOCKSSocket.create(
proxyHost: proxyInfo!.host.address,
proxyPort: proxyInfo.port,
sslEnabled: false,
);
await socket.connect();
await socket.connectTo(uri.host, uri.port);
// print("HTTP Response: $result");
final body = utf8.encode(
jsonEncode({
"jsonrpc": "2.0",
"id": "0",
"method": "get_info",
}),
);
final success =
result.contains('"result":') && !result.contains('"error"');
// Write the request body to the socket.
socket.write(body);
// Read the response.
final response = await socket.inputStream.first;
final result = utf8.decode(response);
// Close the socket.
await socket.close();
return MoneroNodeConnectionResponse(null, null, null, true);
// Parse the response.
//
// This is commented because any issues should throw.
// final Map<String, dynamic> jsonResponse = jsonDecode(result);
// print(jsonResponse);
// if (jsonResponse.containsKey('result')) {
// return MoneroNodeConnectionResponse(null, null, null, true);
// } else {
// return MoneroNodeConnectionResponse(null, null, null, false);
// }
return MoneroNodeConnectionResponse(null, null, null, success);
} catch (e, s) {
if (badCertResponse != null) {
return badCertResponse!;
} else {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
return MoneroNodeConnectionResponse(null, null, null, false);
}
} finally {
httpClient.close(force: true);
}
} catch (e, s) {
if (badCertResponse != null) {
return badCertResponse!;
} else {
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
return MoneroNodeConnectionResponse(null, null, null, false);
}
} finally {
httpClient.close(force: true);
}
}

View file

@ -41,11 +41,16 @@ Future<bool> _xmrHelper(
final uriString = "${uri.scheme}://${uri.host}:${port ?? 0}$path";
if (proxyInfo == null && uri.host.endsWith(".onion")) {
return false;
}
final response = await testMoneroNodeConnection(
Uri.parse(uriString),
false,
proxyInfo: proxyInfo,
);
).timeout(Duration(seconds: proxyInfo != null ? 30 : 10));
if (response.cert != null) {
if (context.mounted) {
@ -109,7 +114,7 @@ Future<bool> testNodeConnection({
final url = formData.host!;
final uri = Uri.tryParse(url);
if (uri != null) {
if (!uri.hasScheme) {
if (!uri.hasScheme && !uri.host.endsWith(".onion")) {
// try https first
testPassed = await _xmrHelper(
formData

View file

@ -127,9 +127,21 @@ class BitcoinFrost extends FrostCurrency {
);
@override
String pubKeyToScriptHash({required Uint8List pubKey}) {
Uint8List addressToPubkey({required String address}) {
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) {
rethrow;
}

View file

@ -6,7 +6,11 @@ import '../crypto_currency.dart';
abstract class FrostCurrency extends CryptoCurrency {
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;
}

View file

@ -1,10 +1,13 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:frostdart/frostdart.dart' as frost;
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:frostdart/util.dart';
import 'package:isar/isar.dart';
import '../../../electrumx_rpc/cached_electrumx_client.dart';
import '../../../electrumx_rpc/electrumx_client.dart';
import '../../../models/balance.dart';
@ -24,10 +27,13 @@ import '../../../utilities/logger.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../crypto_currency/intermediate/frost_currency.dart';
import '../../isar/models/frost_wallet_info.dart';
import '../../isar/models/wallet_info.dart';
import '../../models/tx_data.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)
: super(BitcoinFrost(network) as T);
@ -77,25 +83,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
await mainDB.isar.frostWalletInfo.put(frostWalletInfo);
});
final keys = frost.deserializeKeys(keys: serializedKeys);
final addressString = frost.addressForKeys(
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
? 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,
final address = await _generateAddress(
change: 0,
index: 0,
serializedKeys: serializedKeys,
);
await mainDB.putAddresses([address]);
@ -110,7 +101,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
Future<TxData> frostCreateSignConfig({
required TxData txData,
required String changeAddress,
required int feePerWeight,
}) async {
try {
@ -152,44 +142,86 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
fractionDigits: cryptoCurrency.fractionDigits,
);
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(
rawValue: BigInt.from(utxo.value),
fractionDigits: cryptoCurrency.fractionDigits,
);
utxosToUse.add(utxo);
if (sum > total) {
if (i + 1 < utxos.length) {
utxosRemaining.addAll(utxos.sublist(i));
}
break;
}
}
final serializedKeys = await getSerializedKeys();
final keys = frost.deserializeKeys(keys: serializedKeys!);
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet;
final publicKey = frost
.scriptPubKeyForKeys(
keys: keys,
)
.toUint8ListFromHex;
final List<
({
UTXO utxo,
Uint8List scriptPubKey,
({int account, int index, bool change}) addressDerivationData
})> inputs = [];
final config = Frost.createSignConfig(
network: network,
inputs: utxosToUse
.map(
(e) => (
utxo: e,
for (final utxo in utxosToUse) {
final dData = await getDerivationData(
utxo.address,
);
final publicKey = cryptoCurrency.addressToPubkey(
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,
addressDerivationData: dData,
),
)
.toList(),
outputs: txData.recipients!,
changeAddress: (await getCurrentReceivingAddress())!.value,
feePerWeight: feePerWeight,
);
);
} else {
rethrow;
}
}
}
return txData.copyWith(frostMSConfig: config, utxos: utxosToUse);
} 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<
({
Pointer<TransactionSignMachineWrapper> machinePtr,
@ -324,16 +394,28 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
@override
Future<void> updateTransactions() async {
final myAddress = (await getCurrentReceivingAddress())!;
// Get all addresses.
final List<Address> allAddressesOld =
await _fetchAddressesForElectrumXScan();
final scriptHash = cryptoCurrency.pubKeyToScriptHash(
pubKey: Uint8List.fromList(myAddress.publicKey),
);
final allTxHashes =
(await electrumXClient.getHistory(scripthash: scriptHash)).toSet();
// Separate receiving and change addresses.
final Set<String> receivingAddresses = allAddressesOld
.where((e) => e.subType == AddressSubType.receiving)
.map((e) => e.value)
.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 coin = info.coin;
// Fetch history from ElectrumX.
final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddressesSet);
final List<Map<String, dynamic>> allTransactions = [];
@ -350,7 +432,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
final tx = await electrumXCachedClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
cryptoCurrency: coin,
cryptoCurrency: cryptoCurrency,
);
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
@ -371,6 +453,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
// Parse inputs.
BigInt amountReceivedInThisWallet = BigInt.zero;
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
final List<InputV2> inputs = [];
for (final jsonInput in txData["vin"] as List) {
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.
if (input.addresses.contains(myAddress.value)) {
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
wasSentFromThisWallet = 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.addresses.contains(myAddress.value)) {
if (receivingAddresses
.intersection(output.addresses.toSet())
.isNotEmpty) {
wasReceivedInThisWallet = true;
amountReceivedInThisWallet += output.value;
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);
@ -478,7 +569,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
type = TransactionType.outgoing;
if (wasReceivedInThisWallet) {
if (amountReceivedInThisWallet == totalOut) {
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
totalOut) {
// Definitely sent all to self.
type = TransactionType.sentToSelf;
} else if (amountReceivedInThisWallet == BigInt.zero) {
@ -488,6 +580,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
} else if (wasReceivedInThisWallet) {
// Only found outputs owned by this wallet.
type = TransactionType.incoming;
// TODO: [prio=none] Check for special Bitcoin outputs like ordinals.
} else {
Logging.instance.log(
"Unexpected tx found (ignoring it): $txData",
@ -524,25 +618,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
if (address == null) {
final serializedKeys = await getSerializedKeys();
if (serializedKeys != null) {
final keys = frost.deserializeKeys(keys: serializedKeys);
final addressString = frost.addressForKeys(
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
? 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,
final address = await _generateAddress(
change: 0,
index: 0,
serializedKeys: serializedKeys,
);
await mainDB.updateOrPutAddresses([address]);
@ -729,30 +808,79 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
await mainDB.deleteWalletBlockchainData(walletId);
}
final keys = frost.deserializeKeys(keys: serializedKeys!);
await _saveSerializedKeys(serializedKeys!);
await _saveMultisigConfig(multisigConfig!);
final addressString = frost.addressForKeys(
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet,
keys: keys,
const receiveChain = 0;
const changeChain = 1;
final List<Future<({int index, List<Address> addresses})>>
receiveFutures = [
_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);
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(addressesToStore);
});
GlobalEventBus.instance.fire(
@ -868,23 +996,31 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
@override
Future<bool> updateUTXOs() async {
final address = await getCurrentReceivingAddress();
final allAddresses = await _fetchAddressesForElectrumXScan();
try {
final scriptHash = cryptoCurrency.pubKeyToScriptHash(
pubKey: Uint8List.fromList(address!.publicKey),
);
final fetchedUtxoList = <List<Map<String, dynamic>>>[];
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 = [];
for (int i = 0; i < utxos.length; i++) {
final utxo = await _parseUTXO(
jsonUTXO: utxos[i],
);
for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
final utxo = await _parseUTXO(
jsonUTXO: fetchedUtxoList[i][j],
);
outputArray.add(utxo);
outputArray.add(utxo);
}
}
return await mainDB.updateUTXOs(walletId, outputArray);
@ -1174,4 +1310,389 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
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

@ -202,7 +202,10 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface {
Future<void> updateNode() async {
final node = getCurrentNode();
final host = Uri.parse(node.host).host;
String host = Uri.parse(node.host).host;
if (host.isEmpty) {
host = node.host;
}
({InternetAddress host, int port})? proxy;
if (prefs.useTor) {
proxy = TorService.sharedInstance.getProxyInfo();

View file

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

View file

@ -183,14 +183,75 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
}
Future<Amount> estimateFeeForSpark(Amount amount) async {
// int spendAmount = amount.raw.toInt();
// if (spendAmount == 0) {
return Amount(
rawValue: BigInt.from(0),
fractionDigits: cryptoCurrency.fractionDigits,
);
// }
// TODO actual fee estimation
final spendAmount = amount.raw.toInt();
if (spendAmount == 0) {
return Amount(
rawValue: BigInt.from(0),
fractionDigits: cryptoCurrency.fractionDigits,
);
} else {
// 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
@ -374,7 +435,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
recipientCount + (txData.sparkRecipients?.length ?? 0);
final BigInt estimatedFee;
if (isSendAll) {
final estFee = LibSpark.estimateSparkFee(
final estFee = await _asyncSparkFeesWrapper(
privateKeyHex: privateKey.toHex,
index: kDefaultSparkIndex,
sendAmount: txAmount.raw.toInt(),
@ -2002,3 +2063,53 @@ class MutableSparkRecipient {
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

@ -77,6 +77,7 @@ class DraggableSwitchButtonState extends State<DraggableSwitchButton> {
_enabled = widget.enabled;
valueListener = _isOn ? ValueNotifier(1.0) : ValueNotifier(0.0);
widget.controller?.isOn = () => _isOn;
widget.controller?.activate = () {
_isOn = !_isOn;
// widget.onValueChanged?.call(_isOn);
@ -212,4 +213,5 @@ class DraggableSwitchButtonState extends State<DraggableSwitchButton> {
class DSBController {
VoidCallback? activate;
bool Function()? isOn;
}

View file

@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
camera_linux
coinlib_flutter
flutter_libsparkmobile
frostdart

View file

@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import camera_macos
import connectivity_plus
import desktop_drop
import device_info_plus
@ -24,6 +25,7 @@ import wakelock_macos
import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
CameraMacosPlugin.register(with: registry.registrar(forPlugin: "CameraMacosPlugin"))
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))

View file

@ -1,5 +1,7 @@
PODS:
- coinlib_flutter (0.3.2):
- camera_macos (0.0.1):
- FlutterMacOS
- coinlib_flutter (0.5.0):
- Flutter
- FlutterMacOS
- connectivity_plus (0.0.1):
@ -34,14 +36,14 @@ PODS:
- ReachabilitySwift (5.0.0)
- share_plus (0.0.1):
- FlutterMacOS
- sqlite3 (3.46.0):
- sqlite3/common (= 3.46.0)
- sqlite3/common (3.46.0)
- sqlite3/fts5 (3.46.0):
- "sqlite3 (3.46.0+1)":
- "sqlite3/common (= 3.46.0+1)"
- "sqlite3/common (3.46.0+1)"
- "sqlite3/fts5 (3.46.0+1)":
- sqlite3/common
- sqlite3/perf-threadsafe (3.46.0):
- "sqlite3/perf-threadsafe (3.46.0+1)":
- sqlite3/common
- sqlite3/rtree (3.46.0):
- "sqlite3/rtree (3.46.0+1)":
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- FlutterMacOS
@ -60,6 +62,7 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
- camera_macos (from `Flutter/ephemeral/.symlinks/plugins/camera_macos/macos`)
- coinlib_flutter (from `Flutter/ephemeral/.symlinks/plugins/coinlib_flutter/darwin`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
@ -89,6 +92,8 @@ SPEC REPOS:
- sqlite3
EXTERNAL SOURCES:
camera_macos:
:path: Flutter/ephemeral/.symlinks/plugins/camera_macos/macos
coinlib_flutter:
:path: Flutter/ephemeral/.symlinks/plugins/coinlib_flutter/darwin
connectivity_plus:
@ -135,7 +140,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_size/macos
SPEC CHECKSUMS:
coinlib_flutter: 6abec900d67762a6e7ccfd567a3cd3ae00bbee35
camera_macos: c2603f5eed16f05076cf17e12030d2ce55a77839
coinlib_flutter: 9275e8255ef67d3da33beb6e117d09ced4f46eb5
connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
@ -152,7 +158,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7
sqlite3: 154b084339ede06960a5b3c8160066adc9176b7d
sqlite3: 292c3e1bfe89f64e51ea7fc7dab9182a017c8630
sqlite3_flutter_libs: 1be4459672f8168ded2d8667599b8e3ca5e72b83
stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c
tor_ffi_plugin: 2566c1ed174688cca560fa0c64b7a799c66f07cb

View file

@ -1,9 +1,13 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}

View file

@ -10,5 +10,9 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

View file

@ -28,5 +28,11 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSCameraUsageDescription</key>
<string>QR Code scanning</string>
<key>NSMicrophoneUsageDescription</key>
<string>QR Code Scanning. A temporary requirement due to limitations in the camera_macos package that are being worked on to remove the need for this permission.</string>
<key>NSCameraUseContinuityCameraDeviceType</key>
<true/>
</dict>
</plist>

View file

@ -8,5 +8,9 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>

View file

@ -246,6 +246,39 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
camera_linux:
dependency: "direct main"
description:
name: camera_linux
sha256: "6ea08c23f643364e650e8fad73653747c049cbd00803a7c317132379ee3653ac"
url: "https://pub.dev"
source: hosted
version: "0.0.8"
camera_macos:
dependency: "direct main"
description:
name: camera_macos
sha256: e2ac75c56560f0e86a44de839e6e7f792d786e224c653d51d0c745db80adaff8
url: "https://pub.dev"
source: hosted
version: "0.0.8"
camera_platform_interface:
dependency: "direct main"
description:
name: camera_platform_interface
sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061
url: "https://pub.dev"
source: hosted
version: "2.8.0"
camera_windows:
dependency: "direct main"
description:
path: "packages/camera/camera_windows"
ref: HEAD
resolved-ref: "9bfbfd643ba4e6865ec34124e42a1cc502c400c0"
url: "https://github.com/cypherstack/packages.git"
source: git
version: "0.2.4"
characters:
dependency: transitive
description:
@ -254,6 +287,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
url: "https://pub.dev"
source: hosted
version: "1.3.1"
checked_yaml:
dependency: transitive
description:
@ -830,10 +871,10 @@ packages:
dependency: transitive
description:
name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@ -2128,6 +2169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
zxing2:
dependency: "direct main"
description:
name: zxing2
sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c"
url: "https://pub.dev"
source: hosted
version: "0.2.3"
sdks:
dart: ">=3.3.4 <4.0.0"
flutter: ">=3.19.6"

View file

@ -190,6 +190,14 @@ dependencies:
calendar_date_picker2: ^1.0.2
sqlite3: ^2.4.3
sqlite3_flutter_libs: ^0.5.22
camera_linux: ^0.0.8
zxing2: ^0.2.3
camera_windows:
git: # TODO [prio=low]: Revert to official after https://github.com/flutter/packages/pull/7067.
url: https://github.com/cypherstack/packages.git
path: packages/camera/camera_windows
camera_platform_interface: ^2.8.0
camera_macos: ^0.0.8
dev_dependencies:
flutter_test:

View file

@ -92,8 +92,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/s
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwallet2_api_c.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "wownero_libwallet2_api_c.dll"
COMPONENT Runtime)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.dll"
COMPONENT Runtime)
#install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libgcc_s_seh-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libgcc_s_seh-1.dll"
# COMPONENT Runtime)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libpolyseed.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libpolyseed.dll"
COMPONENT Runtime)
@ -101,8 +101,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/s
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libssp-0.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libssp-0.dll"
COMPONENT Runtime)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll"
COMPONENT Runtime)
#install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll"
# COMPONENT Runtime)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../crypto_plugins/flutter_libmonero/scripts/monero_c/release/wownero/x86_64-w64-mingw32_libwinpthread-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwinpthread-1.dll"
COMPONENT Runtime)

View file

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

View file

@ -1000,6 +1000,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null,
);
@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(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -473,6 +473,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs {
returnValueForMissingStub: null,
);
@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(
Invocation.getter(#hasListeners),
returnValue: false,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -729,6 +729,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null,
);
@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(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -615,6 +615,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs {
returnValueForMissingStub: null,
);
@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(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -718,6 +718,19 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs {
returnValueForMissingStub: null,
);
@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(
Invocation.getter(#hasListeners),
returnValue: false,

View file

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <camera_windows/camera_windows.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <flutter_libepiccash/flutter_libepiccash_plugin_c_api.h>
@ -19,6 +20,8 @@
#include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
CameraWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CameraWindows"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DesktopDropPluginRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
camera_windows
connectivity_plus
desktop_drop
flutter_libepiccash