mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-03-29 18:48:51 +00:00
Merge branch 'staging' into uri
This commit is contained in:
commit
dd11214b3c
72 changed files with 3943 additions and 743 deletions
asset_sources/default_themes/campfire
crypto_plugins
docs
lib
main.dartstack_privacy_calls.dart
models/isar/models
pages
address_book_views/subviews
exchange_view
home_view
receive_view/addresses
send_view
settings_views
global_settings_view
advanced_views
stack_backup_views/helpers
wallet_settings_view/wallet_settings_wallet_settings
token_view/sub_widgets
wallet_view
wallets_view
pages_desktop_specific
desktop_exchange/exchange_steps
desktop_menu.dartdesktop_menu_item.dartmy_stack_view/wallet_view/sub_widgets
settings/settings_menu/advanced_settings
services
themes
utilities
wallets
widgets/custom_buttons
linux/flutter
macos
pubspec.lockscripts/app_config/templates
test
cached_electrumx_test.mocks.dart
pages/send_view
screen_tests/exchange
services/coins
bitcoin
bitcoincash
dogecoin
namecoin
particl
widget_tests
windows/flutter
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit b654bf4488357c8a104900e11f9468d54a39f22b
|
||||
Subproject commit f8746dbef5c5ad5ed2dad12f615723d087083e9c
|
|
@ -1 +1 @@
|
|||
Subproject commit 982f5ab19fe0dd3dd3f6be2c46f8dff13d49027c
|
||||
Subproject commit db7585d8cd493b143e0a0652c618904d1f636d1d
|
|
@ -1 +1 @@
|
|||
Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2
|
||||
Subproject commit 2a74a97fb0f0e22a5280b22c010b710cdeec33bb
|
|
@ -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:
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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 =>
|
||||
|
|
35
lib/models/isar/models/sent_to_address.dart
Normal file
35
lib/models/isar/models/sent_to_address.dart
Normal 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;
|
||||
}
|
1248
lib/models/isar/models/sent_to_address.g.dart
Normal file
1248
lib/models/isar/models/sent_to_address.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: () {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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._();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
camera_linux
|
||||
coinlib_flutter
|
||||
flutter_libsparkmobile
|
||||
frostdart
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
53
pubspec.lock
53
pubspec.lock
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
camera_windows
|
||||
connectivity_plus
|
||||
desktop_drop
|
||||
flutter_libepiccash
|
||||
|
|
Loading…
Reference in a new issue