From 5545137047f0d0a9cebdabec5c4d8e97a1baf568 Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 31 May 2024 08:55:28 -0300 Subject: [PATCH 01/20] Sp fixes (#1471) * feat: missing desktop setting menu * fix: sp utxo pending --- cw_bitcoin/lib/electrum_wallet.dart | 19 ++++++++++++++++++- cw_bitcoin/pubspec.lock | 4 ++-- lib/src/widgets/setting_actions.dart | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 96f871a4b..43bae2e19 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1465,7 +1465,7 @@ abstract class ElectrumWalletBase time = status["block_time"] as int?; final height = status["block_height"] as int? ?? 0; - final tip = await getCurrentChainTip(); + final tip = await getUpdatedChainTip(); if (tip > 0) confirmations = height > 0 ? tip - height + 1 : 0; } else { final verboseTransaction = await electrumClient.getTransactionRaw(hash: hash); @@ -1519,6 +1519,23 @@ abstract class ElectrumWalletBase await fetchTransactionsForAddressType(historiesWithDetails, SegwitAddresType.p2wpkh); } + transactionHistory.transactions.values.forEach((tx) async { + final isPendingSilentPaymentUtxo = + (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null; + + if (isPendingSilentPaymentUtxo) { + final info = + await fetchTransactionInfo(hash: tx.id, height: tx.height, retryOnFailure: true); + + if (info != null) { + tx.confirmations = info.confirmations; + tx.isPending = tx.confirmations == 0; + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + } + }); + return historiesWithDetails; } catch (e) { print(e.toString()); diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 3eadcb112..997ed9452 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -793,8 +793,8 @@ packages: dependency: "direct main" description: path: "." - ref: "sp_v1.0.0" - resolved-ref: a9a4c6d051f37a15a3a52cc2a0094f24c68b62c5 + ref: "sp_v2.0.0" + resolved-ref: "62c152b9086cd968019128845371072f7e1168de" url: "https://github.com/cake-tech/sp_scanner" source: git version: "0.0.1" diff --git a/lib/src/widgets/setting_actions.dart b/lib/src/widgets/setting_actions.dart index 6fbdb6868..272ed57c2 100644 --- a/lib/src/widgets/setting_actions.dart +++ b/lib/src/widgets/setting_actions.dart @@ -29,6 +29,7 @@ class SettingActions { connectionSettingAction, walletSettingAction, addressBookSettingAction, + silentPaymentsSettingAction, securityBackupSettingAction, privacySettingAction, displaySettingAction, From 43a4477b3919326d0123a3eb0a92955292267b8d Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 31 May 2024 15:57:30 +0300 Subject: [PATCH 02/20] fix Monero polyseed issue (#1474) Add desktop settings silent payment update versions --- assets/text/Release_Notes.txt | 3 +-- cw_monero/ios/Classes/monero_api.cpp | 1 - lib/di.dart | 2 +- .../desktop_settings/desktop_settings_page.dart | 11 ++++++++++- scripts/android/app_env.sh | 8 ++++---- scripts/ios/app_env.sh | 8 ++++---- scripts/macos/app_env.sh | 8 ++++---- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 557dd8b26..faad67777 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,2 +1 @@ -Bitcoin Silent Payments -Bug fixes and generic enhancements +Bug fixes and generic enhancements \ No newline at end of file diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 01a8d9a51..a2a17bd5e 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -399,7 +399,6 @@ extern "C" return false; } - wallet->store(std::string(path)); change_current_wallet(wallet); return true; } diff --git a/lib/di.dart b/lib/di.dart index bbad4a636..31655b402 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -571,7 +571,7 @@ Future setup({ getIt.registerFactory( () => Modify2FAPage(setup2FAViewModel: getIt.get())); - getIt.registerFactory(() => DesktopSettingsPage()); + getIt.registerFactory(() => DesktopSettingsPage(getIt.get())); getIt.registerFactoryParam( (pageOption, _) => ReceiveOptionViewModel(getIt.get().wallet!, pageOption)); diff --git a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart index 5355b7bb8..611b2acb7 100644 --- a/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart +++ b/lib/src/screens/settings/desktop_settings/desktop_settings_page.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/setting_action_button.dart'; import 'package:cake_wallet/src/widgets/setting_actions.dart'; import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/router.dart' as Router; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; @@ -10,7 +11,9 @@ import 'package:cake_wallet/themes/extensions/menu_theme.dart'; final _settingsNavigatorKey = GlobalKey(); class DesktopSettingsPage extends StatefulWidget { - const DesktopSettingsPage({super.key}); + const DesktopSettingsPage(this.dashboardViewModel, {super.key}); + + final DashboardViewModel dashboardViewModel; @override State createState() => _DesktopSettingsPageState(); @@ -51,6 +54,12 @@ class _DesktopSettingsPageState extends State { padding: EdgeInsets.only(top: 0), itemBuilder: (_, index) { final item = SettingActions.desktopSettings[index]; + + if (!widget.dashboardViewModel.hasSilentPayments && + item.name(context) == S.of(context).silent_payments_settings) { + return Container(); + } + final isLastTile = index == itemCount - 1; return SettingActionButton( isLastTile: isLastTile, diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 4578fd3d3..deceec53e 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.15.0" -MONERO_COM_BUILD_NUMBER=90 +MONERO_COM_VERSION="1.15.1" +MONERO_COM_BUILD_NUMBER=91 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.18.0" -CAKEWALLET_BUILD_NUMBER=216 +CAKEWALLET_VERSION="4.18.1" +CAKEWALLET_BUILD_NUMBER=217 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index ef038b6c7..8893d4842 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.15.0" -MONERO_COM_BUILD_NUMBER=88 +MONERO_COM_VERSION="1.15.1" +MONERO_COM_BUILD_NUMBER=89 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.18.0" -CAKEWALLET_BUILD_NUMBER=248 +CAKEWALLET_VERSION="4.18.1" +CAKEWALLET_BUILD_NUMBER=249 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index a04660514..e648f1aa0 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.5.0" -MONERO_COM_BUILD_NUMBER=21 +MONERO_COM_VERSION="1.5.1" +MONERO_COM_BUILD_NUMBER=22 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.11.0" -CAKEWALLET_BUILD_NUMBER=78 +CAKEWALLET_VERSION="1.11.1" +CAKEWALLET_BUILD_NUMBER=79 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From 287c2d8b60c9095926ad8f361b5bcd6be4e0962e Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 31 May 2024 14:57:40 +0000 Subject: [PATCH 03/20] CW-636-1-Unfocus text fields when push to next one (#1460) * unfocus text fields when push to next one * unfocus when push to next from new wallet / restore pages[skip ci]] --- lib/src/screens/base_page.dart | 33 ++++++++++++++----- lib/src/screens/exchange/exchange_page.dart | 8 +++++ .../cards/ionia_gift_card_detail_page.dart | 9 ++--- .../screens/new_wallet/new_wallet_page.dart | 8 +++++ .../new_wallet/new_wallet_type_page.dart | 8 +++++ .../restore/restore_from_backup_page.dart | 8 +++++ .../screens/restore/wallet_restore_page.dart | 8 +++++ lib/src/screens/send/send_page.dart | 8 +++++ lib/src/screens/send/send_template_page.dart | 8 +++++ lib/utils/route_aware.dart | 16 ++++----- 10 files changed, 90 insertions(+), 24 deletions(-) diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index 20c918be6..bf60525e4 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/route_aware.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/di.dart'; @@ -32,6 +33,14 @@ abstract class BasePage extends StatelessWidget { Widget? get endDrawer => null; + Function(BuildContext context)? get pushToWidget => null; + + Function(BuildContext context)? get pushToNextWidget => null; + + Function(BuildContext context)? get popWidget => null; + + Function(BuildContext context)? get popNextWidget => null; + AppBarStyle get appBarStyle => AppBarStyle.regular; Widget Function(BuildContext, Widget)? get rootWrapper => null; @@ -162,15 +171,21 @@ abstract class BasePage extends StatelessWidget { @override Widget build(BuildContext context) { - final root = Scaffold( - key: _scaffoldKey, - backgroundColor: pageBackgroundColor(context), - resizeToAvoidBottomInset: resizeToAvoidBottomInset, - extendBodyBehindAppBar: extendBodyBehindAppBar, - endDrawer: endDrawer, - appBar: appBar(context), - body: body(context), - floatingActionButton: floatingActionButton(context)); + final root = RouteAwareWidget( + child: Scaffold( + key: _scaffoldKey, + backgroundColor: pageBackgroundColor(context), + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + extendBodyBehindAppBar: extendBodyBehindAppBar, + endDrawer: endDrawer, + appBar: appBar(context), + body: body(context), + floatingActionButton: floatingActionButton(context)), + pushToWidget: (context) => pushToWidget?.call(context), + pushToNextWidget: (context) => pushToNextWidget?.call(context), + popWidget: (context) => popWidget?.call(context), + popNextWidget: (context) => popNextWidget?.call(context), + ); return rootWrapper?.call(context, root) ?? root; } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index e2d424fa0..5c064df27 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -99,6 +99,14 @@ class ExchangePage extends BasePage { @override AppBarStyle get appBarStyle => AppBarStyle.transparent; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget middle(BuildContext context) => Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart index dba78f557..dcd1d54b4 100644 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart @@ -84,12 +84,7 @@ class IoniaGiftCardDetailPage extends BasePage { } }); - return RouteAwareWidget( - pushToWidget: ()=> viewModel.increaseBrightness(), - pushToNextWidget: ()=> DeviceDisplayBrightness.setBrightness(viewModel.brightness), - popNextWidget: ()=> viewModel.increaseBrightness(), - popWidget: ()=> DeviceDisplayBrightness.setBrightness(viewModel.brightness), - child: ScrollableWithBottomSection( + return ScrollableWithBottomSection( contentPadding: EdgeInsets.all(24), content: Column( children: [ @@ -168,7 +163,7 @@ class IoniaGiftCardDetailPage extends BasePage { }, ), ), - )); + ); } Widget buildIoniaTile(BuildContext context, {required String title, required String subTitle}) { diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 8cc8a138d..8d46827eb 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -38,6 +38,14 @@ class NewWalletPage extends BasePage { @override String get title => S.current.new_wallet; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget body(BuildContext context) => WalletNameForm( _walletNewVM, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index dc22a60db..65c7bd59b 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -34,6 +34,14 @@ class NewWalletTypePage extends BasePage { String get title => isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget body(BuildContext context) => WalletTypeForm( onTypeSelected: onTypeSelected, diff --git a/lib/src/screens/restore/restore_from_backup_page.dart b/lib/src/screens/restore/restore_from_backup_page.dart index f7fddac3f..c5bc2a163 100644 --- a/lib/src/screens/restore/restore_from_backup_page.dart +++ b/lib/src/screens/restore/restore_from_backup_page.dart @@ -21,6 +21,14 @@ class RestoreFromBackupPage extends BasePage { @override String get title => S.current.restore_title_from_backup; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget body(BuildContext context) { reaction((_) => restoreFromBackupViewModel.state, (ExecutionState state) { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 6fcacfb0a..8e6ee0983 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -101,6 +101,14 @@ class WalletRestorePage extends BasePage { // String? derivationPath = null; DerivationInfo? derivationInfo; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget body(BuildContext context) { reaction((_) => walletRestoreViewModel.state, (ExecutionState state) { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 438c22c1d..b46a7f3db 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -66,6 +66,14 @@ class SendPage extends BasePage { @override bool get extendBodyBehindAppBar => true; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget? leading(BuildContext context) { final _backButton = Icon( diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 52458942c..76414ecb2 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -32,6 +32,14 @@ class SendTemplatePage extends BasePage { @override AppBarStyle get appBarStyle => AppBarStyle.transparent; + @override + Function(BuildContext)? get pushToNextWidget => (context) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; + @override Widget trailing(context) => Observer(builder: (_) { return sendTemplateViewModel.recipients.length > 1 diff --git a/lib/utils/route_aware.dart b/lib/utils/route_aware.dart index 28c72c4a4..d1721d912 100644 --- a/lib/utils/route_aware.dart +++ b/lib/utils/route_aware.dart @@ -10,10 +10,10 @@ class RouteAwareWidget extends StatefulWidget { this.popNextWidget}); final Widget child; - final Function()? pushToWidget; - final Function()? pushToNextWidget; - final Function()? popWidget; - final Function()? popNextWidget; + final Function(BuildContext context)? pushToWidget; + final Function(BuildContext context)? pushToNextWidget; + final Function(BuildContext context)? popWidget; + final Function(BuildContext context)? popNextWidget; @override State createState() => RouteAwareWidgetState(); @@ -35,28 +35,28 @@ class RouteAwareWidgetState extends State with RouteAware { @override void didPush() { if (widget.pushToWidget != null) { - widget.pushToWidget!(); + widget.pushToWidget!(context); } } @override void didPushNext() { if (widget.pushToNextWidget != null) { - widget.pushToNextWidget!(); + widget.pushToNextWidget!(context); } } @override void didPop() { if (widget.popWidget != null) { - widget.popWidget!(); + widget.popWidget!(context); } } @override void didPopNext() { if (widget.popNextWidget != null) { - widget.popNextWidget!(); + widget.popNextWidget!(context); } } From 30dc8f9238950226833c0adc861f0cc96158217f Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 6 Jun 2024 04:51:22 +0000 Subject: [PATCH 04/20] Cw 591 in app cake pay integration (#1376) * init commit * buy card UI * buy card detail page * card filter * dropdown button * user auth flow * create order * denomination option * fix searching * denom option fix UI * simulate payment * Update pr_test_build.yml * Update pr_test_build.yml * Implement order expiration handling [skip ci] * refactor code [skip ci] * remove ionia related code [skip ci] * change auth flow * add currency prefix * grid view UI * fix country filter issue * fix underline color * fix fetching card list [skip ci] * list view * update cake pay title * Optimize API usage by fetching CakePay vendors * handle no cards found case * adjust the flow of purchases * UI fixes * fix btc payment data * link extractor * fix fetch next page issue * UI fixes * fix text size * revert base page changes * Revert "revert base page changes" * UI fixes * fix UI * fix link style + localization * update cake pay title * update cake pay subtitle * Update cake_pay_order.dart * revert inject_app_details update --- .github/workflows/pr_test_build.yml | 4 + lib/cake_pay/cake_pay_api.dart | 245 +++++++++ lib/cake_pay/cake_pay_card.dart | 87 ++++ lib/cake_pay/cake_pay_order.dart | 131 +++++ .../cake_pay_payment_credantials.dart | 15 + lib/cake_pay/cake_pay_service.dart | 107 ++++ lib/cake_pay/cake_pay_states.dart | 67 +++ lib/cake_pay/cake_pay_user_credentials.dart | 6 + lib/cake_pay/cake_pay_vendor.dart | 51 ++ lib/di.dart | 230 +++------ lib/ionia/ionia_any_pay_payment_info.dart | 9 - lib/ionia/ionia_anypay.dart | 91 ---- lib/ionia/ionia_api.dart | 440 ---------------- lib/ionia/ionia_category.dart | 22 - lib/ionia/ionia_create_state.dart | 68 --- lib/ionia/ionia_gift_card.dart | 70 --- lib/ionia/ionia_gift_card_instruction.dart | 28 - lib/ionia/ionia_merchant.dart | 101 ---- lib/ionia/ionia_order.dart | 23 - lib/ionia/ionia_service.dart | 171 ------- lib/ionia/ionia_tip.dart | 18 - lib/ionia/ionia_token_data.dart | 43 -- lib/ionia/ionia_user_credentials.dart | 6 - lib/ionia/ionia_virtual_card.dart | 41 -- lib/router.dart | 95 +--- lib/routes.dart | 23 +- lib/src/screens/base_page.dart | 14 +- .../cake_pay/auth/cake_pay_account_page.dart | 90 ++++ .../auth/cake_pay_verify_otp_page.dart} | 40 +- .../auth/cake_pay_welcome_page.dart} | 65 ++- lib/src/screens/cake_pay/cake_pay.dart | 5 + .../cards/cake_pay_buy_card_page.dart | 474 +++++++++++++++++ .../cards/cake_pay_cards_page.dart} | 248 +++++---- .../cake_pay_confirm_purchase_card_page.dart | 403 +++++++++++++++ .../widgets/cake_pay_alert_modal.dart} | 4 +- .../widgets/cake_pay_tile.dart} | 4 +- .../screens/cake_pay/widgets/card_item.dart | 104 ++++ .../widgets/card_menu.dart | 0 .../cake_pay/widgets/image_placeholder.dart | 29 ++ .../cake_pay/widgets/link_extractor.dart | 66 +++ .../widgets/rounded_checkbox.dart | 0 .../widgets/text_icon_button.dart | 0 .../dashboard/pages/cake_features_page.dart | 52 +- .../dashboard/widgets/filter_widget.dart | 160 +++--- .../screens/dashboard/widgets/header_row.dart | 2 +- .../present_receive_option_picker.dart | 2 +- .../ionia/auth/ionia_create_account_page.dart | 159 ------ .../ionia/auth/ionia_welcome_page.dart | 95 ---- .../ionia/cards/ionia_account_cards_page.dart | 204 -------- .../ionia/cards/ionia_account_page.dart | 178 ------- .../cards/ionia_activate_debit_card_page.dart | 115 ----- .../cards/ionia_buy_card_detail_page.dart | 478 ------------------ .../ionia/cards/ionia_buy_gift_card.dart | 186 ------- .../ionia/cards/ionia_custom_redeem_page.dart | 175 ------- .../ionia/cards/ionia_custom_tip_page.dart | 177 ------- .../ionia/cards/ionia_debit_card_page.dart | 393 -------------- .../cards/ionia_gift_card_detail_page.dart | 215 -------- .../ionia/cards/ionia_more_options_page.dart | 91 ---- .../cards/ionia_payment_status_page.dart | 222 -------- lib/src/screens/ionia/ionia.dart | 9 - lib/src/screens/ionia/widgets/card_item.dart | 144 ------ .../ionia/widgets/ionia_filter_modal.dart | 132 ----- .../send/widgets/confirm_sending_alert.dart | 97 +++- lib/src/widgets/number_text_fild_widget.dart | 145 ++++++ .../cake_pay/cake_pay_account_view_model.dart | 20 + .../cake_pay/cake_pay_auth_view_model.dart | 51 ++ .../cake_pay_buy_card_view_model.dart | 48 ++ .../cake_pay_cards_list_view_model.dart | 221 ++++++++ .../cake_pay_purchase_view_model.dart | 162 ++++++ .../dashboard/cake_features_view_model.dart | 8 +- .../dashboard/dropdown_filter_item.dart | 19 + .../dropdown_filter_item_widget.dart | 68 +++ .../ionia/ionia_account_view_model.dart | 50 -- .../ionia/ionia_auth_view_model.dart | 69 --- .../ionia/ionia_buy_card_view_model.dart | 30 -- .../ionia/ionia_custom_redeem_view_model.dart | 51 -- .../ionia/ionia_custom_tip_view_model.dart | 34 -- .../ionia_gift_card_details_view_model.dart | 54 -- .../ionia_gift_cards_list_view_model.dart | 139 ----- .../ionia_payment_status_view_model.dart | 62 --- .../ionia_purchase_merch_view_model.dart | 104 ---- res/values/strings_ar.arb | 15 +- res/values/strings_bg.arb | 17 +- res/values/strings_cs.arb | 15 +- res/values/strings_de.arb | 17 +- res/values/strings_en.arb | 15 +- res/values/strings_es.arb | 15 +- res/values/strings_fr.arb | 17 +- res/values/strings_ha.arb | 15 +- res/values/strings_hi.arb | 15 +- res/values/strings_hr.arb | 17 +- res/values/strings_id.arb | 17 +- res/values/strings_it.arb | 15 +- res/values/strings_ja.arb | 15 +- res/values/strings_ko.arb | 15 +- res/values/strings_my.arb | 15 +- res/values/strings_nl.arb | 15 +- res/values/strings_pl.arb | 15 +- res/values/strings_pt.arb | 17 +- res/values/strings_ru.arb | 15 +- res/values/strings_th.arb | 15 +- res/values/strings_tl.arb | 15 +- res/values/strings_tr.arb | 17 +- res/values/strings_uk.arb | 15 +- res/values/strings_ur.arb | 15 +- res/values/strings_yo.arb | 17 +- res/values/strings_zh.arb | 15 +- scripts/android/inject_app_details.sh | 2 +- 108 files changed, 3500 insertions(+), 5267 deletions(-) create mode 100644 lib/cake_pay/cake_pay_api.dart create mode 100644 lib/cake_pay/cake_pay_card.dart create mode 100644 lib/cake_pay/cake_pay_order.dart create mode 100644 lib/cake_pay/cake_pay_payment_credantials.dart create mode 100644 lib/cake_pay/cake_pay_service.dart create mode 100644 lib/cake_pay/cake_pay_states.dart create mode 100644 lib/cake_pay/cake_pay_user_credentials.dart create mode 100644 lib/cake_pay/cake_pay_vendor.dart delete mode 100644 lib/ionia/ionia_any_pay_payment_info.dart delete mode 100644 lib/ionia/ionia_anypay.dart delete mode 100644 lib/ionia/ionia_api.dart delete mode 100644 lib/ionia/ionia_category.dart delete mode 100644 lib/ionia/ionia_create_state.dart delete mode 100644 lib/ionia/ionia_gift_card.dart delete mode 100644 lib/ionia/ionia_gift_card_instruction.dart delete mode 100644 lib/ionia/ionia_merchant.dart delete mode 100644 lib/ionia/ionia_order.dart delete mode 100644 lib/ionia/ionia_service.dart delete mode 100644 lib/ionia/ionia_tip.dart delete mode 100644 lib/ionia/ionia_token_data.dart delete mode 100644 lib/ionia/ionia_user_credentials.dart delete mode 100644 lib/ionia/ionia_virtual_card.dart create mode 100644 lib/src/screens/cake_pay/auth/cake_pay_account_page.dart rename lib/src/screens/{ionia/auth/ionia_verify_otp_page.dart => cake_pay/auth/cake_pay_verify_otp_page.dart} (81%) rename lib/src/screens/{ionia/auth/ionia_login_page.dart => cake_pay/auth/cake_pay_welcome_page.dart} (62%) create mode 100644 lib/src/screens/cake_pay/cake_pay.dart create mode 100644 lib/src/screens/cake_pay/cards/cake_pay_buy_card_page.dart rename lib/src/screens/{ionia/cards/ionia_manage_cards_page.dart => cake_pay/cards/cake_pay_cards_page.dart} (50%) create mode 100644 lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart rename lib/src/screens/{ionia/widgets/ionia_alert_model.dart => cake_pay/widgets/cake_pay_alert_modal.dart} (97%) rename lib/src/screens/{ionia/widgets/ionia_tile.dart => cake_pay/widgets/cake_pay_tile.dart} (94%) create mode 100644 lib/src/screens/cake_pay/widgets/card_item.dart rename lib/src/screens/{ionia => cake_pay}/widgets/card_menu.dart (100%) create mode 100644 lib/src/screens/cake_pay/widgets/image_placeholder.dart create mode 100644 lib/src/screens/cake_pay/widgets/link_extractor.dart rename lib/src/screens/{ionia => cake_pay}/widgets/rounded_checkbox.dart (100%) rename lib/src/screens/{ionia => cake_pay}/widgets/text_icon_button.dart (100%) delete mode 100644 lib/src/screens/ionia/auth/ionia_create_account_page.dart delete mode 100644 lib/src/screens/ionia/auth/ionia_welcome_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_account_cards_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_account_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_buy_gift_card.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_custom_tip_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_debit_card_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_more_options_page.dart delete mode 100644 lib/src/screens/ionia/cards/ionia_payment_status_page.dart delete mode 100644 lib/src/screens/ionia/ionia.dart delete mode 100644 lib/src/screens/ionia/widgets/card_item.dart delete mode 100644 lib/src/screens/ionia/widgets/ionia_filter_modal.dart create mode 100644 lib/src/widgets/number_text_fild_widget.dart create mode 100644 lib/view_model/cake_pay/cake_pay_account_view_model.dart create mode 100644 lib/view_model/cake_pay/cake_pay_auth_view_model.dart create mode 100644 lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart create mode 100644 lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart create mode 100644 lib/view_model/cake_pay/cake_pay_purchase_view_model.dart create mode 100644 lib/view_model/dashboard/dropdown_filter_item.dart create mode 100644 lib/view_model/dashboard/dropdown_filter_item_widget.dart delete mode 100644 lib/view_model/ionia/ionia_account_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_auth_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_buy_card_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_custom_redeem_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_custom_tip_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_gift_card_details_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_gift_cards_list_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_payment_status_view_model.dart delete mode 100644 lib/view_model/ionia/ionia_purchase_merch_view_model.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index b4ddcfefe..841ea570d 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -151,6 +151,10 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const testCakePayApiKey = '${{ secrets.TEST_CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const cakePayApiKey = '${{ secrets.CAKE_PAY_API_KEY }}';" >> lib/.secrets.g.dart + echo "const authorization = '${{ secrets.CAKE_PAY_AUTHORIZATION }}';" >> lib/.secrets.g.dart + echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart diff --git a/lib/cake_pay/cake_pay_api.dart b/lib/cake_pay/cake_pay_api.dart new file mode 100644 index 000000000..f403ebc63 --- /dev/null +++ b/lib/cake_pay/cake_pay_api.dart @@ -0,0 +1,245 @@ +import 'dart:convert'; + +import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_user_credentials.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:http/http.dart' as http; + +class CakePayApi { + static const testBaseUri = false; + + static const baseTestCakePayUri = 'test.cakepay.com'; + static const baseProdCakePayUri = 'buy.cakepay.com'; + + static const baseCakePayUri = testBaseUri ? baseTestCakePayUri : baseProdCakePayUri; + + static const vendorsPath = '/api/vendors'; + static const countriesPath = '/api/countries'; + static const authPath = '/api/auth'; + static final verifyEmailPath = '/api/verify'; + static final logoutPath = '/api/logout'; + static final createOrderPath = '/api/order'; + static final simulatePaymentPath = '/api/simulate_payment'; + + /// AuthenticateUser + Future authenticateUser({required String email, required String apiKey}) async { + try { + final uri = Uri.https(baseCakePayUri, authPath); + final headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Api-Key $apiKey', + }; + final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final bodyJson = json.decode(response.body) as Map; + + if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) { + return bodyJson['user']['email'] as String; + } + + throw Exception('Failed to authenticate user with error: $bodyJson'); + } catch (e) { + throw Exception('Failed to authenticate user with error: $e'); + } + } + + /// Verify email + Future verifyEmail({ + required String email, + required String code, + required String apiKey, + }) async { + final uri = Uri.https(baseCakePayUri, verifyEmailPath); + final headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Api-Key $apiKey', + }; + final query = {'email': email, 'otp': code}; + + final response = await http.post(uri, headers: headers, body: json.encode(query)); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final bodyJson = json.decode(response.body) as Map; + + if (bodyJson.containsKey('error')) { + throw Exception(bodyJson['error'] as String); + } + + if (bodyJson.containsKey('token')) { + final token = bodyJson['token'] as String; + final userEmail = bodyJson['user']['email'] as String; + return CakePayUserCredentials(userEmail, token); + } else { + throw Exception('E-mail verification failed.'); + } + } + + /// createOrder + Future createOrder({ + required String apiKey, + required int cardId, + required String price, + required int quantity, + required String userEmail, + required String token, + }) async { + final uri = Uri.https(baseCakePayUri, createOrderPath); + final headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Api-Key $apiKey', + }; + final query = { + 'card_id': cardId, + 'price': price, + 'quantity': quantity, + 'user_email': userEmail, + 'token': token, + 'send_email': true + }; + + try { + final response = await http.post(uri, headers: headers, body: json.encode(query)); + + if (response.statusCode != 201) { + final responseBody = json.decode(response.body); + if (responseBody is List) { + throw '${responseBody[0]}'; + } else { + throw Exception('Unexpected error: $responseBody'); + } + } + + final bodyJson = json.decode(response.body) as Map; + return CakePayOrder.fromMap(bodyJson); + } catch (e) { + throw Exception('${e}'); + } + } + + ///Simulate Payment + Future simulatePayment( + {required String CSRFToken, required String authorization, required String orderId}) async { + final uri = Uri.https(baseCakePayUri, simulatePaymentPath + '/$orderId'); + + final headers = { + 'accept': 'application/json', + 'authorization': authorization, + 'X-CSRFToken': CSRFToken, + }; + + final response = await http.get(uri, headers: headers); + + print('Response: ${response.statusCode}'); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final bodyJson = json.decode(response.body) as Map; + + throw Exception('You just bot a gift card with id: ${bodyJson['order_id']}'); + } + + /// Logout + Future logoutUser({required String email, required String apiKey}) async { + final uri = Uri.https(baseCakePayUri, logoutPath); + final headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Api-Key $apiKey', + }; + + try { + final response = await http.post(uri, headers: headers, body: json.encode({'email': email})); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + } catch (e) { + print('Caught exception: $e'); + } + } + + /// Get Countries + Future> getCountries( + {required String CSRFToken, required String authorization}) async { + final uri = Uri.https(baseCakePayUri, countriesPath); + + final headers = { + 'accept': 'application/json', + 'authorization': authorization, + 'X-CSRFToken': CSRFToken, + }; + + final response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final bodyJson = json.decode(response.body) as List; + + return bodyJson.map((country) => country['name'] as String).toList(); + } + + /// Get Vendors + Future> getVendors({ + required String CSRFToken, + required String authorization, + int? page, + String? country, + String? countryCode, + String? search, + List? vendorIds, + bool? giftCards, + bool? prepaidCards, + bool? onDemand, + bool? custom, + }) async { + var queryParams = { + 'page': page?.toString(), + 'country': country, + 'country_code': countryCode, + 'search': search, + 'vendor_ids': vendorIds?.join(','), + 'gift_cards': giftCards?.toString(), + 'prepaid_cards': prepaidCards?.toString(), + 'on_demand': onDemand?.toString(), + 'custom': custom?.toString(), + }; + + final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams); + + var headers = { + 'accept': 'application/json; charset=UTF-8', + 'authorization': authorization, + 'X-CSRFToken': CSRFToken, + }; + + var response = await http.get(uri, headers: headers); + + if (response.statusCode != 200) { + throw Exception(response.body); + } + + final bodyJson = json.decode(response.body); + + if (bodyJson is List && bodyJson.isEmpty) { + return []; + } + + return (bodyJson['results'] as List) + .map((e) => CakePayVendor.fromJson(e as Map)) + .toList(); + } +} diff --git a/lib/cake_pay/cake_pay_card.dart b/lib/cake_pay/cake_pay_card.dart new file mode 100644 index 000000000..26fa0c50b --- /dev/null +++ b/lib/cake_pay/cake_pay_card.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; + +import 'package:cake_wallet/entities/fiat_currency.dart'; + +class CakePayCard { + final int id; + final String name; + final String? description; + final String? termsAndConditions; + final String? howToUse; + final String? expiryAndValidity; + final String? cardImageUrl; + final String? country; + final FiatCurrency fiatCurrency; + final List denominationsUsd; + final List denominations; + final String? minValueUsd; + final String? maxValueUsd; + final String? minValue; + final String? maxValue; + + CakePayCard({ + required this.id, + required this.name, + this.description, + this.termsAndConditions, + this.howToUse, + this.expiryAndValidity, + this.cardImageUrl, + this.country, + required this.fiatCurrency, + required this.denominationsUsd, + required this.denominations, + this.minValueUsd, + this.maxValueUsd, + this.minValue, + this.maxValue, + }); + + factory CakePayCard.fromJson(Map json) { + final name = stripHtmlIfNeeded(json['name'] as String? ?? ''); + final decodedName = fixEncoding(name); + + final description = stripHtmlIfNeeded(json['description'] as String? ?? ''); + final decodedDescription = fixEncoding(description); + + final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? ''); + final decodedTermsAndConditions = fixEncoding(termsAndConditions); + + final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? ''); + final decodedHowToUse = fixEncoding(howToUse); + + final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? ''); + + final List denominationsUsd = + (json['denominations_usd'] as List?)?.map((e) => e.toString()).toList() ?? []; + final List denominations = + (json['denominations'] as List?)?.map((e) => e.toString()).toList() ?? []; + + return CakePayCard( + id: json['id'] as int? ?? 0, + name: decodedName, + description: decodedDescription, + termsAndConditions: decodedTermsAndConditions, + howToUse: decodedHowToUse, + expiryAndValidity: json['expiry_and_validity'] as String?, + cardImageUrl: json['card_image_url'] as String?, + country: json['country'] as String?, + fiatCurrency: fiatCurrency, + denominationsUsd: denominationsUsd, + denominations: denominations, + minValueUsd: json['min_value_usd'] as String?, + maxValueUsd: json['max_value_usd'] as String?, + minValue: json['min_value'] as String?, + maxValue: json['max_value'] as String?, + ); + } + + static String stripHtmlIfNeeded(String text) { + return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); + } + + static String fixEncoding(String text) { + final bytes = latin1.encode(text); + return utf8.decode(bytes, allowMalformed: true); + } +} diff --git a/lib/cake_pay/cake_pay_order.dart b/lib/cake_pay/cake_pay_order.dart new file mode 100644 index 000000000..8cb6e784a --- /dev/null +++ b/lib/cake_pay/cake_pay_order.dart @@ -0,0 +1,131 @@ + +class CakePayOrder { + final String orderId; + final List cards; + final String? externalId; + final double amountUsd; + final String status; + final String? vouchers; + final PaymentData paymentData; + + CakePayOrder({ + required this.orderId, + required this.cards, + required this.externalId, + required this.amountUsd, + required this.status, + required this.vouchers, + required this.paymentData, + }); + + factory CakePayOrder.fromMap(Map map) { + return CakePayOrder( + orderId: map['order_id'] as String, + cards: (map['cards'] as List) + .map((x) => OrderCard.fromMap(x as Map)) + .toList(), + externalId: map['external_id'] as String?, + amountUsd: map['amount_usd'] as double, + status: map['status'] as String, + vouchers: map['vouchers'] as String?, + paymentData: PaymentData.fromMap(map['payment_data'] as Map)); + } +} + +class OrderCard { + final int cardId; + final int? externalId; + final String price; + final int quantity; + final String currencyCode; + + OrderCard({ + required this.cardId, + required this.externalId, + required this.price, + required this.quantity, + required this.currencyCode, + }); + + factory OrderCard.fromMap(Map map) { + return OrderCard( + cardId: map['card_id'] as int, + externalId: map['external_id'] as int?, + price: map['price'] as String, + quantity: map['quantity'] as int, + currencyCode: map['currency_code'] as String, + ); + } +} + +class PaymentData { + final CryptoPaymentData btc; + final CryptoPaymentData xmr; + final DateTime invoiceTime; + final DateTime expirationTime; + final int? commission; + + PaymentData({ + required this.btc, + required this.xmr, + required this.invoiceTime, + required this.expirationTime, + required this.commission, + }); + + factory PaymentData.fromMap(Map map) { + return PaymentData( + btc: CryptoPaymentData.fromMap(map['BTC'] as Map), + xmr: CryptoPaymentData.fromMap(map['XMR'] as Map), + invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int), + expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int), + commission: map['commission'] as int?, + ); + } +} + +class CryptoPaymentData { + final String price; + final PaymentUrl? paymentUrls; + final String address; + + CryptoPaymentData({ + required this.price, + this.paymentUrls, + required this.address, + }); + + factory CryptoPaymentData.fromMap(Map map) { + return CryptoPaymentData( + price: map['price'] as String, + paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map?), + address: map['address'] as String, + ); + } +} + +class PaymentUrl { + final String? bip21; + final String? bip72; + final String? bip72b; + final String? bip73; + final String? bolt11; + + PaymentUrl({ + this.bip21, + this.bip72, + this.bip72b, + this.bip73, + this.bolt11, + }); + + factory PaymentUrl.fromMap(Map? map) { + return PaymentUrl( + bip21: map?['BIP21'] as String?, + bip72: map?['BIP72'] as String?, + bip72b: map?['BIP72b'] as String?, + bip73: map?['BIP73'] as String?, + bolt11: map?['BOLT11'] as String?, + ); + } +} diff --git a/lib/cake_pay/cake_pay_payment_credantials.dart b/lib/cake_pay/cake_pay_payment_credantials.dart new file mode 100644 index 000000000..12c4ce6c2 --- /dev/null +++ b/lib/cake_pay/cake_pay_payment_credantials.dart @@ -0,0 +1,15 @@ +class PaymentCredential { + final double amount; + final int quantity; + final double totalAmount; + final String? userName; + final String fiatCurrency; + + PaymentCredential({ + required this.amount, + required this.quantity, + required this.totalAmount, + required this.userName, + required this.fiatCurrency, + }); +} \ No newline at end of file diff --git a/lib/cake_pay/cake_pay_service.dart b/lib/cake_pay/cake_pay_service.dart new file mode 100644 index 000000000..be8e1d189 --- /dev/null +++ b/lib/cake_pay/cake_pay_service.dart @@ -0,0 +1,107 @@ +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/cake_pay/cake_pay_api.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; + +class CakePayService { + CakePayService(this.secureStorage, this.cakePayApi); + + static const cakePayEmailStorageKey = 'cake_pay_email'; + static const cakePayUsernameStorageKey = 'cake_pay_username'; + static const cakePayUserTokenKey = 'cake_pay_user_token'; + + static String get testCakePayApiKey => secrets.testCakePayApiKey; + + static String get cakePayApiKey => secrets.cakePayApiKey; + + static String get CSRFToken => secrets.CSRFToken; + + static String get authorization => secrets.authorization; + + final SecureStorage secureStorage; + final CakePayApi cakePayApi; + + /// Get Available Countries + Future> getCountries() async => + await cakePayApi.getCountries(CSRFToken: CSRFToken, authorization: authorization); + + /// Get Vendors + Future> getVendors({ + int? page, + String? country, + String? countryCode, + String? search, + List? vendorIds, + bool? giftCards, + bool? prepaidCards, + bool? onDemand, + bool? custom, + }) async { + final result = await cakePayApi.getVendors( + CSRFToken: CSRFToken, + authorization: authorization, + page: page, + country: country, + countryCode: countryCode, + search: search, + vendorIds: vendorIds, + giftCards: giftCards, + prepaidCards: prepaidCards, + onDemand: onDemand, + custom: custom); + return result; + } + + /// LogIn + Future logIn(String email) async { + final userName = await cakePayApi.authenticateUser(email: email, apiKey: cakePayApiKey); + await secureStorage.write(key: cakePayEmailStorageKey, value: userName); + await secureStorage.write(key: cakePayUsernameStorageKey, value: userName); + } + + /// Verify email + Future verifyEmail(String code) async { + final email = (await secureStorage.read(key: cakePayEmailStorageKey))!; + final credentials = + await cakePayApi.verifyEmail(email: email, code: code, apiKey: cakePayApiKey); + await secureStorage.write(key: cakePayUserTokenKey, value: credentials.token); + await secureStorage.write(key: cakePayUsernameStorageKey, value: credentials.username); + } + + Future getUserEmail() async { + return (await secureStorage.read(key: cakePayEmailStorageKey)); + } + + /// Check is user logged + Future isLogged() async { + final username = await secureStorage.read(key: cakePayUsernameStorageKey) ?? ''; + final password = await secureStorage.read(key: cakePayUserTokenKey) ?? ''; + return username.isNotEmpty && password.isNotEmpty; + } + + /// Logout + Future logout(String email) async { + await secureStorage.delete(key: cakePayUsernameStorageKey); + await secureStorage.delete(key: cakePayUserTokenKey); + await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey); + } + + /// Purchase Gift Card + Future createOrder( + {required int cardId, required String price, required int quantity}) async { + final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!; + final token = (await secureStorage.read(key: cakePayUserTokenKey))!; + return await cakePayApi.createOrder( + apiKey: cakePayApiKey, + cardId: cardId, + price: price, + quantity: quantity, + token: token, + userEmail: userEmail); + } + + ///Simulate Purchase Gift Card + Future simulatePayment({required String orderId}) async => await cakePayApi.simulatePayment( + CSRFToken: CSRFToken, authorization: authorization, orderId: orderId); +} diff --git a/lib/cake_pay/cake_pay_states.dart b/lib/cake_pay/cake_pay_states.dart new file mode 100644 index 000000000..2c49353c1 --- /dev/null +++ b/lib/cake_pay/cake_pay_states.dart @@ -0,0 +1,67 @@ +import 'cake_pay_card.dart'; + +abstract class CakePayUserVerificationState {} + +class CakePayUserVerificationStateInitial extends CakePayUserVerificationState {} + +class CakePayUserVerificationStateSuccess extends CakePayUserVerificationState {} + +class CakePayUserVerificationStatePending extends CakePayUserVerificationState {} + +class CakePayUserVerificationStateLoading extends CakePayUserVerificationState {} + +class CakePayUserVerificationStateFailure extends CakePayUserVerificationState { + CakePayUserVerificationStateFailure({required this.error}); + + final String error; +} + +abstract class CakePayOtpState {} + +class CakePayOtpValidating extends CakePayOtpState {} + +class CakePayOtpSuccess extends CakePayOtpState {} + +class CakePayOtpSendDisabled extends CakePayOtpState {} + +class CakePayOtpSendEnabled extends CakePayOtpState {} + +class CakePayOtpFailure extends CakePayOtpState { + CakePayOtpFailure({required this.error}); + + final String error; +} + +class CakePayCreateCardState {} + +class CakePayCreateCardStateSuccess extends CakePayCreateCardState {} + +class CakePayCreateCardStateLoading extends CakePayCreateCardState {} + +class CakePayCreateCardStateFailure extends CakePayCreateCardState { + CakePayCreateCardStateFailure({required this.error}); + + final String error; +} + +class CakePayCardsState {} + +class CakePayCardsStateNoCards extends CakePayCardsState {} + +class CakePayCardsStateFetching extends CakePayCardsState {} + +class CakePayCardsStateFailure extends CakePayCardsState {} + +class CakePayCardsStateSuccess extends CakePayCardsState { + CakePayCardsStateSuccess({required this.card}); + + final CakePayCard card; +} + +abstract class CakePayVendorState {} + +class InitialCakePayVendorLoadingState extends CakePayVendorState {} + +class CakePayVendorLoadingState extends CakePayVendorState {} + +class CakePayVendorLoadedState extends CakePayVendorState {} diff --git a/lib/cake_pay/cake_pay_user_credentials.dart b/lib/cake_pay/cake_pay_user_credentials.dart new file mode 100644 index 000000000..5899b31ad --- /dev/null +++ b/lib/cake_pay/cake_pay_user_credentials.dart @@ -0,0 +1,6 @@ +class CakePayUserCredentials { + const CakePayUserCredentials(this.username, this.token); + + final String username; + final String token; +} \ No newline at end of file diff --git a/lib/cake_pay/cake_pay_vendor.dart b/lib/cake_pay/cake_pay_vendor.dart new file mode 100644 index 000000000..c947fa882 --- /dev/null +++ b/lib/cake_pay/cake_pay_vendor.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'cake_pay_card.dart'; + +class CakePayVendor { + final int id; + final String name; + final bool unavailable; + final String? cakeWarnings; + final List countries; + final CakePayCard? card; + + CakePayVendor({ + required this.id, + required this.name, + required this.unavailable, + this.cakeWarnings, + required this.countries, + this.card, + }); + + factory CakePayVendor.fromJson(Map json) { + final name = stripHtmlIfNeeded(json['name'] as String); + final decodedName = fixEncoding(name); + + var cardsJson = json['cards'] as List?; + CakePayCard? firstCard; + + if (cardsJson != null && cardsJson.isNotEmpty) { + firstCard = CakePayCard.fromJson(cardsJson.first as Map); + } + + return CakePayVendor( + id: json['id'] as int, + name: decodedName, + unavailable: json['unavailable'] as bool? ?? false, + cakeWarnings: json['cake_warnings'] as String?, + countries: List.from(json['countries'] as List? ?? []), + card: firstCard, + ); + } + + static String stripHtmlIfNeeded(String text) { + return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' '); + } + + static String fixEncoding(String text) { + final bytes = latin1.encode(text); + return utf8.decode(bytes, allowMalformed: true); + } +} diff --git a/lib/di.dart b/lib/di.dart index 31655b402..2c09d8cbc 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -2,7 +2,6 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -33,16 +32,10 @@ import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/haven/haven.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; -import 'package:cake_wallet/ionia/ionia_anypay.dart'; -import 'package:cake_wallet/ionia/ionia_api.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/ionia/ionia_tip.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -72,14 +65,6 @@ import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/faq/faq_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; -import 'package:cake_wallet/src/screens/ionia/ionia.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; @@ -124,16 +109,57 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart'; import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart'; +import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; +import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; +import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; +import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; +import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_api.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; +import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; +import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; +import 'package:cake_wallet/view_model/seed_type_view_model.dart'; +import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; +import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; +import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; +import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; +import 'package:cw_core/nano_account.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/node.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; -import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart'; -import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -148,14 +174,7 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/store/templates/send_template_store.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; -import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; @@ -165,46 +184,22 @@ import 'package:cake_wallet/view_model/contact_list/contact_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; -import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/edit_backup_password_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; -import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart'; import 'package:cake_wallet/view_model/order_details_view_model.dart'; import 'package:cake_wallet/view_model/rescan_view_model.dart'; -import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore_from_backup_view_model.dart'; -import 'package:cake_wallet/view_model/seed_type_view_model.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; -import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; -import 'package:cake_wallet/view_model/settings/display_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/silent_payments_settings_view_model.dart'; -import 'package:cake_wallet/view_model/settings/trocador_providers_view_model.dart'; import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; @@ -213,25 +208,16 @@ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_ import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_keys_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; -import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; -import 'package:cake_wallet/view_model/wallet_restore_choose_derivation_view_model.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/nano_account.dart'; -import 'package:cw_core/node.dart'; import 'package:cw_core/receive_page_option.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -239,6 +225,7 @@ import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'cake_pay/cake_pay_payment_credantials.dart'; final getIt = GetIt.instance; @@ -993,6 +980,8 @@ Future setup({ trades: _tradesSource, settingsStore: getIt.get())); + getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); + getIt.registerFactory(() => BackupService(getIt.get(), _walletInfoSource, getIt.get(), getIt.get())); @@ -1088,113 +1077,60 @@ Future setup({ getIt.registerFactoryParam( (QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData)); - getIt.registerFactory(() => IoniaApi()); + getIt.registerFactory(() => CakePayApi()); getIt.registerFactory(() => AnyPayApi()); - getIt.registerFactory( - () => IoniaService(getIt.get(), getIt.get())); + getIt.registerFactory( + () => CakePayService(getIt.get(), getIt.get())); - getIt.registerFactory(() => IoniaAnyPay( - getIt.get(), getIt.get(), getIt.get().wallet!)); + getIt.registerFactory(() => CakePayCardsListViewModel(cakePayService: getIt.get())); - getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); + getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get())); - getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get())); - - getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); - - getIt.registerFactoryParam( - (double amount, merchant) { - return IoniaMerchPurchaseViewModel( - ioniaAnyPayService: getIt.get(), - amount: amount, - ioniaMerchant: merchant, + getIt.registerFactoryParam( + (PaymentCredential paymentCredential, CakePayCard card) { + return CakePayPurchaseViewModel( + cakePayService: getIt.get(), + paymentCredential: paymentCredential, + card: card, sendViewModel: getIt.get()); }); - getIt.registerFactoryParam( - (IoniaMerchant merchant, _) { - return IoniaBuyCardViewModel(ioniaMerchant: merchant); + getIt.registerFactoryParam( + (CakePayVendor vendor, _) { + return CakePayBuyCardViewModel(vendor: vendor); }); - getIt.registerFactory(() => IoniaAccountViewModel(ioniaService: getIt.get())); + getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get())); - getIt.registerFactory(() => IoniaCreateAccountPage(getIt.get())); + getIt.registerFactory(() => CakePayWelcomePage(getIt.get())); - getIt.registerFactory(() => IoniaLoginPage(getIt.get())); - - getIt.registerFactoryParam, void>((List args, _) { + getIt.registerFactoryParam, void>((List args, _) { final email = args.first as String; final isSignIn = args[1] as bool; - return IoniaVerifyIoniaOtp(getIt.get(), email, isSignIn); + return CakePayVerifyOtpPage(getIt.get(), email, isSignIn); }); - getIt.registerFactory(() => IoniaWelcomePage()); + getIt.registerFactoryParam, void>((List args, _) { + final vendor = args.first as CakePayVendor; - getIt.registerFactoryParam, void>((List args, _) { - final merchant = args.first as IoniaMerchant; - - return IoniaBuyGiftCardPage(getIt.get(param1: merchant)); + return CakePayBuyCardPage(getIt.get(param1: vendor), + getIt.get()); }); - getIt.registerFactoryParam, void>( + getIt.registerFactoryParam, void>( (List args, _) { - final amount = args.first as double; - final merchant = args.last as IoniaMerchant; - return IoniaBuyGiftCardDetailPage( - getIt.get(param1: amount, param2: merchant)); + final paymentCredential = args.first as PaymentCredential; + final card = args[1] as CakePayCard; + return CakePayBuyCardDetailPage( + getIt.get(param1: paymentCredential, param2: card)); }); - getIt.registerFactoryParam( - (IoniaGiftCard giftCard, _) { - return IoniaGiftCardDetailsViewModel( - ioniaService: getIt.get(), giftCard: giftCard); - }); + getIt.registerFactory(() => CakePayCardsPage(getIt.get())); - getIt.registerFactoryParam, void>((List args, _) { - final amount = args[0] as double; - final merchant = args[1] as IoniaMerchant; - final tip = args[2] as IoniaTip; - - return IoniaCustomTipViewModel(amount: amount, tip: tip, ioniaMerchant: merchant); - }); - - getIt.registerFactoryParam( - (IoniaGiftCard giftCard, _) { - return IoniaGiftCardDetailPage(getIt.get(param1: giftCard)); - }); - - getIt.registerFactoryParam, void>((List args, _) { - final giftCard = args.first as IoniaGiftCard; - - return IoniaMoreOptionsPage(giftCard); - }); - - getIt.registerFactoryParam( - (IoniaGiftCard giftCard, _) => - IoniaCustomRedeemViewModel(giftCard: giftCard, ioniaService: getIt.get())); - - getIt.registerFactoryParam, void>((List args, _) { - final giftCard = args.first as IoniaGiftCard; - - return IoniaCustomRedeemPage(getIt.get(param1: giftCard)); - }); - - getIt.registerFactoryParam, void>((List args, _) { - return IoniaCustomTipPage(getIt.get(param1: args)); - }); - - getIt.registerFactory(() => IoniaManageCardsPage(getIt.get())); - - getIt.registerFactory(() => IoniaDebitCardPage(getIt.get())); - - getIt.registerFactory(() => IoniaActivateDebitCardPage(getIt.get())); - - getIt.registerFactory(() => IoniaAccountPage(getIt.get())); - - getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get())); + getIt.registerFactory(() => CakePayAccountPage(getIt.get())); getIt.registerFactoryParam( (TransactionInfo transactionInfo, _) => RBFDetailsPage( @@ -1225,18 +1161,6 @@ Future setup({ (AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage( anonpayDetailsViewModel: getIt.get(param1: anonpayInvoiceInfo))); - getIt.registerFactoryParam( - (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) => - IoniaPaymentStatusViewModel(getIt.get(), - paymentInfo: paymentInfo, committedInfo: committedInfo)); - - getIt.registerFactoryParam( - (IoniaAnyPayPaymentInfo paymentInfo, AnyPayPaymentCommittedInfo committedInfo) => - IoniaPaymentStatusPage( - getIt.get(param1: paymentInfo, param2: committedInfo))); - getIt.registerFactoryParam((balanceViewModel, _) => HomeSettingsPage(getIt.get(param1: balanceViewModel))); diff --git a/lib/ionia/ionia_any_pay_payment_info.dart b/lib/ionia/ionia_any_pay_payment_info.dart deleted file mode 100644 index 6146a46fe..000000000 --- a/lib/ionia/ionia_any_pay_payment_info.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:cake_wallet/anypay/any_pay_payment.dart'; -import 'package:cake_wallet/ionia/ionia_order.dart'; - -class IoniaAnyPayPaymentInfo { - const IoniaAnyPayPaymentInfo(this.ioniaOrder, this.anyPayPayment); - - final IoniaOrder ioniaOrder; - final AnyPayPayment anyPayPayment; -} diff --git a/lib/ionia/ionia_anypay.dart b/lib/ionia/ionia_anypay.dart deleted file mode 100644 index f4053094b..000000000 --- a/lib/ionia/ionia_anypay.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:cw_core/output_info.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cake_wallet/anypay/any_pay_payment.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_instruction.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/anypay/anypay_api.dart'; -import 'package:cake_wallet/anypay/any_pay_chain.dart'; -import 'package:cake_wallet/anypay/any_pay_trasnaction.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; - -class IoniaAnyPay { - IoniaAnyPay(this.ioniaService, this.anyPayApi, this.wallet); - - final IoniaService ioniaService; - final AnyPayApi anyPayApi; - final WalletBase wallet; - - Future purchase({ - required String merchId, - required double amount}) async { - final invoice = await ioniaService.purchaseGiftCard( - merchId: merchId, - amount: amount, - currency: wallet.currency.title.toUpperCase()); - final anypayPayment = await anyPayApi.paymentRequest(invoice.uri); - return IoniaAnyPayPaymentInfo(invoice, anypayPayment); - } - - Future commitInvoice(AnyPayPayment payment) async { - final transactionCredentials = payment.instructions - .where((instruction) => instruction.type == AnyPayPaymentInstruction.transactionType) - .map((AnyPayPaymentInstruction instruction) { - switch(payment.chain.toUpperCase()) { - case AnyPayChain.xmr: - return monero!.createMoneroTransactionCreationCredentialsRaw( - outputs: instruction.outputs.map((out) => - OutputInfo( - isParsedAddress: false, - address: out.address, - cryptoAmount: moneroAmountToString(amount: out.amount), - formattedCryptoAmount: out.amount, - sendAll: false)).toList(), - priority: MoneroTransactionPriority.medium); // FIXME: HARDCODED PRIORITY - case AnyPayChain.btc: - return bitcoin!.createBitcoinTransactionCredentialsRaw( - instruction.outputs.map((out) => - OutputInfo( - isParsedAddress: false, - address: out.address, - formattedCryptoAmount: out.amount, - sendAll: false)).toList(), - feeRate: instruction.requiredFeeRate); - case AnyPayChain.ltc: - return bitcoin!.createBitcoinTransactionCredentialsRaw( - instruction.outputs.map((out) => - OutputInfo( - isParsedAddress: false, - address: out.address, - formattedCryptoAmount: out.amount, - sendAll: false)).toList(), - feeRate: instruction.requiredFeeRate); - default: - throw Exception('Incorrect transaction chain: ${payment.chain.toUpperCase()}'); - } - }); - final transactions = (await Future.wait(transactionCredentials - .map((Object credentials) async => await wallet.createTransaction(credentials)))) - .map((PendingTransaction pendingTransaction) { - switch (payment.chain.toUpperCase()){ - case AnyPayChain.xmr: - final ptx = monero!.pendingTransactionInfo(pendingTransaction); - return AnyPayTransaction(ptx['hex'] ?? '', id: ptx['id'] ?? '', key: ptx['key']); - default: - return AnyPayTransaction(pendingTransaction.hex, id: pendingTransaction.id, key: null); - } - }) - .toList(); - - return await anyPayApi.payment( - payment.paymentUrl, - chain: payment.chain, - currency: payment.chain, - transactions: transactions); - } -} \ No newline at end of file diff --git a/lib/ionia/ionia_api.dart b/lib/ionia/ionia_api.dart deleted file mode 100644 index 274e557c7..000000000 --- a/lib/ionia/ionia_api.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'dart:convert'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_order.dart'; -import 'package:http/http.dart'; -import 'package:cake_wallet/ionia/ionia_user_credentials.dart'; -import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; -import 'package:cake_wallet/ionia/ionia_category.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; - -class IoniaApi { - static const baseUri = 'api.ionia.io'; - static const pathPrefix = 'cake'; - static const requestedUUIDHeader = 'requestedUUID'; - static final createUserUri = Uri.https(baseUri, '/$pathPrefix/CreateUser'); - static final verifyEmailUri = Uri.https(baseUri, '/$pathPrefix/VerifyEmail'); - static final signInUri = Uri.https(baseUri, '/$pathPrefix/SignIn'); - static final createCardUri = Uri.https(baseUri, '/$pathPrefix/CreateCard'); - static final getCardsUri = Uri.https(baseUri, '/$pathPrefix/GetCards'); - static final getMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchants'); - static final getMerchantsByFilterUrl = Uri.https(baseUri, '/$pathPrefix/GetMerchantsByFilter'); - static final getPurchaseMerchantsUrl = Uri.https(baseUri, '/$pathPrefix/PurchaseGiftCard'); - static final getCurrentUserGiftCardSummariesUrl = Uri.https(baseUri, '/$pathPrefix/GetCurrentUserGiftCardSummaries'); - static final changeGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/ChargeGiftCard'); - static final getGiftCardUrl = Uri.https(baseUri, '/$pathPrefix/GetGiftCard'); - static final getPaymentStatusUrl = Uri.https(baseUri, '/$pathPrefix/PaymentStatus'); - - // Create user - - Future createUser(String email, {required String clientId}) async { - final headers = {'clientId': clientId}; - final query = {'emailAddress': email}; - final uri = createUserUri.replace(queryParameters: query); - final response = await put(uri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - final data = bodyJson['Data'] as Map; - final isSuccessful = bodyJson['Successful'] as bool; - - if (!isSuccessful) { - throw Exception(data['ErrorMessage'] as String); - } - - return data['username'] as String; - } - - // Verify email - - Future verifyEmail({ - required String email, - required String code, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'EmailAddress': email}; - final query = {'verificationCode': code}; - final uri = verifyEmailUri.replace(queryParameters: query); - final response = await put(uri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - final data = bodyJson['Data'] as Map; - final isSuccessful = bodyJson['Successful'] as bool; - - if (!isSuccessful) { - throw Exception(bodyJson['ErrorMessage'] as String); - } - - final password = data['password'] as String; - final username = data['username'] as String; - return IoniaUserCredentials(username, password); - } - - // Sign In - - Future signIn(String email, {required String clientId}) async { - final headers = {'clientId': clientId}; - final query = {'emailAddress': email}; - final uri = signInUri.replace(queryParameters: query); - final response = await put(uri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - final data = bodyJson['Data'] as Map; - final isSuccessful = bodyJson['Successful'] as bool; - - if (!isSuccessful) { - throw Exception(data['ErrorMessage'] as String); - } - } - - // Get virtual card - - Future getCards({ - required String username, - required String password, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password}; - final response = await post(getCardsUri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - final data = bodyJson['Data'] as Map; - final isSuccessful = bodyJson['Successful'] as bool; - - if (!isSuccessful) { - throw Exception(data['message'] as String); - } - - final virtualCard = data['VirtualCard'] as Map; - return IoniaVirtualCard.fromMap(virtualCard); - } - - // Create virtual card - - Future createCard({ - required String username, - required String password, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password}; - final response = await post(createCardUri, headers: headers); - - if (response.statusCode != 200) { - throw Exception('Unexpected http status: ${response.statusCode}'); - } - - final bodyJson = json.decode(response.body) as Map; - final data = bodyJson['Data'] as Map; - final isSuccessful = bodyJson['Successful'] as bool? ?? false; - - if (!isSuccessful) { - throw Exception(data['message'] as String); - } - - return IoniaVirtualCard.fromMap(data); - } - - // Get Merchants - - Future> getMerchants({ - required String username, - required String password, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password}; - final response = await post(getMerchantsUrl, headers: headers); - - if (response.statusCode != 200) { - return []; - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - return []; - } - - final data = decodedBody['Data'] as List; - final merch = []; - - for (final item in data) { - try { - final element = item as Map; - merch.add(IoniaMerchant.fromJsonMap(element)); - } catch(_) {} - } - - return merch; - } - - // Get Merchants By Filter - - Future> getMerchantsByFilter({ - required String username, - required String password, - required String clientId, - String? search, - List? categories, - int merchantFilterType = 0}) async { - // MerchantFilterType: {All = 0, Nearby = 1, Popular = 2, Online = 3, MyFaves = 4, Search = 5} - - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password, - 'Content-Type': 'application/json'}; - final body = {'MerchantFilterType': merchantFilterType}; - - if (search != null) { - body['SearchCriteria'] = search; - } - - if (categories != null) { - body['Categories'] = categories - .map((e) => e.ids) - .expand((e) => e) - .toList(); - } - - final response = await post(getMerchantsByFilterUrl, headers: headers, body: json.encode(body)); - - if (response.statusCode != 200) { - return []; - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - return []; - } - - final data = decodedBody['Data'] as List; - final merch = []; - - for (final item in data) { - try { - final element = item['Merchant'] as Map; - merch.add(IoniaMerchant.fromJsonMap(element)); - } catch(_) {} - } - - return merch; - } - - // Purchase Gift Card - - Future purchaseGiftCard({ - required String requestedUUID, - required String merchId, - required double amount, - required String currency, - required String username, - required String password, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password, - requestedUUIDHeader: requestedUUID, - 'Content-Type': 'application/json'}; - final body = { - 'Amount': amount, - 'Currency': currency, - 'MerchantId': merchId}; - final response = await post(getPurchaseMerchantsUrl, headers: headers, body: json.encode(body)); - - if (response.statusCode != 200) { - throw Exception('Unexpected response'); - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - throw Exception(decodedBody['ErrorMessage'] as String); - } - - final data = decodedBody['Data'] as Map; - return IoniaOrder.fromMap(data); - } - - // Get Current User Gift Card Summaries - - Future> getCurrentUserGiftCardSummaries({ - required String username, - required String password, - required String clientId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password}; - final response = await post(getCurrentUserGiftCardSummariesUrl, headers: headers); - - if (response.statusCode != 200) { - return []; - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - return []; - } - - final data = decodedBody['Data'] as List; - final cards = []; - - for (final item in data) { - try { - final element = item as Map; - cards.add(IoniaGiftCard.fromJsonMap(element)); - } catch(_) {} - } - - return cards; - } - - // Charge Gift Card - - Future chargeGiftCard({ - required String username, - required String password, - required String clientId, - required int giftCardId, - required double amount}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password, - 'Content-Type': 'application/json'}; - final body = { - 'Id': giftCardId, - 'Amount': amount}; - final response = await post( - changeGiftCardUrl, - headers: headers, - body: json.encode(body)); - - if (response.statusCode != 200) { - throw Exception('Failed to update Gift Card with ID ${giftCardId};Incorrect response status: ${response.statusCode};'); - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - final data = decodedBody['Data'] as Map; - final msg = data['Message'] as String? ?? ''; - - if (msg.isNotEmpty) { - throw Exception(msg); - } - - throw Exception('Failed to update Gift Card with ID ${giftCardId};'); - } - } - - // Get Gift Card - - Future getGiftCard({ - required String username, - required String password, - required String clientId, - required int id}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password, - 'Content-Type': 'application/json'}; - final body = {'Id': id}; - final response = await post( - getGiftCardUrl, - headers: headers, - body: json.encode(body)); - - if (response.statusCode != 200) { - throw Exception('Failed to get Gift Card with ID ${id};Incorrect response status: ${response.statusCode};'); - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - final msg = decodedBody['ErrorMessage'] as String ?? ''; - - if (msg.isNotEmpty) { - throw Exception(msg); - } - - throw Exception('Failed to get Gift Card with ID ${id};'); - } - - final data = decodedBody['Data'] as Map; - return IoniaGiftCard.fromJsonMap(data); - } - - // Payment Status - - Future getPaymentStatus({ - required String username, - required String password, - required String clientId, - required String orderId, - required String paymentId}) async { - final headers = { - 'clientId': clientId, - 'username': username, - 'password': password, - 'Content-Type': 'application/json'}; - final body = { - 'order_id': orderId, - 'paymentId': paymentId}; - final response = await post( - getPaymentStatusUrl, - headers: headers, - body: json.encode(body)); - - if (response.statusCode != 200) { - throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId};Incorrect response status: ${response.statusCode};'); - } - - final decodedBody = json.decode(response.body) as Map; - final isSuccessful = decodedBody['Successful'] as bool? ?? false; - - if (!isSuccessful) { - final msg = decodedBody['ErrorMessage'] as String ?? ''; - - if (msg.isNotEmpty) { - throw Exception(msg); - } - - throw Exception('Failed to get Payment Status for order_id ${orderId} paymentId ${paymentId}'); - } - - final data = decodedBody['Data'] as Map; - return data['gift_card_id'] as int; - } -} \ No newline at end of file diff --git a/lib/ionia/ionia_category.dart b/lib/ionia/ionia_category.dart deleted file mode 100644 index 284b513a7..000000000 --- a/lib/ionia/ionia_category.dart +++ /dev/null @@ -1,22 +0,0 @@ -class IoniaCategory { - const IoniaCategory({ - required this.index, - required this.title, - required this.ids, - required this.iconPath}); - - static const allCategories = [all, apparel, onlineOnly, food, entertainment, delivery, travel]; - static const all = IoniaCategory(index: 0, title: 'All', ids: [], iconPath: 'assets/images/category.png'); - static const apparel = IoniaCategory(index: 1, title: 'Apparel', ids: [1], iconPath: 'assets/images/tshirt.png'); - static const onlineOnly = IoniaCategory(index: 2, title: 'Online Only', ids: [13, 43], iconPath: 'assets/images/global.png'); - static const food = IoniaCategory(index: 3, title: 'Food', ids: [4], iconPath: 'assets/images/food.png'); - static const entertainment = IoniaCategory(index: 4, title: 'Entertainment', ids: [5], iconPath: 'assets/images/gaming.png'); - static const delivery = IoniaCategory(index: 5, title: 'Delivery', ids: [114, 109], iconPath: 'assets/images/delivery.png'); - static const travel = IoniaCategory(index: 6, title: 'Travel', ids: [12], iconPath: 'assets/images/airplane.png'); - - - final int index; - final String title; - final List ids; - final String iconPath; -} diff --git a/lib/ionia/ionia_create_state.dart b/lib/ionia/ionia_create_state.dart deleted file mode 100644 index a4f1f9cad..000000000 --- a/lib/ionia/ionia_create_state.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; -import 'package:flutter/material.dart'; - -abstract class IoniaCreateAccountState {} - -class IoniaInitialCreateState extends IoniaCreateAccountState {} - -class IoniaCreateStateSuccess extends IoniaCreateAccountState {} - -class IoniaCreateStateLoading extends IoniaCreateAccountState {} - -class IoniaCreateStateFailure extends IoniaCreateAccountState { - IoniaCreateStateFailure({required this.error}); - - final String error; -} - -abstract class IoniaOtpState {} - -class IoniaOtpValidating extends IoniaOtpState {} - -class IoniaOtpSuccess extends IoniaOtpState {} - -class IoniaOtpSendDisabled extends IoniaOtpState {} - -class IoniaOtpSendEnabled extends IoniaOtpState {} - -class IoniaOtpFailure extends IoniaOtpState { - IoniaOtpFailure({required this.error}); - - final String error; -} - -class IoniaCreateCardState {} - -class IoniaCreateCardSuccess extends IoniaCreateCardState {} - -class IoniaCreateCardLoading extends IoniaCreateCardState {} - -class IoniaCreateCardFailure extends IoniaCreateCardState { - IoniaCreateCardFailure({required this.error}); - - final String error; -} - -class IoniaFetchCardState {} - -class IoniaNoCardState extends IoniaFetchCardState {} - -class IoniaFetchingCard extends IoniaFetchCardState {} - -class IoniaFetchCardFailure extends IoniaFetchCardState {} - -class IoniaCardSuccess extends IoniaFetchCardState { - IoniaCardSuccess({required this.card}); - - final IoniaVirtualCard card; -} - -abstract class IoniaMerchantState {} - -class InitialIoniaMerchantLoadingState extends IoniaMerchantState {} - -class IoniaLoadingMerchantState extends IoniaMerchantState {} - -class IoniaLoadedMerchantState extends IoniaMerchantState {} - - diff --git a/lib/ionia/ionia_gift_card.dart b/lib/ionia/ionia_gift_card.dart deleted file mode 100644 index 2ba0fd136..000000000 --- a/lib/ionia/ionia_gift_card.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:convert'; -import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart'; -import 'package:flutter/foundation.dart'; - -class IoniaGiftCard { - IoniaGiftCard({ - required this.id, - required this.merchantId, - required this.legalName, - required this.systemName, - required this.barcodeUrl, - required this.cardNumber, - required this.cardPin, - required this.instructions, - required this.tip, - required this.purchaseAmount, - required this.actualAmount, - required this.totalTransactionAmount, - required this.totalDashTransactionAmount, - required this.remainingAmount, - required this.createdDateFormatted, - required this.lastTransactionDateFormatted, - required this.isActive, - required this.isEmpty, - required this.logoUrl}); - - factory IoniaGiftCard.fromJsonMap(Map element) { - return IoniaGiftCard( - id: element['Id'] as int, - merchantId: element['MerchantId'] as int, - legalName: element['LegalName'] as String, - systemName: element['SystemName'] as String, - barcodeUrl: element['BarcodeUrl'] as String, - cardNumber: element['CardNumber'] as String, - cardPin: element['CardPin'] as String, - tip: element['Tip'] as double, - purchaseAmount: element['PurchaseAmount'] as double, - actualAmount: element['ActualAmount'] as double, - totalTransactionAmount: element['TotalTransactionAmount'] as double, - totalDashTransactionAmount: (element['TotalDashTransactionAmount'] as double?) ?? 0.0, - remainingAmount: element['RemainingAmount'] as double, - isActive: element['IsActive'] as bool, - isEmpty: element['IsEmpty'] as bool, - logoUrl: element['LogoUrl'] as String, - createdDateFormatted: element['CreatedDate'] as String, - lastTransactionDateFormatted: element['LastTransactionDate'] as String, - instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String)); - } - - final int id; - final int merchantId; - final String legalName; - final String systemName; - final String barcodeUrl; - final String cardNumber; - final String cardPin; - final List instructions; - final double tip; - final double purchaseAmount; - final double actualAmount; - final double totalTransactionAmount; - final double totalDashTransactionAmount; - double remainingAmount; - final String createdDateFormatted; - final String lastTransactionDateFormatted; - final bool isActive; - final bool isEmpty; - final String logoUrl; - -} \ No newline at end of file diff --git a/lib/ionia/ionia_gift_card_instruction.dart b/lib/ionia/ionia_gift_card_instruction.dart deleted file mode 100644 index fb6751c09..000000000 --- a/lib/ionia/ionia_gift_card_instruction.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:convert'; -import 'package:intl/intl.dart' show toBeginningOfSentenceCase; - -class IoniaGiftCardInstruction { - IoniaGiftCardInstruction(this.header, this.body); - - factory IoniaGiftCardInstruction.fromJsonMap(Map element) { - return IoniaGiftCardInstruction( - toBeginningOfSentenceCase(element['title'] as String? ?? '') ?? '', - element['description'] as String); - } - - static List parseListOfInstructions(String instructionsJSON) { - List instructions = []; - - if (instructionsJSON.isNotEmpty) { - final decodedInstructions = json.decode(instructionsJSON) as List; - instructions = decodedInstructions - .map((dynamic e) =>IoniaGiftCardInstruction.fromJsonMap(e as Map)) - .toList(); - } - - return instructions; - } - - final String header; - final String body; -} \ No newline at end of file diff --git a/lib/ionia/ionia_merchant.dart b/lib/ionia/ionia_merchant.dart deleted file mode 100644 index c355a25c3..000000000 --- a/lib/ionia/ionia_merchant.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_gift_card_instruction.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -class IoniaMerchant { - IoniaMerchant({ - required this.id, - required this.legalName, - required this.systemName, - required this.description, - required this.website, - required this.termsAndConditions, - required this.logoUrl, - required this.cardImageUrl, - required this.cardholderAgreement, - required this.isActive, - required this.isOnline, - required this.isPhysical, - required this.isVariablePurchase, - required this.minimumCardPurchase, - required this.maximumCardPurchase, - required this.acceptsTips, - required this.createdDateFormatted, - required this.modifiedDateFormatted, - required this.usageInstructions, - required this.usageInstructionsBak, - required this.hasBarcode, - required this.instructions, - required this.savingsPercentage}); - - factory IoniaMerchant.fromJsonMap(Map element) { - return IoniaMerchant( - id: element["Id"] as int, - legalName: element["LegalName"] as String, - systemName: element["SystemName"] as String, - description: element["Description"] as String, - website: element["Website"] as String, - termsAndConditions: element["TermsAndConditions"] as String, - logoUrl: element["LogoUrl"] as String, - cardImageUrl: element["CardImageUrl"] as String, - cardholderAgreement: element["CardholderAgreement"] as String, - isActive: element["IsActive"] as bool?, - isOnline: element["IsOnline"] as bool, - isPhysical: element["IsPhysical"] as bool, - isVariablePurchase: element["IsVariablePurchase"] as bool, - minimumCardPurchase: element["MinimumCardPurchase"] as double, - maximumCardPurchase: element["MaximumCardPurchase"] as double, - acceptsTips: element["AcceptsTips"] as bool, - createdDateFormatted: element["CreatedDate"] as String?, - modifiedDateFormatted: element["ModifiedDate"] as String?, - usageInstructions: element["UsageInstructions"] as String?, - usageInstructionsBak: element["UsageInstructionsBak"] as String?, - hasBarcode: element["HasBarcode"] as bool, - instructions: IoniaGiftCardInstruction.parseListOfInstructions(element['PaymentInstructions'] as String), - savingsPercentage: element["SavingsPercentage"] as double); - } - - final int id; - final String legalName; - final String systemName; - final String description; - final String website; - final String termsAndConditions; - final String logoUrl; - final String cardImageUrl; - final String cardholderAgreement; - final bool? isActive; - final bool isOnline; - final bool? isPhysical; - final bool isVariablePurchase; - final double minimumCardPurchase; - final double maximumCardPurchase; - final bool acceptsTips; - final String? createdDateFormatted; - final String? modifiedDateFormatted; - final String? usageInstructions; - final String? usageInstructionsBak; - final bool hasBarcode; - final List instructions; - final double savingsPercentage; - - double get discount => savingsPercentage; - - String get avaibilityStatus { - var status = ''; - - if (isOnline) { - status += S.current.online; - } - - if (isPhysical ?? false) { - if (status.isNotEmpty) { - status = '$status & '; - } - - status = '${status}${S.current.in_store}'; - } - - return status; - } - -} diff --git a/lib/ionia/ionia_order.dart b/lib/ionia/ionia_order.dart deleted file mode 100644 index a9cae07e9..000000000 --- a/lib/ionia/ionia_order.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class IoniaOrder { - IoniaOrder({required this.id, - required this.uri, - required this.currency, - required this.amount, - required this.paymentId}); - factory IoniaOrder.fromMap(Map obj) { - return IoniaOrder( - id: obj['order_id'] as String, - uri: obj['uri'] as String, - currency: obj['currency'] as String, - amount: obj['amount'] as double, - paymentId: obj['paymentId'] as String); - } - - final String id; - final String uri; - final String currency; - final double amount; - final String paymentId; -} \ No newline at end of file diff --git a/lib/ionia/ionia_service.dart b/lib/ionia/ionia_service.dart deleted file mode 100644 index 821824a87..000000000 --- a/lib/ionia/ionia_service.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:cake_wallet/core/secure_storage.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_order.dart'; -import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; -import 'package:cake_wallet/.secrets.g.dart' as secrets; -import 'package:cake_wallet/ionia/ionia_api.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/ionia/ionia_category.dart'; - -class IoniaService { - IoniaService(this.secureStorage, this.ioniaApi); - - static const ioniaEmailStorageKey = 'ionia_email'; - static const ioniaUsernameStorageKey = 'ionia_username'; - static const ioniaPasswordStorageKey = 'ionia_password'; - - static String get clientId => secrets.ioniaClientId; - - final SecureStorage secureStorage; - final IoniaApi ioniaApi; - - // Create user - - Future createUser(String email) async { - final username = await ioniaApi.createUser(email, clientId: clientId); - await secureStorage.write(key: ioniaEmailStorageKey, value: email); - await secureStorage.write(key: ioniaUsernameStorageKey, value: username); - } - - // Verify email - - Future verifyEmail(String code) async { - final email = (await secureStorage.read(key: ioniaEmailStorageKey))!; - final credentials = await ioniaApi.verifyEmail(email: email, code: code, clientId: clientId); - await secureStorage.write(key: ioniaPasswordStorageKey, value: credentials.password); - await secureStorage.write(key: ioniaUsernameStorageKey, value: credentials.username); - } - - // Sign In - - Future signIn(String email) async { - await ioniaApi.signIn(email, clientId: clientId); - await secureStorage.write(key: ioniaEmailStorageKey, value: email); - } - - Future getUserEmail() async { - return (await secureStorage.read(key: ioniaEmailStorageKey))!; - } - - // Check is user logined - - Future isLogined() async { - final username = await secureStorage.read(key: ioniaUsernameStorageKey) ?? ''; - final password = await secureStorage.read(key: ioniaPasswordStorageKey) ?? ''; - return username.isNotEmpty && password.isNotEmpty; - } - - // Logout - - Future logout() async { - await secureStorage.delete(key: ioniaUsernameStorageKey); - await secureStorage.delete(key: ioniaPasswordStorageKey); - } - - // Create virtual card - - Future createCard() async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.createCard(username: username, password: password, clientId: clientId); - } - - // Get virtual card - - Future getCard() async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getCards(username: username, password: password, clientId: clientId); - } - - // Get Merchants - - Future> getMerchants() async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getMerchants(username: username, password: password, clientId: clientId); - } - - // Get Merchants By Filter - - Future> getMerchantsByFilter({ - String? search, - List? categories, - int merchantFilterType = 0}) async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getMerchantsByFilter( - username: username, - password: password, - clientId: clientId, - search: search, - categories: categories, - merchantFilterType: merchantFilterType); - } - - // Purchase Gift Card - - Future purchaseGiftCard({ - required String merchId, - required double amount, - required String currency}) async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - final deviceId = ''; - return ioniaApi.purchaseGiftCard( - requestedUUID: deviceId, - merchId: merchId, - amount: amount, - currency: currency, - username: username, - password: password, - clientId: clientId); - } - - // Get Current User Gift Card Summaries - - Future> getCurrentUserGiftCardSummaries() async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getCurrentUserGiftCardSummaries(username: username, password: password, clientId: clientId); - } - - // Charge Gift Card - - Future chargeGiftCard({ - required int giftCardId, - required double amount}) async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - await ioniaApi.chargeGiftCard( - username: username, - password: password, - clientId: clientId, - giftCardId: giftCardId, - amount: amount); - } - - // Redeem - - Future redeem({required int giftCardId, required double amount}) async { - await chargeGiftCard(giftCardId: giftCardId, amount: amount); - } - - // Get Gift Card - - Future getGiftCard({required int id}) async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getGiftCard(username: username, password: password, clientId: clientId,id: id); - } - - // Payment Status - - Future getPaymentStatus({ - required String orderId, - required String paymentId}) async { - final username = (await secureStorage.read(key: ioniaUsernameStorageKey))!; - final password = (await secureStorage.read(key: ioniaPasswordStorageKey))!; - return ioniaApi.getPaymentStatus(username: username, password: password, clientId: clientId, orderId: orderId, paymentId: paymentId); - } -} \ No newline at end of file diff --git a/lib/ionia/ionia_tip.dart b/lib/ionia/ionia_tip.dart deleted file mode 100644 index 089c108f4..000000000 --- a/lib/ionia/ionia_tip.dart +++ /dev/null @@ -1,18 +0,0 @@ -class IoniaTip { - const IoniaTip({ - required this.originalAmount, - required this.percentage, - this.isCustom = false}); - - final double originalAmount; - final double percentage; - final bool isCustom; - - double get additionalAmount => double.parse((originalAmount * percentage / 100).toStringAsFixed(2)); - - static const tipList = [ - IoniaTip(originalAmount: 0, percentage: 0), - IoniaTip(originalAmount: 10, percentage: 10), - IoniaTip(originalAmount: 20, percentage: 20) - ]; -} diff --git a/lib/ionia/ionia_token_data.dart b/lib/ionia/ionia_token_data.dart deleted file mode 100644 index 13682ae0e..000000000 --- a/lib/ionia/ionia_token_data.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'dart:convert'; - -class IoniaTokenData { - IoniaTokenData({required this.accessToken, required this.tokenType, required this.expiredAt}); - - factory IoniaTokenData.fromJson(String source) { - final decoded = json.decode(source) as Map; - final accessToken = decoded['access_token'] as String; - final expiresIn = decoded['expires_in'] as int; - final tokenType = decoded['token_type'] as String; - final expiredAtInMilliseconds = decoded['expired_at'] as int; - DateTime expiredAt; - - if (expiredAtInMilliseconds != null) { - expiredAt = DateTime.fromMillisecondsSinceEpoch(expiredAtInMilliseconds); - } else { - expiredAt = DateTime.now().add(Duration(seconds: expiresIn)); - } - - return IoniaTokenData( - accessToken: accessToken, - tokenType: tokenType, - expiredAt: expiredAt); - } - - final String accessToken; - final String tokenType; - final DateTime expiredAt; - - bool get isExpired => DateTime.now().isAfter(expiredAt); - - @override - String toString() => '$tokenType $accessToken'; - - String toJson() { - return json.encode({ - 'access_token': accessToken, - 'token_type': tokenType, - 'expired_at': expiredAt.millisecondsSinceEpoch - }); - } -} \ No newline at end of file diff --git a/lib/ionia/ionia_user_credentials.dart b/lib/ionia/ionia_user_credentials.dart deleted file mode 100644 index c398385f5..000000000 --- a/lib/ionia/ionia_user_credentials.dart +++ /dev/null @@ -1,6 +0,0 @@ -class IoniaUserCredentials { - const IoniaUserCredentials(this.username, this.password); - - final String username; - final String password; -} \ No newline at end of file diff --git a/lib/ionia/ionia_virtual_card.dart b/lib/ionia/ionia_virtual_card.dart deleted file mode 100644 index ca3e35dbc..000000000 --- a/lib/ionia/ionia_virtual_card.dart +++ /dev/null @@ -1,41 +0,0 @@ -class IoniaVirtualCard { - IoniaVirtualCard({ - required this.token, - required this.createdAt, - required this.lastFour, - required this.state, - required this.pan, - required this.cvv, - required this.expirationMonth, - required this.expirationYear, - required this.fundsLimit, - required this.spendLimit}); - - factory IoniaVirtualCard.fromMap(Map source) { - final created = source['created'] as String; - final createdAt = DateTime.tryParse(created); - - return IoniaVirtualCard( - token: source['token'] as String, - createdAt: createdAt, - lastFour: source['lastFour'] as String, - state: source['state'] as String, - pan: source['pan'] as String, - cvv: source['cvv'] as String, - expirationMonth: source['expirationMonth'] as String, - expirationYear: source['expirationYear'] as String, - fundsLimit: source['FundsLimit'] as double, - spendLimit: source['spend_limit'] as double); - } - - final String token; - final String lastFour; - final String state; - final String pan; - final String cvv; - final String expirationMonth; - final String expirationYear; - final DateTime? createdAt; - final double fundsLimit; - final double spendLimit; -} \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index e113e42f9..c09664cef 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; @@ -10,7 +9,6 @@ import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/wallet_nft_response.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -36,14 +34,6 @@ import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; import 'package:cake_wallet/src/screens/faq/faq_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_cards_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_account_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_redeem_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_custom_tip_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_gift_card_detail_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_more_options_page.dart'; -import 'package:cake_wallet/src/screens/ionia/cards/ionia_payment_status_page.dart'; -import 'package:cake_wallet/src/screens/ionia/ionia.dart'; import 'package:cake_wallet/src/screens/monero_accounts/monero_account_edit_or_create_page.dart'; import 'package:cake_wallet/src/screens/nano/nano_change_rep_page.dart'; import 'package:cake_wallet/src/screens/nano_accounts/nano_account_edit_or_create_page.dart'; @@ -76,9 +66,11 @@ import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/auth/cake_pay_account_page.dart'; import 'package:cake_wallet/src/screens/settings/silent_payments_settings.dart'; import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/settings/trocador_providers_page.dart'; +import 'package:cake_wallet/src/screens/settings/tor_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; @@ -120,7 +112,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - +import 'package:cake_wallet/src/screens/cake_pay/cake_pay.dart'; import 'src/screens/dashboard/pages/nft_import_page.dart'; late RouteSettings currentRouteSettings; @@ -518,73 +510,30 @@ Route createRoute(RouteSettings settings) { param1: settings.arguments as QrViewData, )); - case Routes.ioniaWelcomePage: + case Routes.cakePayCardsPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); + + case Routes.cakePayBuyCardPage: + final args = settings.arguments as List; return CupertinoPageRoute( - fullscreenDialog: true, - builder: (_) => getIt.get(), + builder: (_) => getIt.get(param1: args)); + + case Routes.cakePayBuyCardDetailPage: + final args = settings.arguments as List; + return CupertinoPageRoute( + builder: (_) => getIt.get(param1: args)); + + case Routes.cakePayWelcomePage: + return CupertinoPageRoute( + builder: (_) => getIt.get(), ); - case Routes.ioniaLoginPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaCreateAccountPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaManageCardsPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaBuyGiftCardPage: + case Routes.cakePayVerifyOtpPage: final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); + return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); - case Routes.ioniaBuyGiftCardDetailPage: - final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); - - case Routes.ioniaVerifyIoniaOtpPage: - final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); - - case Routes.ioniaDebitCardPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaActivateDebitCardPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaAccountPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaAccountCardsPage: - return CupertinoPageRoute(builder: (_) => getIt.get()); - - case Routes.ioniaCustomTipPage: - final args = settings.arguments as List; - return CupertinoPageRoute(builder: (_) => getIt.get(param1: args)); - - case Routes.ioniaGiftCardDetailPage: - final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args.first)); - - case Routes.ioniaCustomRedeemPage: - final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); - - case Routes.ioniaMoreOptionsPage: - final args = settings.arguments as List; - return CupertinoPageRoute( - builder: (_) => getIt.get(param1: args)); - - case Routes.ioniaPaymentStatusPage: - final args = settings.arguments as List; - final paymentInfo = args.first as IoniaAnyPayPaymentInfo; - final commitedInfo = args[1] as AnyPayPaymentCommittedInfo; - return CupertinoPageRoute( - builder: (_) => - getIt.get(param1: paymentInfo, param2: commitedInfo)); + case Routes.cakePayAccountPage: + return CupertinoPageRoute(builder: (_) => getIt.get()); case Routes.webViewPage: final args = settings.arguments as List; diff --git a/lib/routes.dart b/lib/routes.dart index b5208416f..78a93bee7 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -64,22 +64,13 @@ class Routes { static const unspentCoinsDetails = '/unspent_coins_details'; static const addressPage = '/address_page'; static const fullscreenQR = '/fullscreen_qr'; - static const ioniaWelcomePage = '/cake_pay_welcome_page'; - static const ioniaCreateAccountPage = '/cake_pay_create_account_page'; - static const ioniaLoginPage = '/cake_pay_login_page'; - static const ioniaManageCardsPage = '/manage_cards_page'; - static const ioniaBuyGiftCardPage = '/buy_gift_card_page'; - static const ioniaBuyGiftCardDetailPage = '/buy_gift_card_detail_page'; - static const ioniaVerifyIoniaOtpPage = '/cake_pay_verify_otp_page'; - static const ioniaDebitCardPage = '/debit_card_page'; - static const ioniaActivateDebitCardPage = '/activate_debit_card_page'; - static const ioniaAccountPage = 'ionia_account_page'; - static const ioniaAccountCardsPage = 'ionia_account_cards_page'; - static const ioniaCustomTipPage = 'ionia_custom_tip_page'; - static const ioniaGiftCardDetailPage = '/ionia_gift_card_detail_page'; - static const ioniaPaymentStatusPage = '/ionia_payment_status_page'; - static const ioniaMoreOptionsPage = '/ionia_more_options_page'; - static const ioniaCustomRedeemPage = '/ionia_custom_redeem_page'; + static const cakePayWelcomePage = '/cake_pay_welcome_page'; + static const cakePayLoginPage = '/cake_pay_login_page'; + static const cakePayCardsPage = '/cake_pay_cards_page'; + static const cakePayBuyCardPage = '/cake_pay_buy_card_page'; + static const cakePayBuyCardDetailPage = '/cake_pay_buy_card_detail_page'; + static const cakePayVerifyOtpPage = '/cake_pay_verify_otp_page'; + static const cakePayAccountPage = '/cake_pay_account_page'; static const webViewPage = '/web_view_page'; static const silentPaymentsSettings = '/silent_payments_settings'; static const connectionSync = '/connection_sync_page'; diff --git a/lib/src/screens/base_page.dart b/lib/src/screens/base_page.dart index bf60525e4..fbf4bb1fa 100644 --- a/lib/src/screens/base_page.dart +++ b/lib/src/screens/base_page.dart @@ -7,7 +7,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/src/widgets/nav_bar.dart'; import 'package:cake_wallet/generated/i18n.dart'; -enum AppBarStyle { regular, withShadow, transparent } +enum AppBarStyle { regular, withShadow, transparent, completelyTransparent } abstract class BasePage extends StatelessWidget { BasePage() : _scaffoldKey = GlobalKey(); @@ -125,7 +125,7 @@ abstract class BasePage extends StatelessWidget { Widget? floatingActionButton(BuildContext context) => null; - ObstructingPreferredSizeWidget appBar(BuildContext context) { + PreferredSizeWidget appBar(BuildContext context) { final appBarColor = pageBackgroundColor(context); switch (appBarStyle) { @@ -156,6 +156,16 @@ abstract class BasePage extends StatelessWidget { border: null, ); + case AppBarStyle.completelyTransparent: + return AppBar( + leading: leading(context), + title: middle(context), + actions: [if (trailing(context) != null) trailing(context)!], + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + ); + default: // FIX-ME: NavBar no context return NavBar( diff --git a/lib/src/screens/cake_pay/auth/cake_pay_account_page.dart b/lib/src/screens/cake_pay/auth/cake_pay_account_page.dart new file mode 100644 index 000000000..4a958a661 --- /dev/null +++ b/lib/src/screens/cake_pay/auth/cake_pay_account_page.dart @@ -0,0 +1,90 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_tile.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_account_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class CakePayAccountPage extends BasePage { + CakePayAccountPage(this.cakePayAccountViewModel); + + final CakePayAccountViewModel cakePayAccountViewModel; + + + + @override + Widget leading(BuildContext context) { + return MergeSemantics( + child: SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: Semantics( + label: S.of(context).seed_alert_back, + child: TextButton( + style: ButtonStyle( + overlayColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent), + ), + onPressed: () => Navigator.pop(context), + child: backButton(context), + ), + ), + ), + ), + ); + } + + @override + Widget middle(BuildContext context) { + return Text( + S.current.account, + style: textMediumSemiBold( + color: Theme.of(context).extension()!.titleColor, + ), + ); + } + + @override + Widget body(BuildContext context) { + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.all(24), + content: Column( + children: [ + SizedBox(height: 20), + Observer( + builder: (_) => Container(decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.0, + color: + Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: CakePayTile(title: S.of(context).email_address, subTitle: cakePayAccountViewModel.email)), + ), + ], + ), + bottomSectionPadding: EdgeInsets.all(30), + bottomSection: Column( + children: [ + PrimaryButton( + color: Theme.of(context).primaryColor, + textColor: Colors.white, + text: S.of(context).logout, + onPressed: () { + cakePayAccountViewModel.logout(); + Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart b/lib/src/screens/cake_pay/auth/cake_pay_verify_otp_page.dart similarity index 81% rename from lib/src/screens/ionia/auth/ionia_verify_otp_page.dart rename to lib/src/screens/cake_pay/auth/cake_pay_verify_otp_page.dart index e8327b71c..34b02db93 100644 --- a/lib/src/screens/ionia/auth/ionia_verify_otp_page.dart +++ b/lib/src/screens/cake_pay/auth/cake_pay_verify_otp_page.dart @@ -1,39 +1,38 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; -class IoniaVerifyIoniaOtp extends BasePage { - IoniaVerifyIoniaOtp(this._authViewModel, this._email, this.isSignIn) +class CakePayVerifyOtpPage extends BasePage { + CakePayVerifyOtpPage(this._authViewModel, this._email, this.isSignIn) : _codeController = TextEditingController(), _codeFocus = FocusNode() { _codeController.addListener(() { final otp = _codeController.text; _authViewModel.otp = otp; - if (otp.length > 3) { - _authViewModel.otpState = IoniaOtpSendEnabled(); + if (otp.length > 5) { + _authViewModel.otpState = CakePayOtpSendEnabled(); } else { - _authViewModel.otpState = IoniaOtpSendDisabled(); + _authViewModel.otpState = CakePayOtpSendDisabled(); } }); } - final IoniaAuthViewModel _authViewModel; + final CakePayAuthViewModel _authViewModel; final bool isSignIn; final String _email; @@ -53,11 +52,11 @@ class IoniaVerifyIoniaOtp extends BasePage { @override Widget body(BuildContext context) { - reaction((_) => _authViewModel.otpState, (IoniaOtpState state) { - if (state is IoniaOtpFailure) { + reaction((_) => _authViewModel.otpState, (CakePayOtpState state) { + if (state is CakePayOtpFailure) { _onOtpFailure(context, state.error); } - if (state is IoniaOtpSuccess) { + if (state is CakePayOtpSuccess) { _onOtpSuccessful(context); } }); @@ -98,9 +97,7 @@ class IoniaVerifyIoniaOtp extends BasePage { Text(S.of(context).didnt_get_code), SizedBox(width: 20), InkWell( - onTap: () => isSignIn - ? _authViewModel.signIn(_email) - : _authViewModel.createUser(_email), + onTap: () => _authViewModel.logIn(_email), child: Text( S.of(context).resend_code, style: textSmallSemiBold(color: Palette.blueCraiola), @@ -120,8 +117,8 @@ class IoniaVerifyIoniaOtp extends BasePage { builder: (_) => LoadingPrimaryButton( text: S.of(context).continue_text, onPressed: _verify, - isDisabled: _authViewModel.otpState is IoniaOtpSendDisabled, - isLoading: _authViewModel.otpState is IoniaOtpValidating, + isDisabled: _authViewModel.otpState is CakePayOtpSendDisabled, + isLoading: _authViewModel.otpState is CakePayOtpValidating, color: Theme.of(context).primaryColor, textColor: Colors.white, ), @@ -149,8 +146,7 @@ class IoniaVerifyIoniaOtp extends BasePage { } void _onOtpSuccessful(BuildContext context) => - Navigator.of(context) - .pushNamedAndRemoveUntil(Routes.ioniaManageCardsPage, (route) => route.isFirst); + Navigator.pop(context); void _verify() async => await _authViewModel.verifyEmail(_codeController.text); } diff --git a/lib/src/screens/ionia/auth/ionia_login_page.dart b/lib/src/screens/cake_pay/auth/cake_pay_welcome_page.dart similarity index 62% rename from lib/src/screens/ionia/auth/ionia_login_page.dart rename to lib/src/screens/cake_pay/auth/cake_pay_welcome_page.dart index 1bdcfc3a4..cd893daf4 100644 --- a/lib/src/screens/ionia/auth/ionia_login_page.dart +++ b/lib/src/screens/cake_pay/auth/cake_pay_welcome_page.dart @@ -1,22 +1,22 @@ import 'package:cake_wallet/core/email_validator.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; -class IoniaLoginPage extends BasePage { - IoniaLoginPage(this._authViewModel) +class CakePayWelcomePage extends BasePage { + CakePayWelcomePage(this._authViewModel) : _formKey = GlobalKey(), _emailController = TextEditingController() { _emailController.text = _authViewModel.email; @@ -25,14 +25,14 @@ class IoniaLoginPage extends BasePage { final GlobalKey _formKey; - final IoniaAuthViewModel _authViewModel; + final CakePayAuthViewModel _authViewModel; final TextEditingController _emailController; @override Widget middle(BuildContext context) { return Text( - S.current.login, + S.current.welcome_to_cakepay, style: textMediumSemiBold( color: Theme.of(context).extension()!.titleColor, ), @@ -41,25 +41,40 @@ class IoniaLoginPage extends BasePage { @override Widget body(BuildContext context) { - reaction((_) => _authViewModel.signInState, (IoniaCreateAccountState state) { - if (state is IoniaCreateStateFailure) { + reaction((_) => _authViewModel.userVerificationState, (CakePayUserVerificationState state) { + if (state is CakePayUserVerificationStateFailure) { _onLoginUserFailure(context, state.error); } - if (state is IoniaCreateStateSuccess) { + if (state is CakePayUserVerificationStateSuccess) { _onLoginSuccessful(context, _authViewModel); } }); return ScrollableWithBottomSection( contentPadding: EdgeInsets.all(24), - content: Form( - key: _formKey, - child: BaseTextFormField( - hintText: S.of(context).email_address, - keyboardType: TextInputType.emailAddress, - validator: EmailValidator(), - controller: _emailController, - onSubmit: (text) => _login(), - ), + content: Column( + children: [ + SizedBox(height: 90), + Form( + key: _formKey, + child: BaseTextFormField( + hintText: S.of(context).email_address, + keyboardType: TextInputType.emailAddress, + validator: EmailValidator(), + controller: _emailController, + onSubmit: (text) => _login(), + ), + ), + SizedBox(height: 20), + Text( + S.of(context).about_cake_pay, + style: textLarge( + color: Theme.of(context).extension()!.titleColor, + ), + ), + SizedBox(height: 20), + Text(S.of(context).cake_pay_account_note, + style: textLarge(color: Theme.of(context).extension()!.titleColor)), + ], ), bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), bottomSection: Column( @@ -71,7 +86,8 @@ class IoniaLoginPage extends BasePage { builder: (_) => LoadingPrimaryButton( text: S.of(context).login, onPressed: _login, - isLoading: _authViewModel.signInState is IoniaCreateStateLoading, + isLoading: + _authViewModel.userVerificationState is CakePayUserVerificationStateLoading, color: Theme.of(context).primaryColor, textColor: Colors.white, ), @@ -98,9 +114,10 @@ class IoniaLoginPage extends BasePage { }); } - void _onLoginSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed( + void _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) => + Navigator.pushReplacementNamed( context, - Routes.ioniaVerifyIoniaOtpPage, + Routes.cakePayVerifyOtpPage, arguments: [authViewModel.email, true], ); @@ -108,6 +125,6 @@ class IoniaLoginPage extends BasePage { if (_formKey.currentState != null && !_formKey.currentState!.validate()) { return; } - await _authViewModel.signIn(_emailController.text); + await _authViewModel.logIn(_emailController.text); } } diff --git a/lib/src/screens/cake_pay/cake_pay.dart b/lib/src/screens/cake_pay/cake_pay.dart new file mode 100644 index 000000000..a65b6bed3 --- /dev/null +++ b/lib/src/screens/cake_pay/cake_pay.dart @@ -0,0 +1,5 @@ +export 'auth/cake_pay_welcome_page.dart'; +export 'auth/cake_pay_verify_otp_page.dart'; +export 'cards/cake_pay_confirm_purchase_card_page.dart'; +export 'cards/cake_pay_cards_page.dart'; +export 'cards/cake_pay_buy_card_page.dart'; diff --git a/lib/src/screens/cake_pay/cards/cake_pay_buy_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_buy_card_page.dart new file mode 100644 index 000000000..6e1af1c32 --- /dev/null +++ b/lib/src/screens/cake_pay/cards/cake_pay_buy_card_page.dart @@ -0,0 +1,474 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; +import 'package:cake_wallet/src/widgets/number_text_fild_widget.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:keyboard_actions/keyboard_actions.dart'; + +class CakePayBuyCardPage extends BasePage { + CakePayBuyCardPage( + this.cakePayBuyCardViewModel, + this.cakePayService, + ) : _amountFieldFocus = FocusNode(), + _amountController = TextEditingController(), + _quantityFieldFocus = FocusNode(), + _quantityController = + TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) { + _amountController.addListener(() { + cakePayBuyCardViewModel.onAmountChanged(_amountController.text); + }); + } + + final CakePayBuyCardViewModel cakePayBuyCardViewModel; + final CakePayService cakePayService; + + @override + String get title => cakePayBuyCardViewModel.card.name; + + @override + bool get extendBodyBehindAppBar => true; + + @override + AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent; + + @override + Widget? middle(BuildContext context) { + return Text( + title, + textAlign: TextAlign.center, + maxLines: 2, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: titleColor(context)), + ); + } + + final TextEditingController _amountController; + final FocusNode _amountFieldFocus; + final TextEditingController _quantityController; + final FocusNode _quantityFieldFocus; + + @override + Widget body(BuildContext context) { + final card = cakePayBuyCardViewModel.card; + final vendor = cakePayBuyCardViewModel.vendor; + + return KeyboardActions( + disableScroll: true, + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFieldFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + ]), + child: Container( + color: Theme.of(context).colorScheme.background, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Theme.of(context).extension()!.firstGradientColor, + Theme.of(context).extension()!.secondGradientColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + height: responsiveLayoutUtil.screenHeight * 0.35, + width: double.infinity, + child: Column( + children: [ + Expanded(flex: 4, child: const SizedBox()), + Expanded( + flex: 7, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(10)), + child: Image.network( + card.cardImageUrl ?? '', + fit: BoxFit.cover, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) => + CakePayCardImagePlaceholder(), + ), + ), + ), + Expanded(child: const SizedBox()), + ], + )), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + height: responsiveLayoutUtil.screenHeight * 0.5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Expanded( + child: Text(S.of(context).enter_amount, + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 24, + fontWeight: FontWeight.w600, + )), + ), + card.denominations.isNotEmpty + ? Expanded( + flex: 2, + child: _DenominationsAmountWidget( + fiatCurrency: card.fiatCurrency.title, + denominations: card.denominations, + amountFieldFocus: _amountFieldFocus, + amountController: _amountController, + quantityFieldFocus: _quantityFieldFocus, + quantityController: _quantityController, + onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, + onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged, + cakePayBuyCardViewModel: cakePayBuyCardViewModel, + ), + ) + : Expanded( + flex: 2, + child: _EnterAmountWidget( + minValue: card.minValue ?? '-', + maxValue: card.maxValue ?? '-', + fiatCurrency: card.fiatCurrency.title, + amountFieldFocus: _amountFieldFocus, + amountController: _amountController, + onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, + ), + ), + Expanded( + flex: 5, + child: Column( + children: [ + if (vendor.cakeWarnings != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white.withOpacity(0.20)), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + vendor.cakeWarnings!, + textAlign: TextAlign.center, + style: textSmallSemiBold(color: Colors.white), + ), + ), + ), + ), + Expanded( + child: SingleChildScrollView( + child: ClickableLinksText( + text: card.description ?? '', + textStyle: TextStyle( + color: Theme.of(context) + .extension()! + .secondaryTextColor, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + bottomSection: Column( + children: [ + Observer(builder: (_) { + return Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () => navigateToCakePayBuyCardDetailPage(context, card), + text: S.of(context).buy_now, + isDisabled: !cakePayBuyCardViewModel.isEnablePurchase, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ), + ); + }), + ], + ), + ), + ), + ); + } + + Future navigateToCakePayBuyCardDetailPage(BuildContext context, CakePayCard card) async { + final userName = await cakePayService.getUserEmail(); + final paymentCredential = PaymentCredential( + amount: cakePayBuyCardViewModel.amount, + quantity: cakePayBuyCardViewModel.quantity, + totalAmount: cakePayBuyCardViewModel.totalAmount, + userName: userName, + fiatCurrency: card.fiatCurrency.title, + ); + + Navigator.pushNamed( + context, + Routes.cakePayBuyCardDetailPage, + arguments: [paymentCredential, card], + ); + } +} + +class _DenominationsAmountWidget extends StatelessWidget { + const _DenominationsAmountWidget({ + required this.fiatCurrency, + required this.denominations, + required this.amountFieldFocus, + required this.amountController, + required this.quantityFieldFocus, + required this.quantityController, + required this.cakePayBuyCardViewModel, + required this.onAmountChanged, + required this.onQuantityChanged, + }); + + final String fiatCurrency; + final List denominations; + final FocusNode amountFieldFocus; + final TextEditingController amountController; + final FocusNode quantityFieldFocus; + final TextEditingController quantityController; + final CakePayBuyCardViewModel cakePayBuyCardViewModel; + final Function(String) onAmountChanged; + final Function(int?) onQuantityChanged; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 12, + child: Column( + children: [ + Expanded( + child: DropdownFilterList( + items: denominations, + itemPrefix: fiatCurrency, + selectedItem: denominations.first, + textStyle: textMediumSemiBold( + color: Theme.of(context).extension()!.titleColor), + onItemSelected: (value) { + amountController.text = value; + onAmountChanged(value); + }, + caption: '', + ), + ), + const SizedBox(height: 4), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.0, + color: Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: Text(S.of(context).choose_card_value + ':', + maxLines: 2, + style: textSmall( + color: Theme.of(context).extension()!.secondaryTextColor)), + ), + ), + ], + ), + ), + Expanded(child: const SizedBox()), + Expanded( + flex: 8, + child: Column( + children: [ + Expanded( + child: NumberTextField( + controller: quantityController, + focusNode: quantityFieldFocus, + min: 1, + max: 99, + onChanged: (value) => onQuantityChanged(value), + ), + ), + const SizedBox(height: 4), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.0, + color: Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: Text(S.of(context).quantity + ':', + maxLines: 1, + style: textSmall( + color: Theme.of(context).extension()!.secondaryTextColor)), + ), + ), + ], + ), + ), + Expanded(child: const SizedBox()), + Expanded( + flex: 12, + child: Column( + children: [ + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: Observer( + builder: (_) => AutoSizeText( + '$fiatCurrency ${cakePayBuyCardViewModel.totalAmount}', + maxLines: 1, + style: textMediumSemiBold( + color: + Theme.of(context).extension()!.titleColor)))), + ), + const SizedBox(height: 4), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + top: BorderSide( + width: 1.0, + color: + Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: Text(S.of(context).total + ':', + maxLines: 1, + style: textSmall( + color: + Theme.of(context).extension()!.secondaryTextColor)), + ), + ), + ], + )), + ], + ); + } +} + +class _EnterAmountWidget extends StatelessWidget { + const _EnterAmountWidget({ + required this.minValue, + required this.maxValue, + required this.fiatCurrency, + required this.amountFieldFocus, + required this.amountController, + required this.onAmountChanged, + }); + + final String minValue; + final String maxValue; + final String fiatCurrency; + final FocusNode amountFieldFocus; + final TextEditingController amountController; + final Function(String) onAmountChanged; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.0, + color: Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: BaseTextFormField( + controller: amountController, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + hintText: '0.00', + maxLines: null, + borderColor: Colors.transparent, + prefixIcon: Padding( + padding: const EdgeInsets.only(top: 12), + child: Text( + '$fiatCurrency: ', + style: textMediumSemiBold( + color: Theme.of(context).extension()!.titleColor), + ), + ), + textStyle: + textMediumSemiBold(color: Theme.of(context).extension()!.titleColor), + placeholderTextStyle: textMediumSemiBold( + color: Theme.of(context).extension()!.secondaryTextColor), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), + FilteringTextInputFormatter.allow( + RegExp(r'^\d+(\.|\,)?\d{0,2}'), + ), + ], + ), + ), + SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.of(context).min_amount(minValue) + ' $fiatCurrency', + style: textSmall( + color: Theme.of(context).extension()!.secondaryTextColor)), + Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency', + style: textSmall( + color: Theme.of(context).extension()!.secondaryTextColor)), + ], + ), + ], + ); + } +} diff --git a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart similarity index 50% rename from lib/src/screens/ionia/cards/ionia_manage_cards_page.dart rename to lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart index c29f571b4..35a58ce0a 100644 --- a/lib/src/screens/ionia/cards/ionia_manage_cards_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_cards_page.dart @@ -1,42 +1,40 @@ -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/gradient_background.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_menu.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/ionia_filter_modal.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/card_item.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/card_menu.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/filter_widget.dart'; import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; -import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/debounce.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/src/widgets/gradient_background.dart'; import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/debounce.dart'; +import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; -class IoniaManageCardsPage extends BasePage { - IoniaManageCardsPage(this._cardsListViewModel): searchFocusNode = FocusNode() { +class CakePayCardsPage extends BasePage { + CakePayCardsPage(this._cardsListViewModel) : searchFocusNode = FocusNode() { _searchController.addListener(() { if (_searchController.text != _cardsListViewModel.searchString) { _searchDebounce.run(() { - _cardsListViewModel.searchMerchant(_searchController.text); + _cardsListViewModel.resetLoadingNextPageState(); + _cardsListViewModel.getVendors(text: _searchController.text); }); } }); - - _cardsListViewModel.getMerchants(); - } + final FocusNode searchFocusNode; - final IoniaGiftCardsListViewModel _cardsListViewModel; + final CakePayCardsListViewModel _cardsListViewModel; final _searchDebounce = Debounce(Duration(milliseconds: 500)); final _searchController = TextEditingController(); @@ -46,8 +44,7 @@ class IoniaManageCardsPage extends BasePage { @override Widget Function(BuildContext, Widget) get rootWrapper => - (BuildContext context, Widget scaffold) => - GradientBackground(scaffold: scaffold); + (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold); @override bool get resizeToAvoidBottomInset => false; @@ -58,7 +55,7 @@ class IoniaManageCardsPage extends BasePage { @override Widget middle(BuildContext context) { return Text( - S.of(context).gift_cards, + 'Cake Pay', style: textMediumSemiBold( color: Theme.of(context).extension()!.textColor, ), @@ -68,9 +65,17 @@ class IoniaManageCardsPage extends BasePage { @override Widget trailing(BuildContext context) { return _TrailingIcon( - asset: 'assets/images/profile.png', - onPressed: () => Navigator.pushNamed(context, Routes.ioniaAccountPage), - ); + asset: 'assets/images/profile.png', + iconColor: pageIconColor(context) ?? Colors.white, + onPressed: () { + _cardsListViewModel.isCakePayUserAuthenticated().then((value) { + if (value) { + Navigator.pushNamed(context, Routes.cakePayAccountPage); + return; + } + Navigator.pushNamed(context, Routes.cakePayWelcomePage); + }); + }); } @override @@ -79,8 +84,12 @@ class IoniaManageCardsPage extends BasePage { label: S.of(context).filter_by, child: InkWell( onTap: () async { - await showCategoryFilter(context); - _cardsListViewModel.getMerchants(); + _cardsListViewModel.storeInitialFilterStates(); + await showFilterWidget(context); + if (_cardsListViewModel.hasFiltersChanged) { + _cardsListViewModel.resetLoadingNextPageState(); + _cardsListViewModel.getVendors(); + } }, child: Container( width: 32, @@ -120,7 +129,7 @@ class IoniaManageCardsPage extends BasePage { ), SizedBox(height: 8), Expanded( - child: IoniaManageCardsPageBody( + child: CakePayCardsPageBody( cardsListViewModel: _cardsListViewModel, ), ), @@ -129,36 +138,35 @@ class IoniaManageCardsPage extends BasePage { ); } - Future showCategoryFilter(BuildContext context) async { + Future showFilterWidget(BuildContext context) async { return showPopUp( context: context, builder: (BuildContext context) { - return IoniaFilterModal( - ioniaGiftCardsListViewModel: _cardsListViewModel, - ); + return FilterWidget(filterItems: _cardsListViewModel.createFilterItems); }, ); } } -class IoniaManageCardsPageBody extends StatefulWidget { - const IoniaManageCardsPageBody({ +class CakePayCardsPageBody extends StatefulWidget { + const CakePayCardsPageBody({ Key? key, required this.cardsListViewModel, }) : super(key: key); - final IoniaGiftCardsListViewModel cardsListViewModel; + final CakePayCardsListViewModel cardsListViewModel; @override - _IoniaManageCardsPageBodyState createState() => _IoniaManageCardsPageBodyState(); + _CakePayCardsPageBodyState createState() => _CakePayCardsPageBodyState(); } -class _IoniaManageCardsPageBodyState extends State { +class _CakePayCardsPageBodyState extends State { double get backgroundHeight => MediaQuery.of(context).size.height * 0.75; double thumbHeight = 72; - bool get isAlwaysShowScrollThumb => merchantsList == null ? false : merchantsList.length > 3; - List get merchantsList => widget.cardsListViewModel.ioniaMerchants; + bool get isAlwaysShowScrollThumb => merchantsList.isEmpty ? false : merchantsList.length > 3; + + List get merchantsList => widget.cardsListViewModel.cakePayVendors; final _scrollController = ScrollController(); @@ -166,61 +174,93 @@ class _IoniaManageCardsPageBodyState extends State { void initState() { _scrollController.addListener(() { final scrollOffsetFromTop = _scrollController.hasClients - ? (_scrollController.offset / _scrollController.position.maxScrollExtent * (backgroundHeight - thumbHeight)) + ? (_scrollController.offset / + _scrollController.position.maxScrollExtent * + (backgroundHeight - thumbHeight)) : 0.0; widget.cardsListViewModel.setScrollOffsetFromTop(scrollOffsetFromTop); + + double threshold = 200.0; + bool isNearBottom = + _scrollController.offset >= _scrollController.position.maxScrollExtent - threshold; + if (isNearBottom && !_scrollController.position.outOfRange) { + widget.cardsListViewModel.fetchNextPage(); + } }); super.initState(); } @override Widget build(BuildContext context) { - return Observer( - builder: (_) { - final merchantState = widget.cardsListViewModel.merchantState; - if (merchantState is IoniaLoadedMerchantState) { + return Observer(builder: (_) { + final vendorsState = widget.cardsListViewModel.vendorsState; + if (vendorsState is CakePayVendorLoadedState) { + bool isLoadingMore = widget.cardsListViewModel.isLoadingNextPage; + final vendors = widget.cardsListViewModel.cakePayVendors; + + if (vendors.isEmpty) { + return Center(child: Text(S.of(context).no_cards_found)); + } return Stack(children: [ - ListView.separated( - padding: EdgeInsets.only(left: 2, right: 22), - controller: _scrollController, - itemCount: merchantsList.length, - separatorBuilder: (_, __) => SizedBox(height: 4), - itemBuilder: (_, index) { - final merchant = merchantsList[index]; - return CardItem( - logoUrl: merchant.logoUrl, - onTap: () { - Navigator.of(context).pushNamed(Routes.ioniaBuyGiftCardPage, arguments: [merchant]); - }, - title: merchant.legalName, - subTitle: merchant.avaibilityStatus, - backgroundColor: Theme.of(context).extension()!.syncedBackgroundColor, - titleColor: Theme.of(context).extension()!.textColor, - subtitleColor: Theme.of(context).extension()!.labelTextColor, - discount: merchant.discount, - ); - }, - ), - isAlwaysShowScrollThumb - ? CakeScrollbar( - backgroundHeight: backgroundHeight, - thumbHeight: thumbHeight, - rightOffset: 1, - width: 3, - backgroundColor: Theme.of(context).extension()!.iconColor.withOpacity(0.05), - thumbColor: Theme.of(context).extension()!.iconColor.withOpacity(0.5), - fromTop: widget.cardsListViewModel.scrollOffsetFromTop, - ) - : Offstage() - ]); - } - return Center( - child: CircularProgressIndicator( - backgroundColor: Theme.of(context).extension()!.textColor, - valueColor: AlwaysStoppedAnimation(Theme.of(context).extension()!.firstGradientBottomPanelColor), + GridView.builder( + controller: _scrollController, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1, + childAspectRatio: 5, + crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5, + mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5, + ), + padding: EdgeInsets.only(left: 2, right: 22), + itemCount: vendors.length + (isLoadingMore ? 1 : 0), + itemBuilder: (_, index) { + if (index >= vendors.length) { + return _VendorLoadedIndicator(); + } + final vendor = vendors[index]; + return CardItem( + logoUrl: vendor.card?.cardImageUrl, + onTap: () { + Navigator.of(context).pushNamed(Routes.cakePayBuyCardPage, arguments: [vendor]); + }, + title: vendor.name, + subTitle: vendor.card?.description ?? '', + backgroundColor: + Theme.of(context).extension()!.syncedBackgroundColor, + titleColor: Theme.of(context).extension()!.textColor, + subtitleColor: Theme.of(context).extension()!.labelTextColor, + discount: 0.0, + ); + }, ), - ); + isAlwaysShowScrollThumb + ? CakeScrollbar( + backgroundHeight: backgroundHeight, + thumbHeight: thumbHeight, + rightOffset: 1, + width: 3, + backgroundColor: + Theme.of(context).extension()!.iconColor.withOpacity(0.05), + thumbColor: + Theme.of(context).extension()!.iconColor.withOpacity(0.5), + fromTop: widget.cardsListViewModel.scrollOffsetFromTop, + ) + : Offstage() + ]); } + return _VendorLoadedIndicator(); + }); + } +} + +class _VendorLoadedIndicator extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: CircularProgressIndicator( + backgroundColor: Theme.of(context).extension()!.textColor, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).extension()!.firstGradientBottomPanelColor), + ), ); } } @@ -233,6 +273,7 @@ class _SearchWidget extends StatelessWidget { }) : super(key: key); final TextEditingController controller; final FocusNode focusNode; + @override Widget build(BuildContext context) { final searchIcon = ExcludeSemantics( @@ -284,30 +325,25 @@ class _SearchWidget extends StatelessWidget { } class _TrailingIcon extends StatelessWidget { - const _TrailingIcon({required this.asset, this.onPressed}); + const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor}); final String asset; final VoidCallback? onPressed; + final Color iconColor; @override Widget build(BuildContext context) { return Semantics( - label: S.of(context).profile, - child: Material( - color: Colors.transparent, - child: IconButton( - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - highlightColor: Colors.transparent, - splashColor: Colors.transparent, - iconSize: 25, - onPressed: onPressed, - icon: Image.asset( - asset, - color: Theme.of(context).extension()!.textColor, + label: S.of(context).profile, + child: Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + highlightColor: Colors.transparent, + onPressed: onPressed, + icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor), ), - ), - ), - ); + )); } } diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart new file mode 100644 index 000000000..15aa576c8 --- /dev/null +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -0,0 +1,403 @@ +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart'; +import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/picker_theme.dart'; +import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_purchase_view_model.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; + +class CakePayBuyCardDetailPage extends BasePage { + CakePayBuyCardDetailPage(this.cakePayPurchaseViewModel); + + final CakePayPurchaseViewModel cakePayPurchaseViewModel; + + @override + String get title => cakePayPurchaseViewModel.card.name; + + @override + Widget? middle(BuildContext context) { + return Text( + title, + textAlign: TextAlign.center, + maxLines: 2, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: titleColor(context)), + ); + } + + @override + Widget? trailing(BuildContext context) => null; + + bool _effectsInstalled = false; + + @override + Widget body(BuildContext context) { + _setEffects(context); + + final card = cakePayPurchaseViewModel.card; + + return ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Observer(builder: (_) { + return Column( + children: [ + SizedBox(height: 36), + ClipRRect( + borderRadius: + BorderRadius.horizontal(left: Radius.circular(20), right: Radius.circular(20)), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.searchBackgroundFillColor, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withOpacity(0.20)), + ), + child: Row( + children: [ + Expanded( + child: Container( + child: ClipRRect( + borderRadius: BorderRadius.horizontal( + left: Radius.circular(20), right: Radius.circular(20)), + child: Image.network( + card.cardImageUrl ?? '', + fit: BoxFit.cover, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) => + CakePayCardImagePlaceholder(), + ), + )), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column(children: [ + Row( + children: [ + Text( + S.of(context).value + ':', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + SizedBox(width: 8), + Text( + '${cakePayPurchaseViewModel.amount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + ], + ), + SizedBox(height: 16), + Row( + children: [ + Text( + S.of(context).quantity + ':', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + SizedBox(width: 8), + Text( + '${cakePayPurchaseViewModel.quantity}', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + ], + ), + SizedBox(height: 16), + Row( + children: [ + Text( + S.of(context).total + ':', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + SizedBox(width: 8), + Text( + '${cakePayPurchaseViewModel.totalAmount.toStringAsFixed(2)} ${cakePayPurchaseViewModel.fiatCurrency}', + style: textLarge( + color: + Theme.of(context).extension()!.titleColor), + ), + ], + ), + ]), + ), + ) + ], + ), + ), + ), + SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: TextIconButton( + label: S.of(context).how_to_use_card, + onTap: () => _showHowToUseCard(context, card), + ), + ), + SizedBox(height: 20), + if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).expiry_and_validity + ':', + style: textMediumSemiBold( + color: Theme.of(context).extension()!.titleColor)), + SizedBox(height: 10), + Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).extension()!.secondaryTextColor, + width: 1, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + card.expiryAndValidity ?? '', + style: textMedium( + color: Theme.of(context).extension()!.secondaryTextColor, + ), + ), + ), + ), + ], + ), + ), + ], + ); + }), + bottomSection: Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 12), + child: Observer(builder: (_) { + return LoadingPrimaryButton( + isLoading: cakePayPurchaseViewModel.sendViewModel.state is IsExecutingState, + onPressed: () => purchaseCard(context), + text: S.of(context).purchase_gift_card, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + ); + }), + ), + SizedBox(height: 8), + InkWell( + onTap: () => _showTermsAndCondition(context, card.termsAndConditions), + child: Text(S.of(context).settings_terms_and_conditions, + style: textMediumSemiBold( + color: Theme.of(context).primaryColor, + ).copyWith(fontSize: 12)), + ), + SizedBox(height: 16) + ], + ), + ); + } + + void _showTermsAndCondition(BuildContext context, String? termsAndConditions) { + showPopUp( + context: context, + builder: (BuildContext context) { + return CakePayAlertModal( + title: S.of(context).settings_terms_and_conditions, + content: Align( + alignment: Alignment.bottomLeft, + child: ClickableLinksText( + text: termsAndConditions ?? '', + textStyle: TextStyle( + color: Theme.of(context).extension()!.secondaryTextColor, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + ), + ), + actionTitle: S.of(context).agree, + showCloseButton: false, + heightFactor: 0.6, + ); + }); + } + + Future purchaseCard(BuildContext context) async { + bool isLogged = await cakePayPurchaseViewModel.cakePayService.isLogged(); + if (!isLogged) { + Navigator.of(context).pushNamed(Routes.cakePayWelcomePage); + } else { + await cakePayPurchaseViewModel.createOrder(); + } + } + + void _showHowToUseCard( + BuildContext context, + CakePayCard card, + ) { + showPopUp( + context: context, + builder: (BuildContext context) { + return CakePayAlertModal( + title: S.of(context).how_to_use_card, + content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: EdgeInsets.all(10), + child: Text( + card.name, + style: textLargeSemiBold( + color: Theme.of(context).extension()!.tilesTextColor, + ), + )), + ClickableLinksText( + text: card.howToUse ?? '', + textStyle: TextStyle( + color: Theme.of(context).extension()!.secondaryTextColor, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + linkStyle: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 18, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w400, + ), + ), + ]), + actionTitle: S.current.got_it, + ); + }); + } + + Future _showConfirmSendingAlert(BuildContext context) async { + if (cakePayPurchaseViewModel.order == null) { + return; + } + ReactionDisposer? disposer; + + disposer = reaction((_) => cakePayPurchaseViewModel.isOrderExpired, (bool isExpired) { + if (isExpired) { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + if (disposer != null) { + disposer(); + } + } + }); + + final order = cakePayPurchaseViewModel.order; + final pendingTransaction = cakePayPurchaseViewModel.sendViewModel.pendingTransaction!; + + await showPopUp( + context: context, + builder: (_) { + return Observer( + builder: (_) => ConfirmSendingAlert( + alertTitle: S.of(context).confirm_sending, + paymentId: S.of(context).payment_id, + paymentIdValue: order?.orderId, + expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, + onDispose: () => _handleDispose(disposer), + amount: S.of(context).send_amount, + amountValue: pendingTransaction.amountFormatted, + fiatAmountValue: + cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(context).send_fee, + feeValue: pendingTransaction.feeFormatted, + feeFiatAmount: + cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + feeRate: pendingTransaction.feeRate, + outputs: cakePayPurchaseViewModel.sendViewModel.outputs, + rightButtonText: S.of(context).send, + leftButtonText: S.of(context).cancel, + actionRightButton: () async { + Navigator.of(context).pop(); + await cakePayPurchaseViewModel.sendViewModel.commitTransaction(); + }, + actionLeftButton: () => Navigator.of(context).pop())); + }, + ); + } + + void _setEffects(BuildContext context) { + if (_effectsInstalled) { + return; + } + + reaction((_) => cakePayPurchaseViewModel.sendViewModel.state, (ExecutionState state) { + if (state is FailureState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showStateAlert(context, S.of(context).error, state.error); + }); + } + + if (state is ExecutedSuccessfullyState) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _showConfirmSendingAlert(context); + }); + } + + if (state is TransactionCommitted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + cakePayPurchaseViewModel.sendViewModel.clearOutputs(); + if (context.mounted) { + showStateAlert(context, S.of(context).sending, S.of(context).transaction_sent); + } + }); + } + }); + + _effectsInstalled = true; + } + + void showStateAlert(BuildContext context, String title, String content) { + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: title, + alertContent: content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + } + + void _handleDispose(ReactionDisposer? disposer) { + cakePayPurchaseViewModel.dispose(); + if (disposer != null) { + disposer(); + } + } +} diff --git a/lib/src/screens/ionia/widgets/ionia_alert_model.dart b/lib/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart similarity index 97% rename from lib/src/screens/ionia/widgets/ionia_alert_model.dart rename to lib/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart index 57a93a127..742ba1486 100644 --- a/lib/src/screens/ionia/widgets/ionia_alert_model.dart +++ b/lib/src/screens/cake_pay/widgets/cake_pay_alert_modal.dart @@ -5,8 +5,8 @@ import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; import 'package:cake_wallet/typography.dart'; import 'package:flutter/material.dart'; -class IoniaAlertModal extends StatelessWidget { - const IoniaAlertModal({ +class CakePayAlertModal extends StatelessWidget { + const CakePayAlertModal({ Key? key, required this.title, required this.content, diff --git a/lib/src/screens/ionia/widgets/ionia_tile.dart b/lib/src/screens/cake_pay/widgets/cake_pay_tile.dart similarity index 94% rename from lib/src/screens/ionia/widgets/ionia_tile.dart rename to lib/src/screens/cake_pay/widgets/cake_pay_tile.dart index 932674451..a26786ab2 100644 --- a/lib/src/screens/ionia/widgets/ionia_tile.dart +++ b/lib/src/screens/cake_pay/widgets/cake_pay_tile.dart @@ -3,8 +3,8 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; -class IoniaTile extends StatelessWidget { - const IoniaTile({ +class CakePayTile extends StatelessWidget { + const CakePayTile({ Key? key, required this.title, required this.subTitle, diff --git a/lib/src/screens/cake_pay/widgets/card_item.dart b/lib/src/screens/cake_pay/widgets/card_item.dart new file mode 100644 index 000000000..ce804adc2 --- /dev/null +++ b/lib/src/screens/cake_pay/widgets/card_item.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import 'image_placeholder.dart'; + +class CardItem extends StatelessWidget { + CardItem({ + required this.title, + required this.subTitle, + required this.backgroundColor, + required this.titleColor, + required this.subtitleColor, + this.hideBorder = false, + this.discount = 0.0, + this.isAmount = false, + this.discountBackground, + this.onTap, + this.logoUrl, + }); + + final VoidCallback? onTap; + final String title; + final String subTitle; + final String? logoUrl; + final double discount; + final bool isAmount; + final bool hideBorder; + final Color backgroundColor; + final Color titleColor; + final Color subtitleColor; + final AssetImage? discountBackground; + + @override + Widget build(BuildContext context) { + return Theme( + data: ThemeData( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: InkWell( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(10), + border: hideBorder + ? Border.all(color: Colors.transparent) + : Border.all(color: Colors.white.withOpacity(0.20)), + ), + child: Row( + children: [ + if (logoUrl != null) + AspectRatio( + aspectRatio: 1.8, + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(10)), + child: Image.network( + logoUrl!, + fit: BoxFit.cover, + loadingBuilder: + (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) return child; + return Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + Text( + subTitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontSize: 10, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/screens/ionia/widgets/card_menu.dart b/lib/src/screens/cake_pay/widgets/card_menu.dart similarity index 100% rename from lib/src/screens/ionia/widgets/card_menu.dart rename to lib/src/screens/cake_pay/widgets/card_menu.dart diff --git a/lib/src/screens/cake_pay/widgets/image_placeholder.dart b/lib/src/screens/cake_pay/widgets/image_placeholder.dart new file mode 100644 index 000000000..e389bab07 --- /dev/null +++ b/lib/src/screens/cake_pay/widgets/image_placeholder.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class CakePayCardImagePlaceholder extends StatelessWidget { + const CakePayCardImagePlaceholder({this.text}); + + final String? text; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.8, + child: Container( + child: Center( + child: Text( + text ?? 'Image not found!', + style: TextStyle( + color: Colors.black, + fontSize: 12, + fontWeight: FontWeight.w900, + ), + ), + ), + decoration: BoxDecoration( + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/src/screens/cake_pay/widgets/link_extractor.dart b/lib/src/screens/cake_pay/widgets/link_extractor.dart new file mode 100644 index 000000000..43b4a3e52 --- /dev/null +++ b/lib/src/screens/cake_pay/widgets/link_extractor.dart @@ -0,0 +1,66 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ClickableLinksText extends StatelessWidget { + const ClickableLinksText({ + required this.text, + required this.textStyle, + this.linkStyle, + }); + + final String text; + final TextStyle textStyle; + final TextStyle? linkStyle; + + @override + Widget build(BuildContext context) { + List spans = []; + RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); + Iterable matches = linkRegExp.allMatches(text); + + int previousEnd = 0; + matches.forEach((match) { + if (match.start > previousEnd) { + spans.add(TextSpan(text: text.substring(previousEnd, match.start), style: textStyle)); + } + String url = text.substring(match.start, match.end); + if (url.toLowerCase().endsWith('.md')) { + spans.add( + TextSpan( + text: url, + style: TextStyle( + color: Colors.blue, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + recognizer: TapGestureRecognizer() + ..onTap = () async { + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } + }, + ), + ); + } else { + spans.add( + TextSpan( + text: url, + style: linkStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl(Uri.parse(url)); + }, + ), + ); + } + previousEnd = match.end; + }); + + if (previousEnd < text.length) { + spans.add(TextSpan(text: text.substring(previousEnd), style: textStyle)); + } + + return RichText(text: TextSpan(children: spans)); + } +} diff --git a/lib/src/screens/ionia/widgets/rounded_checkbox.dart b/lib/src/screens/cake_pay/widgets/rounded_checkbox.dart similarity index 100% rename from lib/src/screens/ionia/widgets/rounded_checkbox.dart rename to lib/src/screens/cake_pay/widgets/rounded_checkbox.dart diff --git a/lib/src/screens/ionia/widgets/text_icon_button.dart b/lib/src/screens/cake_pay/widgets/text_icon_button.dart similarity index 100% rename from lib/src/screens/ionia/widgets/text_icon_button.dart rename to lib/src/screens/cake_pay/widgets/text_icon_button.dart diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index 9ccb7833c..b034fb799 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -1,17 +1,18 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CakeFeaturesPage extends StatelessWidget { - CakeFeaturesPage({ - required this.dashboardViewModel, - required this.cakeFeaturesViewModel, - }); + CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel}); final DashboardViewModel dashboardViewModel; final CakeFeaturesViewModel cakeFeaturesViewModel; @@ -45,20 +46,11 @@ class CakeFeaturesPage extends StatelessWidget { child: ListView( controller: _scrollController, children: [ - // SizedBox(height: 20), - // DashBoardRoundedCardWidget( - // onTap: () => launchUrl( - // Uri.parse("https://cakelabs.com/news/cake-pay-mobile-to-shut-down/"), - // mode: LaunchMode.externalApplication, - // ), - // title: S.of(context).cake_pay_title, - // subTitle: S.of(context).cake_pay_subtitle, - // ), SizedBox(height: 20), DashBoardRoundedCardWidget( - onTap: () => _launchUrl("buy.cakepay.com"), - title: S.of(context).cake_pay_web_cards_title, - subTitle: S.of(context).cake_pay_web_cards_subtitle, + onTap: () => _navigatorToGiftCardsPage(context), + title: 'Cake Pay', + subTitle: S.of(context).cake_pay_subtitle, svgPicture: SvgPicture.asset( 'assets/images/cards.svg', height: 125, @@ -88,6 +80,28 @@ class CakeFeaturesPage extends StatelessWidget { Uri.https(url), mode: LaunchMode.externalApplication, ); - } catch (_) {} + } catch (e) { + print(e); + } + } + + void _navigatorToGiftCardsPage(BuildContext context) { + final walletType = dashboardViewModel.type; + + switch (walletType) { + case WalletType.haven: + showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).gift_cards_unavailable, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + break; + default: + Navigator.pushNamed(context, Routes.cakePayCardsPage); + } } } diff --git a/lib/src/screens/dashboard/widgets/filter_widget.dart b/lib/src/screens/dashboard/widgets/filter_widget.dart index ec867ae49..eaf00a1de 100644 --- a/lib/src/screens/dashboard/widgets/filter_widget.dart +++ b/lib/src/screens/dashboard/widgets/filter_widget.dart @@ -3,18 +3,21 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/filter_tile.dart'; import 'package:cake_wallet/src/widgets/section_divider.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; -import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item.dart'; +import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item_widget.dart'; +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; + //import 'package:date_range_picker/date_range_picker.dart' as date_rage_picker; import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; class FilterWidget extends StatelessWidget { - FilterWidget({required this.dashboardViewModel}); + FilterWidget({required this.filterItems}); - final DashboardViewModel dashboardViewModel; + final Map> filterItems; @override Widget build(BuildContext context) { @@ -27,75 +30,90 @@ class FilterWidget extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(24)), child: Container( color: Theme.of(context).extension()!.backgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(24.0), - child: Text( - S.of(context).filter_by, - style: TextStyle( - color: Theme.of(context).extension()!.detailsTitlesColor, - fontSize: 16, - fontFamily: 'Lato', - decoration: TextDecoration.none, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: EdgeInsets.all(24.0), + child: Text( + S.of(context).filter_by, + style: TextStyle( + color: + Theme.of(context).extension()!.detailsTitlesColor, + fontSize: 16, + fontFamily: 'Lato', + decoration: TextDecoration.none, + ), + ), + ), + sectionDivider, + ListView.separated( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: filterItems.length, + separatorBuilder: (context, _) => sectionDivider, + itemBuilder: (_, index1) { + final title = filterItems.keys.elementAt(index1); + final section = filterItems.values.elementAt(index1); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 20, left: 24, right: 24), + child: Text( + title, + style: TextStyle( + color: Theme.of(context).extension()!.titleColor, + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.bold, + decoration: TextDecoration.none), + ), ), - ), - ), - sectionDivider, - ListView.separated( - padding: EdgeInsets.zero, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: dashboardViewModel.filterItems.length, - separatorBuilder: (context, _) => sectionDivider, - itemBuilder: (_, index1) { - final title = dashboardViewModel.filterItems.keys - .elementAt(index1); - final section = dashboardViewModel.filterItems.values - .elementAt(index1); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: - EdgeInsets.only(top: 20, left: 24, right: 24), - child: Text( - title, - style: TextStyle( - color: Theme.of(context).extension()!.titleColor, - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.bold, - decoration: TextDecoration.none), - ), - ), - ListView.builder( - padding: EdgeInsets.symmetric(vertical: 8.0), - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: section.length, - itemBuilder: (_, index2) { - final item = section[index2]; - final content = Observer( - builder: (_) => StandardCheckbox( - value: item.value(), - caption: item.caption, - gradientBackground: true, - borderColor: - Theme.of(context).dividerColor, - iconColor: Colors.white, - onChanged: (value) => - item.onChanged(), - )); - return FilterTile(child: content); - }, - ) - ], - ); - }, - ), - ]), + ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 28.0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: section.length, + itemBuilder: (_, index2) { + final item = section[index2]; + + if (item is DropdownFilterItem) { + return Padding( + padding: EdgeInsets.fromLTRB(8, 0, 8, 16), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1.0, + color: Theme.of(context).extension()!.secondaryTextColor), + ), + ), + child: DropdownFilterList( + items: item.items, + caption: item.caption, + selectedItem: item.selectedItem, + onItemSelected: item.onItemSelected, + ), + ), + ); + } + final content = Observer( + builder: (_) => StandardCheckbox( + value: item.value(), + caption: item.caption, + gradientBackground: true, + borderColor: Theme.of(context).dividerColor, + iconColor: Colors.white, + onChanged: (value) => item.onChanged(), + )); + return FilterTile(child: content); + }, + ) + ], + ); + }, + ), + ]), ), ), ) diff --git a/lib/src/screens/dashboard/widgets/header_row.dart b/lib/src/screens/dashboard/widgets/header_row.dart index 2093a238f..cb4f67fc2 100644 --- a/lib/src/screens/dashboard/widgets/header_row.dart +++ b/lib/src/screens/dashboard/widgets/header_row.dart @@ -37,7 +37,7 @@ class HeaderRow extends StatelessWidget { onTap: () { showPopUp( context: context, - builder: (context) => FilterWidget(dashboardViewModel: dashboardViewModel), + builder: (context) => FilterWidget(filterItems: dashboardViewModel.filterItems), ); }, child: Semantics( diff --git a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart index bebb58107..3abcbbfeb 100644 --- a/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart +++ b/lib/src/screens/dashboard/widgets/present_receive_option_picker.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart'; +import 'package:cake_wallet/src/screens/cake_pay/widgets/rounded_checkbox.dart'; import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/typography.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; diff --git a/lib/src/screens/ionia/auth/ionia_create_account_page.dart b/lib/src/screens/ionia/auth/ionia_create_account_page.dart deleted file mode 100644 index e6dc83c3c..000000000 --- a/lib/src/screens/ionia/auth/ionia_create_account_page.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/core/email_validator.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class IoniaCreateAccountPage extends BasePage { - IoniaCreateAccountPage(this._authViewModel) - : _emailFocus = FocusNode(), - _emailController = TextEditingController(), - _formKey = GlobalKey() { - _emailController.text = _authViewModel.email; - _emailController.addListener(() => _authViewModel.email = _emailController.text); - } - - final IoniaAuthViewModel _authViewModel; - - final GlobalKey _formKey; - - final FocusNode _emailFocus; - final TextEditingController _emailController; - - static const privacyPolicyUrl = 'https://ionia.docsend.com/view/jhjvdn7qq7k3ukwt'; - static const termsAndConditionsUrl = 'https://ionia.docsend.com/view/uceirymz2ijacq5g'; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.sign_up, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - reaction((_) => _authViewModel.createUserState, (IoniaCreateAccountState state) { - if (state is IoniaCreateStateFailure) { - _onCreateUserFailure(context, state.error); - } - if (state is IoniaCreateStateSuccess) { - _onCreateSuccessful(context, _authViewModel); - } - }); - - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Form( - key: _formKey, - child: BaseTextFormField( - hintText: S.of(context).email_address, - focusNode: _emailFocus, - validator: EmailValidator(), - keyboardType: TextInputType.emailAddress, - controller: _emailController, - onSubmit: (_) => _createAccount(), - ), - ), - bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24), - bottomSection: Column( - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Observer( - builder: (_) => LoadingPrimaryButton( - text: S.of(context).create_account, - onPressed: _createAccount, - isLoading: - _authViewModel.createUserState is IoniaCreateStateLoading, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ), - SizedBox( - height: 20, - ), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - text: S.of(context).agree_to, - style: TextStyle( - color: Color(0xff7A93BA), - fontSize: 12, - fontFamily: 'Lato', - ), - children: [ - TextSpan( - text: S.of(context).settings_terms_and_conditions, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w700, - ), - recognizer: TapGestureRecognizer() - ..onTap = () async { - if (await canLaunch(termsAndConditionsUrl)) await launch(termsAndConditionsUrl); - }, - ), - TextSpan(text: ' ${S.of(context).and} '), - TextSpan( - text: S.of(context).privacy_policy, - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w700, - ), - recognizer: TapGestureRecognizer() - ..onTap = () async { - if (await canLaunch(privacyPolicyUrl)) await launch(privacyPolicyUrl); - }), - TextSpan(text: ' ${S.of(context).by_cake_pay}'), - ], - ), - ), - ], - ), - ], - ), - ); - } - - void _onCreateUserFailure(BuildContext context, String error) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.create_account, - alertContent: error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - - void _onCreateSuccessful(BuildContext context, IoniaAuthViewModel authViewModel) => Navigator.pushNamed( - context, - Routes.ioniaVerifyIoniaOtpPage, - arguments: [authViewModel.email, false], - ); - - void _createAccount() async { - if (_formKey.currentState != null && !_formKey.currentState!.validate()) { - return; - } - await _authViewModel.createUser(_emailController.text); - } -} diff --git a/lib/src/screens/ionia/auth/ionia_welcome_page.dart b/lib/src/screens/ionia/auth/ionia_welcome_page.dart deleted file mode 100644 index e44e3a26d..000000000 --- a/lib/src/screens/ionia/auth/ionia_welcome_page.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -class IoniaWelcomePage extends BasePage { - IoniaWelcomePage(); - - @override - Widget middle(BuildContext context) { - return Text( - S.current.welcome_to_cakepay, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - children: [ - SizedBox(height: 90), - Text( - S.of(context).about_cake_pay, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - fontFamily: 'Lato', - color: Theme.of(context).extension()!.titleColor, - ), - ), - SizedBox(height: 20), - Text( - S.of(context).cake_pay_account_note, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - fontFamily: 'Lato', - color: Theme.of(context).extension()!.titleColor, - ), - ), - ], - ), - Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - PrimaryButton( - text: S.of(context).create_account, - onPressed: () => Navigator.of(context).pushNamed(Routes.ioniaCreateAccountPage), - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - SizedBox( - height: 16, - ), - Text( - S.of(context).already_have_account, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - fontFamily: 'Lato', - color: Theme.of(context).extension()!.titleColor, - ), - ), - SizedBox(height: 8), - InkWell( - onTap: () => Navigator.of(context).pushNamed(Routes.ioniaLoginPage), - child: Text( - S.of(context).login, - style: TextStyle( - color: Palette.blueCraiola, - fontSize: 18, - letterSpacing: 1.5, - fontWeight: FontWeight.w900, - ), - ), - ), - SizedBox(height: 20) - ], - ) - ], - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart b/lib/src/screens/ionia/cards/ionia_account_cards_page.dart deleted file mode 100644 index b96249b69..000000000 --- a/lib/src/screens/ionia/cards/ionia_account_cards_page.dart +++ /dev/null @@ -1,204 +0,0 @@ - -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; -import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/order_theme.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; - -class IoniaAccountCardsPage extends BasePage { - IoniaAccountCardsPage(this.ioniaAccountViewModel); - - final IoniaAccountViewModel ioniaAccountViewModel; - - @override - Widget middle(BuildContext context) { - return Text( - S.of(context).cards, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - return _IoniaCardTabs(ioniaAccountViewModel); - } -} - -class _IoniaCardTabs extends StatefulWidget { - _IoniaCardTabs(this.ioniaAccountViewModel); - - final IoniaAccountViewModel ioniaAccountViewModel; - - @override - _IoniaCardTabsState createState() => _IoniaCardTabsState(); -} - -class _IoniaCardTabsState extends State<_IoniaCardTabs> with SingleTickerProviderStateMixin { - _IoniaCardTabsState(); - - TabController? _tabController; - - @override - void initState() { - _tabController = TabController(length: 2, vsync: this); - super.initState(); - } - - @override - void dispose() { - super.dispose(); - _tabController?.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 45, - width: 230, - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - color: Theme.of(context).extension()!.titleColor - .withOpacity(0.1), - borderRadius: BorderRadius.circular( - 25.0, - ), - ), - child: Theme( - data: ThemeData(primaryTextTheme: TextTheme(bodyLarge: TextStyle(backgroundColor: Colors.transparent))), - child: TabBar( - controller: _tabController, - indicator: BoxDecoration( - borderRadius: BorderRadius.circular( - 25.0, - ), - color: Theme.of(context).primaryColor, - ), - labelColor: Theme.of(context).extension()!.iconColor, - unselectedLabelColor: - Theme.of(context).extension()!.titleColor, - tabs: [ - Tab( - text: S.of(context).active, - ), - Tab( - text: S.of(context).redeemed, - ), - ], - ), - ), - ), - SizedBox(height: 16), - Expanded( - child: Observer(builder: (_) { - final viewModel = widget.ioniaAccountViewModel; - return TabBarView( - controller: _tabController, - children: [ - _IoniaCardListView( - emptyText: S.of(context).gift_card_balance_note, - merchList: viewModel.activeMechs, - isLoading: viewModel.merchantState is IoniaLoadingMerchantState, - onTap: (giftCard) { - Navigator.pushNamed( - context, - Routes.ioniaGiftCardDetailPage, - arguments: [giftCard]) - .then((_) => viewModel.updateUserGiftCards()); - }), - _IoniaCardListView( - emptyText: S.of(context).gift_card_redeemed_note, - merchList: viewModel.redeemedMerchs, - isLoading: viewModel.merchantState is IoniaLoadingMerchantState, - onTap: (giftCard) { - Navigator.pushNamed( - context, - Routes.ioniaGiftCardDetailPage, - arguments: [giftCard]) - .then((_) => viewModel.updateUserGiftCards()); - }), - ], - ); - }), - ), - ], - ), - ); - } -} - -class _IoniaCardListView extends StatelessWidget { - _IoniaCardListView({ - Key? key, - required this.emptyText, - required this.merchList, - required this.onTap, - this.isLoading = false, - }) : super(key: key); - - final String emptyText; - final List merchList; - final void Function(IoniaGiftCard giftCard) onTap; - final bool isLoading; - - @override - Widget build(BuildContext context) { - if(isLoading){ - return Center( - child: CircularProgressIndicator( - backgroundColor: Theme.of(context).extension()!.textColor, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).extension()!.firstGradientBottomPanelColor), - ), - ); - } - return merchList.isEmpty - ? Center( - child: Text( - emptyText, - textAlign: TextAlign.center, - style: textSmall( - color: Theme.of(context).extension()!.detailsTitlesColor, - ), - ), - ) - : ListView.builder( - itemCount: merchList.length, - itemBuilder: (context, index) { - final merchant = merchList[index]; - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: CardItem( - onTap: () => onTap?.call(merchant), - title: merchant.legalName, - backgroundColor: Theme.of(context).extension()!.titleColor - .withOpacity(0.1), - discount: 0, - hideBorder: true, - discountBackground: AssetImage('assets/images/red_badge_discount.png'), - titleColor: Theme.of(context).extension()!.titleColor, - subtitleColor: Theme.of(context).hintColor, - subTitle: '', - logoUrl: merchant.logoUrl, - ), - ); - }, - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_account_page.dart b/lib/src/screens/ionia/cards/ionia_account_page.dart deleted file mode 100644 index 8fddc507a..000000000 --- a/lib/src/screens/ionia/cards/ionia_account_page.dart +++ /dev/null @@ -1,178 +0,0 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_account_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; - -class IoniaAccountPage extends BasePage { - IoniaAccountPage(this.ioniaAccountViewModel); - - final IoniaAccountViewModel ioniaAccountViewModel; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.account, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - children: [ - _GradiantContainer( - content: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Observer( - builder: (_) => RichText( - text: TextSpan( - text: '${ioniaAccountViewModel.countOfMerch}', - style: textLargeSemiBold(), - children: [ - TextSpan( - text: ' ${S.of(context).active_cards}', - style: textSmall(color: Colors.white.withOpacity(0.7))), - ], - ), - )), - InkWell( - onTap: () { - Navigator.pushNamed(context, Routes.ioniaAccountCardsPage) - .then((_) => ioniaAccountViewModel.updateUserGiftCards()); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - S.of(context).view_all, - style: textSmallSemiBold(), - ), - ), - ) - ], - ), - ), - SizedBox(height: 8), - //Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // _GradiantContainer( - // padding: EdgeInsets.all(16), - // width: deviceWidth * 0.28, - // content: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // S.of(context).total_saving, - // style: textSmall(), - // ), - // SizedBox(height: 8), - // Text( - // '\$100', - // style: textMediumSemiBold(), - // ), - // ], - // ), - // ), - // _GradiantContainer( - // padding: EdgeInsets.all(16), - // width: deviceWidth * 0.28, - // content: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // S.of(context).last_30_days, - // style: textSmall(), - // ), - // SizedBox(height: 8), - // Text( - // '\$100', - // style: textMediumSemiBold(), - // ), - // ], - // ), - // ), - // _GradiantContainer( - // padding: EdgeInsets.all(16), - // width: deviceWidth * 0.28, - // content: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text( - // S.of(context).avg_savings, - // style: textSmall(), - // ), - // SizedBox(height: 8), - // Text( - // '10%', - // style: textMediumSemiBold(), - // ), - // ], - // ), - // ), - // ], - //), - SizedBox(height: 40), - Observer( - builder: (_) => IoniaTile(title: S.of(context).email_address, subTitle: ioniaAccountViewModel.email ?? ''), - ), - Divider() - ], - ), - bottomSectionPadding: EdgeInsets.all(30), - bottomSection: Column( - children: [ - PrimaryButton( - color: Theme.of(context).primaryColor, - textColor: Colors.white, - text: S.of(context).logout, - onPressed: () { - ioniaAccountViewModel.logout(); - Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); - }, - ), - ], - ), - ); - } -} - -class _GradiantContainer extends StatelessWidget { - const _GradiantContainer({ - Key? key, - required this.content, - }) : super(key: key); - - final Widget content; - - @override - Widget build(BuildContext context) { - return Container( - child: content, - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.secondGradientColor, - Theme.of(context).extension()!.firstGradientColor, - ], - begin: Alignment.topRight, - end: Alignment.bottomLeft, - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart deleted file mode 100644 index f0e641c42..000000000 --- a/lib/src/screens/ionia/cards/ionia_activate_debit_card_page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:mobx/mobx.dart'; - -class IoniaActivateDebitCardPage extends BasePage { - - IoniaActivateDebitCardPage(this._cardsListViewModel); - - final IoniaGiftCardsListViewModel _cardsListViewModel; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.debit_card, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - reaction((_) => _cardsListViewModel.createCardState, (IoniaCreateCardState state) { - if (state is IoniaCreateCardFailure) { - _onCreateCardFailure(context, state.error); - } - if (state is IoniaCreateCardSuccess) { - _onCreateCardSuccess(context); - } - }); - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SizedBox(height: 16), - Text(S.of(context).debit_card_terms), - SizedBox(height: 24), - Text(S.of(context).please_reference_document), - SizedBox(height: 40), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - children: [ - TextIconButton( - label: S.current.cardholder_agreement, - onTap: () {}, - ), - SizedBox( - height: 24, - ), - TextIconButton( - label: S.current.e_sign_consent, - onTap: () {}, - ), - ], - ), - ), - ], - ), - ), - bottomSection: LoadingPrimaryButton( - onPressed: () { - _cardsListViewModel.createCard(); - }, - isLoading: _cardsListViewModel.createCardState is IoniaCreateCardLoading, - text: S.of(context).agree_and_continue, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ); - } - - void _onCreateCardFailure(BuildContext context, String errorMessage) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.current.error, - alertContent: errorMessage, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } - - void _onCreateCardSuccess(BuildContext context) { - Navigator.pushNamed( - context, - Routes.ioniaDebitCardPage, - ); - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).congratulations, - alertContent: S.of(context).you_now_have_debit_card, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop(), - ); - }, - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart deleted file mode 100644 index 917c1d8fd..000000000 --- a/lib/src/screens/ionia/cards/ionia_buy_card_detail_page.dart +++ /dev/null @@ -1,478 +0,0 @@ -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_tip.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/discount_badge.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_purchase_merch_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; - -class IoniaBuyGiftCardDetailPage extends BasePage { - IoniaBuyGiftCardDetailPage(this.ioniaPurchaseViewModel); - - final IoniaMerchPurchaseViewModel ioniaPurchaseViewModel; - - @override - Widget middle(BuildContext context) { - return Text( - ioniaPurchaseViewModel.ioniaMerchant.legalName, - style: textMediumSemiBold(color: Theme.of(context).extension()!.titleColor), - ); - } - - @override - Widget? trailing(BuildContext context) - => ioniaPurchaseViewModel.ioniaMerchant.discount > 0 - ? DiscountBadge(percentage: ioniaPurchaseViewModel.ioniaMerchant.discount) - : null; - - @override - Widget body(BuildContext context) { - reaction((_) => ioniaPurchaseViewModel.invoiceCreationState, (ExecutionState state) { - if (state is FailureState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - }); - } - }); - - reaction((_) => ioniaPurchaseViewModel.invoiceCommittingState, (ExecutionState state) { - if (state is FailureState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - }); - } - - if (state is ExecutedSuccessfullyState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pushReplacementNamed( - Routes.ioniaPaymentStatusPage, - arguments: [ - ioniaPurchaseViewModel.paymentInfo, - ioniaPurchaseViewModel.committedInfo]); - }); - } - }); - - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Observer(builder: (_) { - final tipAmount = ioniaPurchaseViewModel.tipAmount; - return Column( - children: [ - SizedBox(height: 36), - Container( - padding: EdgeInsets.symmetric(vertical: 24), - margin: EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Column( - children: [ - Text( - S.of(context).gift_card_amount, - style: textSmall(), - ), - SizedBox(height: 4), - Text( - '\$${ioniaPurchaseViewModel.giftCardAmount.toStringAsFixed(2)}', - style: textXLargeSemiBold(), - ), - SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).bill_amount, - style: textSmall(), - ), - SizedBox(height: 4), - Text( - '\$${ioniaPurchaseViewModel.billAmount.toStringAsFixed(2)}', - style: textLargeSemiBold(), - ), - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - S.of(context).tip, - style: textSmall(), - ), - SizedBox(height: 4), - Text( - '\$${tipAmount.toStringAsFixed(2)}', - style: textLargeSemiBold(), - ), - ], - ), - ], - ), - ), - ], - ), - ), - if(ioniaPurchaseViewModel.ioniaMerchant.acceptsTips) - Padding( - padding: const EdgeInsets.fromLTRB(24.0, 24.0, 0, 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).tip, - style: TextStyle( - color: Theme.of(context).extension()!.titleColor, - fontWeight: FontWeight.w700, - fontSize: 14, - ), - ), - SizedBox(height: 4), - Observer( - builder: (_) => TipButtonGroup( - selectedTip: ioniaPurchaseViewModel.selectedTip!.percentage, - tipsList: ioniaPurchaseViewModel.tips, - onSelect: (value) => ioniaPurchaseViewModel.addTip(value), - amount: ioniaPurchaseViewModel.amount, - merchant: ioniaPurchaseViewModel.ioniaMerchant, - ), - ) - ], - ), - ), - SizedBox(height: 20), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: TextIconButton( - label: S.of(context).how_to_use_card, - onTap: () => _showHowToUseCard(context, ioniaPurchaseViewModel.ioniaMerchant), - ), - ), - ], - ); - }), - bottomSection: Column( - children: [ - Padding( - padding: EdgeInsets.only(bottom: 12), - child: Observer(builder: (_) { - return LoadingPrimaryButton( - isLoading: ioniaPurchaseViewModel.invoiceCreationState is IsExecutingState || - ioniaPurchaseViewModel.invoiceCommittingState is IsExecutingState, - onPressed: () => purchaseCard(context), - text: S.of(context).purchase_gift_card, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ); - }), - ), - SizedBox(height: 8), - InkWell( - onTap: () => _showTermsAndCondition(context), - child: Text(S.of(context).settings_terms_and_conditions, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.firstGradientBottomPanelColor, - ).copyWith(fontSize: 12)), - ), - SizedBox(height: 16) - ], - ), - ); - } - - void _showTermsAndCondition(BuildContext context) { - showPopUp( - context: context, - builder: (BuildContext context) { - return IoniaAlertModal( - title: S.of(context).settings_terms_and_conditions, - content: Align( - alignment: Alignment.bottomLeft, - child: Text( - ioniaPurchaseViewModel.ioniaMerchant.termsAndConditions, - style: textMedium( - color: Theme.of(context).extension()!.tilesTextColor, - ), - ), - ), - actionTitle: S.of(context).agree, - showCloseButton: false, - heightFactor: 0.6, - ); - }); - } - - Future purchaseCard(BuildContext context) async { - await ioniaPurchaseViewModel.createInvoice(); - - if (ioniaPurchaseViewModel.invoiceCreationState is ExecutedSuccessfullyState) { - await _presentSuccessfulInvoiceCreationPopup(context); - } - } - - void _showHowToUseCard( - BuildContext context, - IoniaMerchant merchant, - ) { - showPopUp( - context: context, - builder: (BuildContext context) { - return IoniaAlertModal( - title: S.of(context).how_to_use_card, - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: merchant.instructions - .map((instruction) { - return [ - Padding( - padding: EdgeInsets.all(10), - child: Text( - instruction.header, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.tilesTextColor, - ), - )), - Text( - instruction.body, - style: textMedium( - color: Theme.of(context).extension()!.tilesTextColor, - ), - ) - ]; - }) - .expand((e) => e) - .toList()), - actionTitle: S.current.got_it, - ); - }); - } - - Future _presentSuccessfulInvoiceCreationPopup(BuildContext context) async { - if (ioniaPurchaseViewModel.invoice == null) { - return; - } - - final amount = ioniaPurchaseViewModel.invoice!.totalAmount; - final addresses = ioniaPurchaseViewModel.invoice!.outAddresses; - ioniaPurchaseViewModel.sendViewModel.outputs.first.setCryptoAmount(amount); - ioniaPurchaseViewModel.sendViewModel.outputs.first.address = addresses.first; - - await showPopUp( - context: context, - builder: (_) { - return ConfirmSendingAlert( - alertTitle: S.of(context).confirm_sending, - paymentId: S.of(context).payment_id, - paymentIdValue: ioniaPurchaseViewModel.invoice!.paymentId, - amount: S.of(context).send_amount, - amountValue: '$amount ${ioniaPurchaseViewModel.invoice!.chain}', - fiatAmountValue: - '~ ${ioniaPurchaseViewModel.sendViewModel.outputs.first.fiatAmount} ' - '${ioniaPurchaseViewModel.sendViewModel.fiat.title}', - fee: S.of(context).send_fee, - feeValue: - '${ioniaPurchaseViewModel.sendViewModel.outputs.first.estimatedFee} ' - '${ioniaPurchaseViewModel.invoice!.chain}', - feeFiatAmount: - '${ioniaPurchaseViewModel.sendViewModel.outputs.first.estimatedFeeFiatAmount} ' - '${ioniaPurchaseViewModel.sendViewModel.fiat.title}', - outputs: ioniaPurchaseViewModel.sendViewModel.outputs, - rightButtonText: S.of(context).ok, - leftButtonText: S.of(context).cancel, - alertLeftActionButtonTextColor: Colors.white, - alertRightActionButtonTextColor: Colors.white, - alertLeftActionButtonColor: Palette.brightOrange, - alertRightActionButtonColor: Theme.of(context).primaryColor, - actionRightButton: () async { - Navigator.of(context).pop(); - await ioniaPurchaseViewModel.commitPaymentInvoice(); - }, - actionLeftButton: () => Navigator.of(context).pop()); - }, - ); - } -} - -class TipButtonGroup extends StatelessWidget { - const TipButtonGroup({ - Key? key, - required this.selectedTip, - required this.onSelect, - required this.tipsList, - required this.amount, - required this.merchant, - }) : super(key: key); - - final Function(IoniaTip) onSelect; - final double selectedTip; - final List tipsList; - final double amount; - final IoniaMerchant merchant; - - bool _isSelected(double value) => selectedTip == value; - Set get filter => tipsList.map((e) => e.percentage).toSet(); - bool get _isCustomSelected => !filter.contains(selectedTip); - - @override - Widget build(BuildContext context) { - return Container( - height: 50, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: tipsList.length, - itemBuilder: (BuildContext context, int index) { - final tip = tipsList[index]; - return Padding( - padding: EdgeInsets.only(right: 5), - child: TipButton( - isSelected: tip.isCustom ? _isCustomSelected : _isSelected(tip.percentage), - onTap: () async { - IoniaTip ioniaTip = tip; - if(tip.isCustom){ - final customTip = await Navigator.pushNamed(context, Routes.ioniaCustomTipPage, arguments: [amount, merchant, tip]) as IoniaTip?; - ioniaTip = customTip ?? tip; - } - onSelect(ioniaTip); - }, - caption: tip.isCustom ? S.of(context).custom : '${tip.percentage.toStringAsFixed(0)}%', - subTitle: tip.isCustom ? null : '\$${tip.additionalAmount.toStringAsFixed(2)}', - )); - })); - } -} - -class TipButton extends StatelessWidget { - const TipButton({ - required this.caption, - required this.onTap, - this.subTitle, - this.isSelected = false, - }); - - final String caption; - final String? subTitle; - final bool isSelected; - final void Function() onTap; - - bool isDark(BuildContext context) => Theme.of(context).brightness == Brightness.dark; - - Color captionTextColor(BuildContext context) { - if (isDark(context)) { - return Theme.of(context).extension()!.titleColor; - } - - return isSelected - ? Theme.of(context).dialogTheme.backgroundColor! - : Theme.of(context).extension()!.titleColor; - } - - Color subTitleTextColor(BuildContext context) { - if (isDark(context)) { - return Theme.of(context).extension()!.titleColor; - } - - return isSelected - ? Theme.of(context).dialogTheme.backgroundColor! - : Theme.of(context).extension()!.detailsTitlesColor; - } - - Color? backgroundColor(BuildContext context) { - if (isDark(context)) { - return isSelected - ? null - : Theme.of(context).extension()!.titleColor.withOpacity(0.01); - } - - return isSelected - ? null - : Theme.of(context).extension()!.titleColor.withOpacity(0.1); - } - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - height: 49, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(caption, - style: textSmallSemiBold( - color: captionTextColor(context))), - if (subTitle != null) ...[ - SizedBox(height: 4), - Text( - subTitle!, - style: textXxSmallSemiBold( - color: subTitleTextColor(context), - ), - ), - ] - ], - ), - padding: EdgeInsets.symmetric(horizontal: 18, vertical: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: backgroundColor(context), - gradient: isSelected - ? LinearGradient( - colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ) - : null, - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart b/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart deleted file mode 100644 index ba5b4fbbd..000000000 --- a/lib/src/screens/ionia/cards/ionia_buy_gift_card.dart +++ /dev/null @@ -1,186 +0,0 @@ -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_buy_card_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; - -class IoniaBuyGiftCardPage extends BasePage { - IoniaBuyGiftCardPage( - this.ioniaBuyCardViewModel, - ) : _amountFieldFocus = FocusNode(), - _amountController = TextEditingController() { - _amountController.addListener(() { - ioniaBuyCardViewModel.onAmountChanged(_amountController.text); - }); - } - - final IoniaBuyCardViewModel ioniaBuyCardViewModel; - - @override - String get title => S.current.enter_amount; - - @override - bool get extendBodyBehindAppBar => true; - - @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; - - Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); - - final TextEditingController _amountController; - final FocusNode _amountFieldFocus; - - @override - Widget body(BuildContext context) { - final merchant = ioniaBuyCardViewModel.ioniaMerchant; - return KeyboardActions( - disableScroll: true, - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _amountFieldFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - color: Theme.of(context).colorScheme.background, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 25), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - gradient: LinearGradient(colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 150), - SizedBox( - width: 200, - child: BaseTextFormField( - controller: _amountController, - focusNode: _amountFieldFocus, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')), - FilteringTextInputFormatter.allow( - RegExp(r'^\d+(\.|\,)?\d{0,2}'), - ), - ], - hintText: '1000', - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - fontWeight: FontWeight.w600, - fontSize: 36, - ), - prefixIcon: Text( - 'USD: ', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - fontSize: 36, - ), - ), - textColor: Colors.white, - textStyle: TextStyle( - color: Colors.white, - fontSize: 36, - ), - ), - ), - Divider( - color: Theme.of(context).extension()!.textFieldBorderColor, - height: 1, - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).min_amount(merchant.minimumCardPurchase.toStringAsFixed(2)), - style: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - ), - ), - Text( - S.of(context).max_amount(merchant.maximumCardPurchase.toStringAsFixed(2)), - style: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - ), - ), - ], - ), - SizedBox(height: 24), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: CardItem( - title: merchant.legalName, - backgroundColor: Theme.of(context).extension()!.titleColor - .withOpacity(0.1), - discount: merchant.discount, - titleColor: Theme.of(context).extension()!.titleColor, - subtitleColor: Theme.of(context).hintColor, - subTitle: merchant.avaibilityStatus, - logoUrl: merchant.logoUrl, - ), - ) - ], - ), - bottomSection: Column( - children: [ - Observer(builder: (_) { - return Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () => Navigator.of(context).pushNamed( - Routes.ioniaBuyGiftCardDetailPage, - arguments: [ - ioniaBuyCardViewModel.amount, - ioniaBuyCardViewModel.ioniaMerchant, - ], - ), - text: S.of(context).continue_text, - isDisabled: !ioniaBuyCardViewModel.isEnablePurchase, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ); - }), - SizedBox(height: 30), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart b/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart deleted file mode 100644 index 7cc4d1f0c..000000000 --- a/lib/src/screens/ionia/cards/ionia_custom_redeem_page.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_redeem_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; - -class IoniaCustomRedeemPage extends BasePage { - IoniaCustomRedeemPage( - this.ioniaCustomRedeemViewModel, - ) : _amountFieldFocus = FocusNode(), - _amountController = TextEditingController() { - _amountController.addListener(() { - ioniaCustomRedeemViewModel.updateAmount(_amountController.text); - }); - } - - final IoniaCustomRedeemViewModel ioniaCustomRedeemViewModel; - - @override - String get title => S.current.custom_redeem_amount; - - @override - bool get extendBodyBehindAppBar => true; - - @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; - - Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); - - final TextEditingController _amountController; - final FocusNode _amountFieldFocus; - - @override - Widget body(BuildContext context) { - final _width = MediaQuery.of(context).size.width; - final giftCard = ioniaCustomRedeemViewModel.giftCard; - return KeyboardActions( - disableScroll: true, - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _amountFieldFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - color: Theme.of(context).colorScheme.background, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 25), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 150), - BaseTextFormField( - controller: _amountController, - focusNode: _amountFieldFocus, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))], - hintText: '1000', - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - fontWeight: FontWeight.w500, - fontSize: 36, - ), - borderColor: Theme.of(context).extension()!.textFieldBorderColor, - textColor: Colors.white, - textStyle: TextStyle( - color: Colors.white, - fontSize: 36, - ), - suffixIcon: SizedBox( - width: _width / 6, - ), - prefixIcon: Padding( - padding: EdgeInsets.only( - top: 5.0, - left: _width / 4, - ), - child: Text( - 'USD: ', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 36, - ), - ), - ), - ), - SizedBox(height: 8), - Observer( - builder: (_) => !ioniaCustomRedeemViewModel.disableRedeem - ? Center( - child: Text( - '\$${giftCard.remainingAmount} - \$${ioniaCustomRedeemViewModel.amount} = \$${ioniaCustomRedeemViewModel.formattedRemaining} ${S.of(context).remaining}', - style: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - ), - ), - ) - : SizedBox.shrink(), - ), - SizedBox(height: 24), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: CardItem( - title: giftCard.legalName, - backgroundColor: Theme.of(context).extension()!.titleColor - .withOpacity(0.1), - discount: giftCard.remainingAmount, - isAmount: true, - discountBackground: AssetImage('assets/images/red_badge_discount.png'), - titleColor: Theme.of(context).extension()!.titleColor, - subtitleColor: Theme.of(context).hintColor, - subTitle: S.of(context).online, - logoUrl: giftCard.logoUrl, - ), - ), - ], - ), - bottomSection: Column( - children: [ - Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: LoadingPrimaryButton( - isLoading: ioniaCustomRedeemViewModel.redeemState is IsExecutingState, - isDisabled: ioniaCustomRedeemViewModel.disableRedeem, - text: S.of(context).add_custom_redemption, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - onPressed: () => ioniaCustomRedeemViewModel.addCustomRedeem().then((value) { - Navigator.of(context).pop(ioniaCustomRedeemViewModel.remaining.toString()); - }), - ), - ), - ), - SizedBox(height: 30), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart b/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart deleted file mode 100644 index eced01e96..000000000 --- a/lib/src/screens/ionia/cards/ionia_custom_tip_page.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/card_item.dart'; -import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; -import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_custom_tip_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:keyboard_actions/keyboard_actions.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; - -class IoniaCustomTipPage extends BasePage { - IoniaCustomTipPage( - this.customTipViewModel, - ) : _amountFieldFocus = FocusNode(), - _amountController = TextEditingController() { - _amountController.addListener(() { - customTipViewModel.onTipChanged(_amountController.text); - }); - } - - final IoniaCustomTipViewModel customTipViewModel; - - - @override - String get title => S.current.enter_amount; - - @override - bool get extendBodyBehindAppBar => true; - - @override - AppBarStyle get appBarStyle => AppBarStyle.transparent; - - Color get textColor => currentTheme.type == ThemeType.dark ? Colors.white : Color(0xff393939); - - final TextEditingController _amountController; - final FocusNode _amountFieldFocus; - - @override - Widget body(BuildContext context) { - final _width = MediaQuery.of(context).size.width; - final merchant = customTipViewModel.ioniaMerchant; - return KeyboardActions( - disableScroll: true, - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _amountFieldFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - color: Theme.of(context).colorScheme.background, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 25), - decoration: BoxDecoration( - borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 150), - BaseTextFormField( - controller: _amountController, - focusNode: _amountFieldFocus, - keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), - inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\-|\ ]'))], - hintText: '1000', - placeholderTextStyle: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - fontWeight: FontWeight.w500, - fontSize: 36, - ), - borderColor: Theme.of(context).extension()!.textFieldBorderColor, - textColor: Colors.white, - textStyle: TextStyle( - color: Colors.white, - fontSize: 36, - ), - suffixIcon: SizedBox( - width: _width / 6, - ), - prefixIcon: Padding( - padding: EdgeInsets.only( - top: 5.0, - left: _width / 4, - ), - child: Text( - 'USD: ', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 36, - ), - ), - ), - ), - SizedBox(height: 8), - Observer(builder: (_) { - if (customTipViewModel.percentage == 0.0) { - return SizedBox.shrink(); - } - - return RichText( - textAlign: TextAlign.center, - text: TextSpan( - text: '\$${_amountController.text}', - style: TextStyle( - color: Theme.of(context).extension()!.textFieldBorderColor, - ), - children: [ - TextSpan(text: ' ${S.of(context).is_percentage} '), - TextSpan(text: '${customTipViewModel.percentage.toStringAsFixed(2)}%'), - TextSpan(text: ' ${S.of(context).percentageOf(customTipViewModel.amount.toStringAsFixed(2))} '), - ], - ), - ); - }), - SizedBox(height: 24), - ], - ), - ), - Padding( - padding: const EdgeInsets.all(24.0), - child: CardItem( - title: merchant.legalName, - backgroundColor: Theme.of(context).extension()!.titleColor - .withOpacity(0.1), - discount: 0.0, - titleColor: Theme.of(context).extension()!.titleColor, - subtitleColor: Theme.of(context).hintColor, - subTitle: merchant.isOnline ? S.of(context).online : S.of(context).offline, - logoUrl: merchant.logoUrl, - ), - ) - ], - ), - bottomSection: Column( - children: [ - Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () { - Navigator.of(context).pop(customTipViewModel.customTip); - }, - text: S.of(context).add_tip, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ), - SizedBox(height: 30), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_debit_card_page.dart b/lib/src/screens/ionia/cards/ionia_debit_card_page.dart deleted file mode 100644 index 7e6a43253..000000000 --- a/lib/src/screens/ionia/cards/ionia_debit_card_page.dart +++ /dev/null @@ -1,393 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_virtual_card.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/widgets/alert_background.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; - -class IoniaDebitCardPage extends BasePage { - final IoniaGiftCardsListViewModel _cardsListViewModel; - - IoniaDebitCardPage(this._cardsListViewModel); - - @override - Widget middle(BuildContext context) { - return Text( - S.current.debit_card, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - return Observer( - builder: (_) { - final cardState = _cardsListViewModel.cardState; - if (cardState is IoniaFetchingCard) { - return Center(child: CircularProgressIndicator()); - } - if (cardState is IoniaCardSuccess) { - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Padding( - padding: const EdgeInsets.all(16.0), - child: _IoniaDebitCard( - cardInfo: cardState.card, - ), - ), - bottomSection: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Text( - S.of(context).billing_address_info, - style: textSmall( - color: Theme.of(context).extension()!.iconsColor), - textAlign: TextAlign.center, - ), - ), - SizedBox(height: 24), - PrimaryButton( - text: S.of(context).order_physical_card, - onPressed: () {}, - color: Color(0xffE9F2FC), - textColor: Theme.of(context).extension()!.tilesTextColor, - ), - SizedBox(height: 8), - PrimaryButton( - text: S.of(context).add_value, - onPressed: () {}, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - SizedBox(height: 16) - ], - ), - ); - } - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.zero, - content: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - _IoniaDebitCard(isCardSample: true), - SizedBox(height: 40), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - children: [ - TextIconButton( - label: S.current.how_to_use_card, - onTap: () => _showHowToUseCard(context), - ), - SizedBox( - height: 24, - ), - TextIconButton( - label: S.current.frequently_asked_questions, - onTap: () {}, - ), - ], - ), - ), - SizedBox(height: 50), - Container( - padding: EdgeInsets.all(20), - margin: EdgeInsets.all(8), - width: double.infinity, - decoration: BoxDecoration( - color: Color.fromRGBO(233, 242, 252, 1), - borderRadius: BorderRadius.circular(20), - ), - child: RichText( - text: TextSpan( - text: S.of(context).get_a, - style: textMedium( - color: - Theme.of(context).extension()!.tilesTextColor), - children: [ - TextSpan( - text: S.of(context).digital_and_physical_card, - style: textMediumBold( - color: Theme.of(context).extension()!.tilesTextColor), - ), - TextSpan( - text: S.of(context).get_card_note, - ) - ], - )), - ), - ], - ), - ), - bottomSectionPadding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 32, - ), - bottomSection: PrimaryButton( - text: S.of(context).activate, - onPressed: () => _showHowToUseCard(context, activate: true), - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ); - }, - ); - } - - void _showHowToUseCard(BuildContext context, {bool activate = false}) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertBackground( - child: Material( - color: Colors.transparent, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(height: 10), - Container( - padding: EdgeInsets.only(top: 24, left: 24, right: 24), - margin: EdgeInsets.all(24), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(30), - ), - child: Column( - children: [ - Text( - S.of(context).how_to_use_card, - style: textLargeSemiBold( - color: - Theme.of(context).extension()!.thumbColor, - ), - ), - SizedBox(height: 24), - Align( - alignment: Alignment.bottomLeft, - child: Text( - S.of(context).signup_for_card_accept_terms, - style: textSmallSemiBold( - color: Theme.of(context).extension()!.tilesTextColor, - ), - ), - ), - SizedBox(height: 24), - _TitleSubtitleTile( - title: S.of(context).add_fund_to_card('1000'), - subtitle: S.of(context).use_card_info_two, - ), - SizedBox(height: 21), - _TitleSubtitleTile( - title: S.of(context).use_card_info_three, - subtitle: S.of(context).optionally_order_card, - ), - SizedBox(height: 35), - PrimaryButton( - onPressed: () => activate - ? Navigator.pushNamed(context, Routes.ioniaActivateDebitCardPage) - : Navigator.pop(context), - text: S.of(context).got_it, - color: Color.fromRGBO(233, 242, 252, 1), - textColor: - Theme.of(context).extension()!.tilesTextColor, - ), - SizedBox(height: 21), - ], - ), - ), - InkWell( - onTap: () => Navigator.pop(context), - child: Container( - margin: EdgeInsets.only(bottom: 40), - child: CircleAvatar( - child: Icon( - Icons.close, - color: Colors.black, - ), - backgroundColor: Colors.white, - ), - ), - ) - ], - ), - ), - ); - }); - } -} - -class _IoniaDebitCard extends StatefulWidget { - const _IoniaDebitCard({ - Key? key, - this.cardInfo, - this.isCardSample = false, - }) : super(key: key); - - final bool isCardSample; - final IoniaVirtualCard? cardInfo; - - @override - _IoniaDebitCardState createState() => _IoniaDebitCardState(); -} - -class _IoniaDebitCardState extends State<_IoniaDebitCard> { - bool _showDetails = false; - void _toggleVisibility() { - setState(() => _showDetails = !_showDetails); - } - - String _formatPan(String pan) { - if (pan == null) return ''; - return pan.replaceAllMapped(RegExp(r'.{4}'), (match) => '${match.group(0)} '); - } - - String get _getLast4 => widget.isCardSample ? '0000' : widget.cardInfo!.pan.substring(widget.cardInfo!.pan.length - 5); - - String get _getSpendLimit => widget.isCardSample ? '10000' : widget.cardInfo!.spendLimit.toStringAsFixed(2); - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 19), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.firstGradientColor, - Theme.of(context).extension()!.secondGradientColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - S.current.cakepay_prepaid_card, - style: textSmall(), - ), - Image.asset( - 'assets/images/mastercard.png', - width: 54, - ), - ], - ), - Text( - widget.isCardSample ? S.of(context).upto(_getSpendLimit) : '\$$_getSpendLimit', - style: textXLargeSemiBold(), - ), - SizedBox(height: 16), - Text( - _showDetails ? _formatPan(widget.cardInfo?.pan ?? '') : '**** **** **** $_getLast4', - style: textMediumSemiBold(), - ), - SizedBox(height: 32), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (widget.isCardSample) - Text( - S.current.no_id_needed, - style: textMediumBold(), - ) - else ...[ - Column( - children: [ - Text( - 'CVV', - style: textXSmallSemiBold(), - ), - SizedBox(height: 4), - Text( - _showDetails ? widget.cardInfo!.cvv : '***', - style: textMediumSemiBold(), - ) - ], - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).expires, - style: textXSmallSemiBold(), - ), - SizedBox(height: 4), - Text( - '${widget.cardInfo?.expirationMonth ?? S.of(context).mm}/${widget.cardInfo?.expirationYear ?? S.of(context).yy}', - style: textMediumSemiBold(), - ) - ], - ), - ] - ], - ), - if (!widget.isCardSample) ...[ - SizedBox(height: 8), - Center( - child: InkWell( - onTap: () => _toggleVisibility(), - child: Text( - _showDetails ? S.of(context).hide_details : S.of(context).show_details, - style: textSmall(), - ), - ), - ), - ], - ], - ), - ); - } -} - -class _TitleSubtitleTile extends StatelessWidget { - const _TitleSubtitleTile({ - Key? key, - required this.title, - required this.subtitle, - }) : super(key: key); - - final String title; - final String subtitle; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: textSmallSemiBold( - color: Theme.of(context).extension()!.tilesTextColor), - ), - SizedBox(height: 4), - Text( - subtitle, - style: textSmall( - color: Theme.of(context).extension()!.tilesTextColor), - ), - ], - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart b/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart deleted file mode 100644 index dcd1d54b4..000000000 --- a/lib/src/screens/ionia/cards/ionia_gift_card_detail_page.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/ionia_alert_model.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/ionia_tile.dart'; -import 'package:cake_wallet/src/screens/ionia/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/utils/route_aware.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_card_details_view_model.dart'; -import 'package:device_display_brightness/device_display_brightness.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; - -class IoniaGiftCardDetailPage extends BasePage { - IoniaGiftCardDetailPage(this.viewModel); - - final IoniaGiftCardDetailsViewModel viewModel; - - @override - Widget? leading(BuildContext context) { - if (ModalRoute.of(context)!.isFirst) { - return null; - } - - final _backButton = Icon( - Icons.arrow_back_ios, - color: Theme.of(context).extension()!.titleColor, - size: 16, - ); - return Padding( - padding: const EdgeInsets.only(left: 10.0), - child: SizedBox( - height: 37, - width: 37, - child: ButtonTheme( - minWidth: double.minPositive, - child: TextButton( - // FIX-ME: Style - //highlightColor: Colors.transparent, - //splashColor: Colors.transparent, - //padding: EdgeInsets.all(0), - onPressed: ()=> onClose(context), - child: _backButton), - ), - ), - ); - } - - @override - Widget middle(BuildContext context) { - return Text( - viewModel.giftCard.legalName, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor), - ); - } - - @override - Widget body(BuildContext context) { - reaction((_) => viewModel.redeemState, (ExecutionState state) { - if (state is FailureState) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: state.error, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - }); - } - }); - - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - children: [ - if (viewModel.giftCard.barcodeUrl != null && viewModel.giftCard.barcodeUrl.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 24.0, - vertical: 24, - ), - child: Image.network(viewModel.giftCard.barcodeUrl), - ), - SizedBox(height: 24), - buildIoniaTile( - context, - title: S.of(context).gift_card_number, - subTitle: viewModel.giftCard.cardNumber, - ), - if (viewModel.giftCard.cardPin.isNotEmpty) ...[ - Divider(height: 30), - buildIoniaTile( - context, - title: S.of(context).pin_number, - subTitle: viewModel.giftCard.cardPin, - ) - ], - Divider(height: 30), - Observer( - builder: (_) => buildIoniaTile( - context, - title: S.of(context).amount, - subTitle: viewModel.remainingAmount.toStringAsFixed(2), - )), - Divider(height: 50), - TextIconButton( - label: S.of(context).how_to_use_card, - onTap: () => _showHowToUseCard(context, viewModel.giftCard), - ), - ], - ), - bottomSection: Padding( - padding: EdgeInsets.only(bottom: 12), - child: Observer( - builder: (_) { - if (!viewModel.giftCard.isEmpty) { - return Column( - children: [ - PrimaryButton( - onPressed: () async { - await Navigator.of(context).pushNamed( - Routes.ioniaMoreOptionsPage, - arguments: [viewModel.giftCard]) as String?; - viewModel.refeshCard(); - }, - text: S.of(context).more_options, - color: Theme.of(context).cardColor, - textColor: Theme.of(context).extension()!.titleColor, - ), - SizedBox(height: 12), - LoadingPrimaryButton( - isLoading: viewModel.redeemState is IsExecutingState, - onPressed: () => viewModel.redeem().then( - (_) { - Navigator.of(context).pushNamedAndRemoveUntil( - Routes.ioniaManageCardsPage, (route) => route.isFirst); - }, - ), - text: S.of(context).mark_as_redeemed, - color: Theme.of(context).primaryColor, - textColor: Colors.white, - ), - ], - ); - } - - return Container(); - }, - ), - ), - ); - } - - Widget buildIoniaTile(BuildContext context, {required String title, required String subTitle}) { - return IoniaTile( - title: title, - subTitle: subTitle, - onTap: () { - Clipboard.setData(ClipboardData(text: subTitle)); - showBar(context, S.of(context).transaction_details_copied(title)); - }); - } - - void _showHowToUseCard( - BuildContext context, - IoniaGiftCard merchant, - ) { - showPopUp( - context: context, - builder: (BuildContext context) { - return IoniaAlertModal( - title: S.of(context).how_to_use_card, - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: viewModel.giftCard.instructions - .map((instruction) { - return [ - Padding( - padding: EdgeInsets.all(10), - child: Text( - instruction.header, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.tilesTextColor, - ), - )), - Text( - instruction.body, - style: textMedium( - color: Theme.of(context).extension()!.tilesTextColor, - ), - ) - ]; - }) - .expand((e) => e) - .toList()), - actionTitle: S.of(context).got_it, - ); - }); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_more_options_page.dart b/lib/src/screens/ionia/cards/ionia_more_options_page.dart deleted file mode 100644 index eb6ed8860..000000000 --- a/lib/src/screens/ionia/cards/ionia_more_options_page.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:flutter/material.dart'; - -class IoniaMoreOptionsPage extends BasePage { - IoniaMoreOptionsPage(this.giftCard); - - final IoniaGiftCard giftCard; - - @override - Widget middle(BuildContext context) { - return Text( - S.current.more_options, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor, - ), - ); - } - - @override - Widget body(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - height: 10, - ), - Center( - child: Text( - S.of(context).choose_from_available_options, - style: textMedium( - color: Theme.of(context).extension()!.titleColor, - ), - ), - ), - SizedBox(height: 40), - InkWell( - onTap: () async { - final amount = await Navigator.of(context) - .pushNamed(Routes.ioniaCustomRedeemPage, arguments: [giftCard]) as String?; - if (amount != null && amount.isNotEmpty) { - Navigator.pop(context); - } - }, - child: _GradiantContainer( - content: Padding( - padding: const EdgeInsets.only(top: 24, left: 20, right: 24, bottom: 50), - child: Text( - S.of(context).custom_redeem_amount, - style: textXLargeSemiBold(), - ), - ), - ), - ) - ], - ), - ); - } -} - -class _GradiantContainer extends StatelessWidget { - const _GradiantContainer({Key? key, required this.content}) : super(key: key); - - final Widget content; - - @override - Widget build(BuildContext context) { - return Container( - child: content, - padding: EdgeInsets.all(24), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.secondGradientBackgroundColor, - Theme.of(context).extension()!.firstGradientBackgroundColor, - ], - begin: Alignment.topRight, - end: Alignment.bottomLeft, - ), - ), - ); - } -} diff --git a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart b/lib/src/screens/ionia/cards/ionia_payment_status_page.dart deleted file mode 100644 index dce976444..000000000 --- a/lib/src/screens/ionia/cards/ionia_payment_status_page.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_payment_status_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart'; - -class IoniaPaymentStatusPage extends BasePage { - IoniaPaymentStatusPage(this.viewModel); - - final IoniaPaymentStatusViewModel viewModel; - - @override - Widget middle(BuildContext context) { - return Text( - S.of(context).generating_gift_card, - textAlign: TextAlign.center, - style: textMediumSemiBold( - color: Theme.of(context).extension()!.titleColor)); - } - - @override - Widget body(BuildContext context) { - return _IoniaPaymentStatusPageBody(viewModel); - } -} - -class _IoniaPaymentStatusPageBody extends StatefulWidget { - _IoniaPaymentStatusPageBody(this.viewModel); - - final IoniaPaymentStatusViewModel viewModel; - - @override - _IoniaPaymentStatusPageBodyBodyState createState() => _IoniaPaymentStatusPageBodyBodyState(); -} - -class _IoniaPaymentStatusPageBodyBodyState extends State<_IoniaPaymentStatusPageBody> { - ReactionDisposer? _onGiftCardReaction; - - @override - void initState() { - if (widget.viewModel.giftCard != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context) - .pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [widget.viewModel.giftCard]); - }); - } - - _onGiftCardReaction = reaction((_) => widget.viewModel.giftCard, (IoniaGiftCard? giftCard) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context) - .pushReplacementNamed(Routes.ioniaGiftCardDetailPage, arguments: [giftCard]); - }); - }); - - super.initState(); - } - - @override - void dispose() { - _onGiftCardReaction?.reaction.dispose(); - widget.viewModel.timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ScrollableWithBottomSection( - contentPadding: EdgeInsets.all(24), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row(children: [ - Padding( - padding: EdgeInsets.only(right: 10), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.green), - height: 10, - width: 10)), - Text( - S.of(context).awaiting_payment_confirmation, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.titleColor)) - ]), - SizedBox(height: 40), - Row(children: [ - SizedBox(width: 20), - Expanded(child: - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ...widget.viewModel - .committedInfo - .transactions - .map((transaction) => buildDescriptionTileWithCopy(context, S.of(context).transaction_details_transaction_id, transaction.id)), - if (widget.viewModel.paymentInfo.ioniaOrder.id != null) - ...[Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).order_id, widget.viewModel.paymentInfo.ioniaOrder.id)], - if (widget.viewModel.paymentInfo.ioniaOrder.paymentId != null) - ...[Divider(height: 30), - buildDescriptionTileWithCopy(context, S.of(context).payment_id, widget.viewModel.paymentInfo.ioniaOrder.paymentId)], - ])) - ]), - SizedBox(height: 40), - Observer(builder: (_) { - if (widget.viewModel.giftCard != null) { - return Container( - padding: EdgeInsets.only(top: 40), - child: Row(children: [ - Padding( - padding: EdgeInsets.only(right: 10,), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.green), - height: 10, - width: 10)), - Text( - S.of(context).gift_card_is_generated, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.titleColor)) - ])); - } - - return Row(children: [ - Padding( - padding: EdgeInsets.only(right: 10), - child: Observer(builder: (_) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: widget.viewModel.giftCard == null ? Colors.grey : Colors.green), - height: 10, - width: 10); - })), - Text( - S.of(context).generating_gift_card, - style: textLargeSemiBold( - color: Theme.of(context).extension()!.titleColor))]); - }), - ], - ), - bottomSection: Padding( - padding: EdgeInsets.only(bottom: 12), - child: Column(children: [ - Container( - padding: EdgeInsets.only(left: 40, right: 40, bottom: 20), - child: Text( - widget.viewModel.payingByBitcoin ? S.of(context).bitcoin_payments_require_1_confirmation - : S.of(context).proceed_after_one_minute, - style: textMedium( - color: Theme.of(context).extension()!.titleColor, - ).copyWith(fontWeight: FontWeight.w500), - textAlign: TextAlign.center, - )), - Observer(builder: (_) { - if (widget.viewModel.giftCard != null) { - return PrimaryButton( - onPressed: () => Navigator.of(context) - .pushReplacementNamed( - Routes.ioniaGiftCardDetailPage, - arguments: [widget.viewModel.giftCard]), - text: S.of(context).open_gift_card, - color: Theme.of(context).primaryColor, - textColor: Colors.white); - } - - return PrimaryButton( - onPressed: () => Navigator.of(context).pushNamed(Routes.support), - text: S.of(context).contact_support, - color: Theme.of(context).cardColor, - textColor: Theme.of(context).extension()!.titleColor); - }) - ]) - ), - ); - } - - Widget buildDescriptionTile(BuildContext context, String title, String subtitle, VoidCallback onTap) { - return GestureDetector( - onTap: () => onTap(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: textXSmall( - color: Theme.of(context).extension()!.detailsTitlesColor, - ), - ), - SizedBox(height: 8), - Text( - subtitle, - style: textMedium( - color: Theme.of(context).extension()!.titleColor, - ), - ), - ], - )); - } - - Widget buildDescriptionTileWithCopy(BuildContext context, String title, String subtitle) { - return buildDescriptionTile(context, title, subtitle, () { - Clipboard.setData(ClipboardData(text: subtitle)); - showBar(context, - S.of(context).transaction_details_copied(title)); - }); - } -} \ No newline at end of file diff --git a/lib/src/screens/ionia/ionia.dart b/lib/src/screens/ionia/ionia.dart deleted file mode 100644 index bdc2065a9..000000000 --- a/lib/src/screens/ionia/ionia.dart +++ /dev/null @@ -1,9 +0,0 @@ -export 'auth/ionia_welcome_page.dart'; -export 'auth/ionia_create_account_page.dart'; -export 'auth/ionia_login_page.dart'; -export 'auth/ionia_verify_otp_page.dart'; -export 'cards/ionia_activate_debit_card_page.dart'; -export 'cards/ionia_buy_card_detail_page.dart'; -export 'cards/ionia_manage_cards_page.dart'; -export 'cards/ionia_debit_card_page.dart'; -export 'cards/ionia_buy_gift_card.dart'; diff --git a/lib/src/screens/ionia/widgets/card_item.dart b/lib/src/screens/ionia/widgets/card_item.dart deleted file mode 100644 index 405de5adc..000000000 --- a/lib/src/screens/ionia/widgets/card_item.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:cake_wallet/src/widgets/discount_badge.dart'; -import 'package:flutter/material.dart'; - -class CardItem extends StatelessWidget { - CardItem({ - required this.title, - required this.subTitle, - required this.backgroundColor, - required this.titleColor, - required this.subtitleColor, - this.hideBorder = false, - this.discount = 0.0, - this.isAmount = false, - this.discountBackground, - this.onTap, - this.logoUrl, - }); - - final VoidCallback? onTap; - final String title; - final String subTitle; - final String? logoUrl; - final double discount; - final bool isAmount; - final bool hideBorder; - final Color backgroundColor; - final Color titleColor; - final Color subtitleColor; - final AssetImage? discountBackground; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Stack( - children: [ - Container( - padding: EdgeInsets.all(12), - width: double.infinity, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(20), - border: hideBorder ? Border.symmetric(horizontal: BorderSide.none, vertical: BorderSide.none) : Border.all( - color: Colors.white.withOpacity(0.20), - ), - ), - child: Row( - children: [ - if (logoUrl != null) ...[ - ClipOval( - child: Image.network( - logoUrl!, - width: 40.0, - height: 40.0, - fit: BoxFit.cover, - loadingBuilder: (BuildContext _, Widget child, ImageChunkEvent? loadingProgress) { - if (loadingProgress == null) { - return child; - } else { - return _PlaceholderContainer(text: 'Logo'); - } - }, - errorBuilder: (_, __, ___) => _PlaceholderContainer(text: '!'), - ), - ), - SizedBox(width: 5), - ], - Column( - crossAxisAlignment: (subTitle?.isEmpty ?? false) - ? CrossAxisAlignment.center - : CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: Text( - title, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: titleColor, - fontSize: 20, - fontWeight: FontWeight.w900, - ), - ), - ), - if (subTitle?.isNotEmpty ?? false) - Padding( - padding: EdgeInsets.only(top: 5), - child: Text( - subTitle, - style: TextStyle( - color: subtitleColor, - fontWeight: FontWeight.w500, - fontFamily: 'Lato')), - ) - ], - ), - ], - ), - ), - if (discount != 0.0) - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.only(top: 20.0), - child: DiscountBadge( - percentage: discount, - isAmount: isAmount, - discountBackground: discountBackground, - ), - ), - ), - ], - ), - ); - } -} - -class _PlaceholderContainer extends StatelessWidget { - const _PlaceholderContainer({required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - return Container( - height: 42, - width: 42, - child: Center( - child: Text( - text, - style: TextStyle( - color: Colors.black, - fontSize: 12, - fontWeight: FontWeight.w900, - ), - ), - ), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(100), - ), - ); - } -} diff --git a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart b/lib/src/screens/ionia/widgets/ionia_filter_modal.dart deleted file mode 100644 index 8a6820fcd..000000000 --- a/lib/src/screens/ionia/widgets/ionia_filter_modal.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:cake_wallet/src/screens/ionia/widgets/rounded_checkbox.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cake_wallet/src/widgets/alert_background.dart'; -import 'package:cake_wallet/typography.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/view_model/ionia/ionia_gift_cards_list_view_model.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/palette.dart'; -import 'package:cake_wallet/themes/extensions/menu_theme.dart'; - -class IoniaFilterModal extends StatelessWidget { - IoniaFilterModal({required this.ioniaGiftCardsListViewModel}){ - ioniaGiftCardsListViewModel.resetIoniaCategories(); - } - - final IoniaGiftCardsListViewModel ioniaGiftCardsListViewModel; - - @override - Widget build(BuildContext context) { - final searchIcon = Padding( - padding: EdgeInsets.all(10), - child: Image.asset( - 'assets/images/mini_search_icon.png', - color: Theme.of(context).primaryColor, - ), - ); - return Scaffold( - resizeToAvoidBottomInset: false, - body: AlertBackground( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(height: 10), - Container( - padding: EdgeInsets.only(top: 24, bottom: 20), - margin: EdgeInsets.all(24), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(30), - ), - child: Column( - children: [ - SizedBox( - height: 40, - child: Padding( - padding: const EdgeInsets.only(left: 24, right: 24), - child: TextField( - onChanged: ioniaGiftCardsListViewModel.onSearchFilter, - style: textMedium( - color: Theme.of(context).extension()!.titleColor, - ), - decoration: InputDecoration( - filled: true, - prefixIcon: searchIcon, - hintText: S.of(context).search_category, - contentPadding: EdgeInsets.only(bottom: 5), - fillColor: Theme.of(context).extension()!.dividerColor.withOpacity(0.5), - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ), - SizedBox(height: 10), - Divider(thickness: 2), - SizedBox(height: 24), - Observer(builder: (_) { - return ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: ioniaGiftCardsListViewModel.ioniaCategories.length, - itemBuilder: (_, index) { - final category = ioniaGiftCardsListViewModel.ioniaCategories[index]; - return Padding( - padding: const EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: InkWell( - onTap: () => ioniaGiftCardsListViewModel.setSelectedFilter(category), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - category.iconPath, - color: Theme.of(context).extension()!.titleColor, - ), - SizedBox(width: 10), - Text(category.title, - style: textSmall( - color: Theme.of(context).extension()!.titleColor, - ).copyWith(fontWeight: FontWeight.w500)), - ], - ), - Observer(builder: (_) { - final value = ioniaGiftCardsListViewModel.selectedIndices; - return RoundedCheckbox( - value: value.contains(category), - ); - }), - ], - ), - ), - ); - }, - ); - }), - ], - ), - ), - InkWell( - onTap: () => Navigator.pop(context), - child: Container( - margin: EdgeInsets.only(bottom: 40), - child: CircleAvatar( - child: Icon( - Icons.close, - color: Palette.darkBlueCraiola, - ), - backgroundColor: Colors.white, - ), - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index ce711ce8b..3af1c3f8c 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -12,6 +12,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { {required this.alertTitle, this.paymentId, this.paymentIdValue, + this.expirationTime, required this.amount, required this.amountValue, required this.fiatAmountValue, @@ -28,11 +29,13 @@ class ConfirmSendingAlert extends BaseAlertDialog { this.alertLeftActionButtonTextColor, this.alertRightActionButtonTextColor, this.alertLeftActionButtonColor, - this.alertRightActionButtonColor}); + this.alertRightActionButtonColor, + this.onDispose}); final String alertTitle; final String? paymentId; final String? paymentIdValue; + final String? expirationTime; final String amount; final String amountValue; final String fiatAmountValue; @@ -50,6 +53,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { final Color? alertRightActionButtonTextColor; final Color? alertLeftActionButtonColor; final Color? alertRightActionButtonColor; + final Function? onDispose; @override String get titleText => alertTitle; @@ -88,6 +92,7 @@ class ConfirmSendingAlert extends BaseAlertDialog { Widget content(BuildContext context) => ConfirmSendingAlertContent( paymentId: paymentId, paymentIdValue: paymentIdValue, + expirationTime: expirationTime, amount: amount, amountValue: amountValue, fiatAmountValue: fiatAmountValue, @@ -95,13 +100,15 @@ class ConfirmSendingAlert extends BaseAlertDialog { feeRate: feeRate, feeValue: feeValue, feeFiatAmount: feeFiatAmount, - outputs: outputs); + outputs: outputs, + onDispose: onDispose); } class ConfirmSendingAlertContent extends StatefulWidget { ConfirmSendingAlertContent( {this.paymentId, this.paymentIdValue, + this.expirationTime, required this.amount, required this.amountValue, required this.fiatAmountValue, @@ -109,10 +116,12 @@ class ConfirmSendingAlertContent extends StatefulWidget { this.feeRate, required this.feeValue, required this.feeFiatAmount, - required this.outputs}); + required this.outputs, + required this.onDispose}) {} final String? paymentId; final String? paymentIdValue; + final String? expirationTime; final String amount; final String amountValue; final String fiatAmountValue; @@ -121,11 +130,13 @@ class ConfirmSendingAlertContent extends StatefulWidget { final String feeValue; final String feeFiatAmount; final List outputs; + final Function? onDispose; @override ConfirmSendingAlertContentState createState() => ConfirmSendingAlertContentState( paymentId: paymentId, paymentIdValue: paymentIdValue, + expirationTime: expirationTime, amount: amount, amountValue: amountValue, fiatAmountValue: fiatAmountValue, @@ -133,13 +144,15 @@ class ConfirmSendingAlertContent extends StatefulWidget { feeRate: feeRate, feeValue: feeValue, feeFiatAmount: feeFiatAmount, - outputs: outputs); + outputs: outputs, + onDispose: onDispose); } class ConfirmSendingAlertContentState extends State { ConfirmSendingAlertContentState( {this.paymentId, this.paymentIdValue, + this.expirationTime, required this.amount, required this.amountValue, required this.fiatAmountValue, @@ -147,7 +160,8 @@ class ConfirmSendingAlertContentState extends State this.feeRate, required this.feeValue, required this.feeFiatAmount, - required this.outputs}) + required this.outputs, + this.onDispose}) : recipientTitle = '' { recipientTitle = outputs.length > 1 ? S.current.transaction_details_recipient_address @@ -156,6 +170,7 @@ class ConfirmSendingAlertContentState extends State final String? paymentId; final String? paymentIdValue; + final String? expirationTime; final String amount; final String amountValue; final String fiatAmountValue; @@ -164,6 +179,7 @@ class ConfirmSendingAlertContentState extends State final String feeValue; final String feeFiatAmount; final List outputs; + final Function? onDispose; final double backgroundHeight = 160; final double thumbHeight = 72; @@ -172,6 +188,12 @@ class ConfirmSendingAlertContentState extends State String recipientTitle; bool showScrollbar = false; + @override + void dispose() { + if (onDispose != null) onDispose!(); + super.dispose(); + } + @override Widget build(BuildContext context) { controller.addListener(() { @@ -217,14 +239,18 @@ class ConfirmSendingAlertContentState extends State Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - paymentIdValue!, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: Theme.of(context).extension()!.titleColor, - decoration: TextDecoration.none, + Container( + width: 160, + child: Text( + paymentIdValue!, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), ), ), ], @@ -232,6 +258,8 @@ class ConfirmSendingAlertContentState extends State ], ), ), + if (widget.expirationTime != null) + ExpirationTimeWidget(expirationTime: widget.expirationTime!), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -468,3 +496,46 @@ class ConfirmSendingAlertContentState extends State ]); } } + +class ExpirationTimeWidget extends StatelessWidget { + const ExpirationTimeWidget({ + required this.expirationTime, + }); + + final String expirationTime; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 32), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.current.offer_expires_in, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + Text( + expirationTime, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ) + ], + ), + ); + } +} diff --git a/lib/src/widgets/number_text_fild_widget.dart b/lib/src/widgets/number_text_fild_widget.dart new file mode 100644 index 000000000..52e7e23cf --- /dev/null +++ b/lib/src/widgets/number_text_fild_widget.dart @@ -0,0 +1,145 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/typography.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class NumberTextField extends StatefulWidget { + final TextEditingController? controller; + final FocusNode? focusNode; + final int min; + final int max; + final int step; + final double arrowsWidth; + final double arrowsHeight; + final EdgeInsets contentPadding; + final double borderWidth; + final ValueChanged? onChanged; + + const NumberTextField({ + Key? key, + this.controller, + this.focusNode, + this.min = 0, + this.max = 999, + this.step = 1, + this.arrowsWidth = 24, + this.arrowsHeight = kMinInteractiveDimension, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.borderWidth = 2, + this.onChanged, + }) : super(key: key); + + @override + State createState() => _NumberTextFieldState(); +} + +class _NumberTextFieldState extends State { + late TextEditingController _controller; + late FocusNode _focusNode; + bool _canGoUp = false; + bool _canGoDown = false; + + @override + void initState() { + super.initState(); + _controller = widget.controller ?? TextEditingController(); + _focusNode = widget.focusNode ?? FocusNode(); + _updateArrows(int.tryParse(_controller.text)); + } + + @override + void didUpdateWidget(covariant NumberTextField oldWidget) { + super.didUpdateWidget(oldWidget); + _controller = widget.controller ?? _controller; + _focusNode = widget.focusNode ?? _focusNode; + _updateArrows(int.tryParse(_controller.text)); + } + + @override + Widget build(BuildContext context) => TextField( + style: textMediumSemiBold(color: Theme.of(context).extension()!.titleColor), + enableInteractiveSelection: false, + textAlign: TextAlign.center, + textAlignVertical: TextAlignVertical.bottom, + controller: _controller, + focusNode: _focusNode, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.number, + maxLength: widget.max.toString().length + (widget.min.isNegative ? 1 : 0), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.all(0), + fillColor: Colors.transparent, + counterText: '', + isDense: true, + filled: true, + suffixIconConstraints: BoxConstraints( + maxHeight: widget.arrowsHeight, + maxWidth: widget.arrowsWidth + widget.contentPadding.right), + prefixIconConstraints: BoxConstraints( + maxHeight: widget.arrowsHeight, + maxWidth: widget.arrowsWidth + widget.contentPadding.left), + prefixIcon: Material( + type: MaterialType.transparency, + child: InkWell( + child: Container( + width: widget.arrowsWidth, + alignment: Alignment.bottomCenter, + child: Icon(Icons.arrow_left_outlined, size: widget.arrowsWidth)), + onTap: _canGoDown ? () => _update(false) : null)), + suffixIcon: Material( + type: MaterialType.transparency, + child: InkWell( + child: Container( + width: widget.arrowsWidth, + alignment: Alignment.bottomCenter, + child: Icon(Icons.arrow_right_outlined, size: widget.arrowsWidth)), + onTap: _canGoUp ? () => _update(true) : null))), + maxLines: 1, + onChanged: (value) { + final intValue = int.tryParse(value); + widget.onChanged?.call(intValue); + _updateArrows(intValue); + }, + inputFormatters: [_NumberTextInputFormatter(widget.min, widget.max)]); + + void _update(bool up) { + var intValue = int.tryParse(_controller.text); + intValue == null ? intValue = widget.min : intValue += up ? widget.step : -widget.step; + intValue = intValue.clamp(widget.min, widget.max); // Ensure intValue is within range + _controller.text = intValue.toString(); + + // Manually call the onChanged callback after updating the controller's text + widget.onChanged?.call(intValue); + + _updateArrows(intValue); + _focusNode.requestFocus(); + } + + void _updateArrows(int? value) { + final canGoUp = value == null || value < widget.max; + final canGoDown = value == null || value > widget.min; + if (_canGoUp != canGoUp || _canGoDown != canGoDown) + setState(() { + _canGoUp = canGoUp; + _canGoDown = canGoDown; + }); + } +} + +class _NumberTextInputFormatter extends TextInputFormatter { + final int min; + final int max; + + _NumberTextInputFormatter(this.min, this.max); + + @override + TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + if (const ['-', ''].contains(newValue.text)) return newValue; + final intValue = int.tryParse(newValue.text); + if (intValue == null) return oldValue; + if (intValue < min) return newValue.copyWith(text: min.toString()); + if (intValue > max) return newValue.copyWith(text: max.toString()); + return newValue.copyWith(text: intValue.toString()); + } +} diff --git a/lib/view_model/cake_pay/cake_pay_account_view_model.dart b/lib/view_model/cake_pay/cake_pay_account_view_model.dart new file mode 100644 index 000000000..85c68c9fb --- /dev/null +++ b/lib/view_model/cake_pay/cake_pay_account_view_model.dart @@ -0,0 +1,20 @@ +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_pay_account_view_model.g.dart'; + +class CakePayAccountViewModel = CakePayAccountViewModelBase with _$CakePayAccountViewModel; + +abstract class CakePayAccountViewModelBase with Store { + CakePayAccountViewModelBase({required this.cakePayService}) : email = '' { + cakePayService.getUserEmail().then((email) => this.email = email ?? ''); + } + + final CakePayService cakePayService; + + @observable + String email; + + @action + Future logout() async => cakePayService.logout(email); +} diff --git a/lib/view_model/cake_pay/cake_pay_auth_view_model.dart b/lib/view_model/cake_pay/cake_pay_auth_view_model.dart new file mode 100644 index 000000000..f23d43f1f --- /dev/null +++ b/lib/view_model/cake_pay/cake_pay_auth_view_model.dart @@ -0,0 +1,51 @@ +import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_pay_auth_view_model.g.dart'; + +class CakePayAuthViewModel = CakePayAuthViewModelBase with _$CakePayAuthViewModel; + +abstract class CakePayAuthViewModelBase with Store { + CakePayAuthViewModelBase({required this.cakePayService}) + : userVerificationState = CakePayUserVerificationStateInitial(), + otpState = CakePayOtpSendDisabled(), + email = '', + otp = ''; + + final CakePayService cakePayService; + + @observable + CakePayUserVerificationState userVerificationState; + + @observable + CakePayOtpState otpState; + + @observable + String email; + + @observable + String otp; + + @action + Future verifyEmail(String code) async { + try { + otpState = CakePayOtpValidating(); + await cakePayService.verifyEmail(code); + otpState = CakePayOtpSuccess(); + } catch (_) { + otpState = CakePayOtpFailure(error: 'Invalid OTP. Try again'); + } + } + + @action + Future logIn(String email) async { + try { + userVerificationState = CakePayUserVerificationStateLoading(); + await cakePayService.logIn(email); + userVerificationState = CakePayUserVerificationStateSuccess(); + } catch (e) { + userVerificationState = CakePayUserVerificationStateFailure(error: e.toString()); + } + } +} diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart new file mode 100644 index 000000000..4fd97213d --- /dev/null +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -0,0 +1,48 @@ +import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_pay_buy_card_view_model.g.dart'; + +class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel; + +abstract class CakePayBuyCardViewModelBase with Store { + CakePayBuyCardViewModelBase({required this.vendor}) + : amount = vendor.card!.denominations.isNotEmpty + ? double.parse(vendor.card!.denominations.first) + : 0, + quantity = 1, + min = double.parse(vendor.card!.minValue ?? '0'), + max = double.parse(vendor.card!.maxValue ?? '0'), + card = vendor.card!; + + final CakePayVendor vendor; + final CakePayCard card; + + final double min; + final double max; + + bool get isDenominationSelected => card.denominations.isNotEmpty; + + @observable + double amount; + + @observable + int quantity; + + @computed + bool get isEnablePurchase => + (amount >= min && amount <= max) || (isDenominationSelected && quantity > 0); + + @computed + double get totalAmount => amount * quantity; + + @action + void onQuantityChanged(int? input) => quantity = input ?? 1; + + @action + void onAmountChanged(String input) { + if (input.isEmpty) return; + amount = double.parse(input.replaceAll(',', '.')); + } +} diff --git a/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart new file mode 100644 index 000000000..d0483596e --- /dev/null +++ b/lib/view_model/cake_pay/cake_pay_cards_list_view_model.dart @@ -0,0 +1,221 @@ +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_states.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_vendor.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/view_model/dashboard/dropdown_filter_item.dart'; +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_pay_cards_list_view_model.g.dart'; + +class CakePayCardsListViewModel = CakePayCardsListViewModelBase with _$CakePayCardsListViewModel; + +abstract class CakePayCardsListViewModelBase with Store { + CakePayCardsListViewModelBase({ + required this.cakePayService, + }) : cardState = CakePayCardsStateNoCards(), + cakePayVendors = [], + availableCountries = [], + page = 1, + selectedCountry = 'USA', + displayPrepaidCards = true, + displayGiftCards = true, + displayDenominationsCards = true, + displayCustomValueCards = true, + scrollOffsetFromTop = 0.0, + vendorsState = InitialCakePayVendorLoadingState(), + createCardState = CakePayCreateCardState(), + searchString = '', + CakePayVendorList = [] { + initialization(); + } + + void initialization() async { + await getCountries(); + selectedCountry = availableCountries.first; + getVendors(); + } + + final CakePayService cakePayService; + + List CakePayVendorList; + + Map> get createFilterItems => { + S.current.filter_by: [ + FilterItem( + value: () => displayPrepaidCards, + caption: S.current.prepaid_cards, + onChanged: togglePrepaidCards), + FilterItem( + value: () => displayGiftCards, + caption: S.current.gift_cards, + onChanged: toggleGiftCards), + ], + S.current.value_type: [ + FilterItem( + value: () => displayDenominationsCards, + caption: S.current.denominations, + onChanged: toggleDenominationsCards), + FilterItem( + value: () => displayCustomValueCards, + caption: S.current.custom_value, + onChanged: toggleCustomValueCards), + ], + S.current.countries: [ + DropdownFilterItem( + items: availableCountries, + caption: '', + selectedItem: selectedCountry, + onItemSelected: (String value) => setSelectedCountry(value), + ), + ] + }; + + String searchString; + + int page; + + late String _initialSelectedCountry; + + late bool _initialDisplayPrepaidCards; + + late bool _initialDisplayGiftCards; + + late bool _initialDisplayDenominationsCards; + + late bool _initialDisplayCustomValueCards; + + @observable + double scrollOffsetFromTop; + + @observable + CakePayCreateCardState createCardState; + + @observable + CakePayCardsState cardState; + + @observable + CakePayVendorState vendorsState; + + @observable + bool hasMoreDataToFetch = true; + + @observable + bool isLoadingNextPage = false; + + @observable + List cakePayVendors; + + @observable + List availableCountries; + + @observable + bool displayPrepaidCards; + + @observable + bool displayGiftCards; + + @observable + bool displayDenominationsCards; + + @observable + bool displayCustomValueCards; + + @observable + String selectedCountry; + + bool get hasFiltersChanged => + selectedCountry != _initialSelectedCountry || + displayPrepaidCards != _initialDisplayPrepaidCards || + displayGiftCards != _initialDisplayGiftCards || + displayDenominationsCards != _initialDisplayDenominationsCards || + displayCustomValueCards != _initialDisplayCustomValueCards; + + Future getCountries() async { + availableCountries = await cakePayService.getCountries(); + } + + @action + Future getVendors({ + String? text, + int? currentPage, + }) async { + vendorsState = CakePayVendorLoadingState(); + searchString = text ?? ''; + var newVendors = await cakePayService.getVendors( + country: selectedCountry, + page: currentPage ?? page, + search: searchString, + giftCards: displayGiftCards, + prepaidCards: displayPrepaidCards, + custom: displayCustomValueCards, + onDemand: displayDenominationsCards); + + cakePayVendors = CakePayVendorList = newVendors; + + vendorsState = CakePayVendorLoadedState(); + } + + @action + Future fetchNextPage() async { + if (vendorsState is CakePayVendorLoadingState || !hasMoreDataToFetch || isLoadingNextPage) + return; + + isLoadingNextPage = true; + page++; + try { + var newVendors = await cakePayService.getVendors( + country: selectedCountry, + page: page, + search: searchString, + giftCards: displayGiftCards, + prepaidCards: displayPrepaidCards, + custom: displayCustomValueCards, + onDemand: displayDenominationsCards); + + cakePayVendors.addAll(newVendors); + } catch (error) { + if (error.toString().contains('detail":"Invalid page."')) { + hasMoreDataToFetch = false; + } + } finally { + isLoadingNextPage = false; + } + } + + Future isCakePayUserAuthenticated() async { + return await cakePayService.isLogged(); + } + + void resetLoadingNextPageState() { + hasMoreDataToFetch = true; + page = 1; + } + + void storeInitialFilterStates() { + _initialSelectedCountry = selectedCountry; + _initialDisplayPrepaidCards = displayPrepaidCards; + _initialDisplayGiftCards = displayGiftCards; + _initialDisplayDenominationsCards = displayDenominationsCards; + _initialDisplayCustomValueCards = displayCustomValueCards; + } + + @action + void setSelectedCountry(String country) => selectedCountry = country; + + @action + void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards; + + @action + void toggleGiftCards() => displayGiftCards = !displayGiftCards; + + @action + void toggleDenominationsCards() => displayDenominationsCards = !displayDenominationsCards; + + @action + void toggleCustomValueCards() => displayCustomValueCards = !displayCustomValueCards; + + void setScrollOffsetFromTop(double scrollOffset) { + scrollOffsetFromTop = scrollOffset; + } +} diff --git a/lib/view_model/cake_pay/cake_pay_purchase_view_model.dart b/lib/view_model/cake_pay/cake_pay_purchase_view_model.dart new file mode 100644 index 000000000..a580db054 --- /dev/null +++ b/lib/view_model/cake_pay/cake_pay_purchase_view_model.dart @@ -0,0 +1,162 @@ +import 'dart:async'; + +import 'package:cake_wallet/cake_pay/cake_pay_card.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_order.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_payment_credantials.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/view_model/send/send_view_model.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:mobx/mobx.dart'; + +part 'cake_pay_purchase_view_model.g.dart'; + +class CakePayPurchaseViewModel = CakePayPurchaseViewModelBase with _$CakePayPurchaseViewModel; + +abstract class CakePayPurchaseViewModelBase with Store { + CakePayPurchaseViewModelBase({ + required this.cakePayService, + required this.paymentCredential, + required this.card, + required this.sendViewModel, + }) : walletType = sendViewModel.walletType; + + final WalletType walletType; + + final PaymentCredential paymentCredential; + + final CakePayCard card; + + final SendViewModel sendViewModel; + + final CakePayService cakePayService; + + CakePayOrder? order; + + Timer? _timer; + + DateTime? expirationTime; + + Duration? remainingTime; + + String? get userName => paymentCredential.userName; + + double get amount => paymentCredential.amount; + + int get quantity => paymentCredential.quantity; + + double get totalAmount => paymentCredential.totalAmount; + + String get fiatCurrency => paymentCredential.fiatCurrency; + + CryptoPaymentData? get cryptoPaymentData { + if (order == null) return null; + + if (WalletType.monero == walletType) { + return order!.paymentData.xmr; + } + + if (WalletType.bitcoin == walletType) { + final paymentUrls = order!.paymentData.btc.paymentUrls!.bip21; + + final uri = Uri.parse(paymentUrls!); + + final address = uri.path; + final price = uri.queryParameters['amount']; + + return CryptoPaymentData( + address: address, + price: price ?? '0', + ); + } + + return null; + } + + @observable + bool isOrderExpired = false; + + @observable + String formattedRemainingTime = ''; + + @action + Future createOrder() async { + if (walletType != WalletType.bitcoin && walletType != WalletType.monero) { + sendViewModel.state = FailureState('Unsupported wallet type, please use Bitcoin or Monero.'); + } + try { + order = await cakePayService.createOrder( + cardId: card.id, + price: paymentCredential.amount.toString(), + quantity: paymentCredential.quantity); + await confirmSending(); + expirationTime = order!.paymentData.expirationTime; + updateRemainingTime(); + _startExpirationTimer(); + } catch (e) { + sendViewModel.state = FailureState( + sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency)); + } + } + + @action + Future confirmSending() async { + final cryptoPaymentData = this.cryptoPaymentData; + try { + if (order == null || cryptoPaymentData == null) return; + + sendViewModel.clearOutputs(); + final output = sendViewModel.outputs.first; + output.address = cryptoPaymentData.address; + output.setCryptoAmount(cryptoPaymentData.price); + + await sendViewModel.createTransaction(); + } catch (e) { + throw e; + } + } + + @action + void updateRemainingTime() { + if (expirationTime == null) { + formattedRemainingTime = ''; + return; + } + + remainingTime = expirationTime!.difference(DateTime.now()); + + isOrderExpired = remainingTime!.isNegative; + + if (isOrderExpired) { + disposeExpirationTimer(); + sendViewModel.state = FailureState('Order has expired.'); + } else { + formattedRemainingTime = formatDuration(remainingTime!); + } + } + + void _startExpirationTimer() { + _timer?.cancel(); + _timer = Timer.periodic(Duration(seconds: 1), (_) { + updateRemainingTime(); + }); + } + + String formatDuration(Duration duration) { + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + final seconds = duration.inSeconds.remainder(60); + return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + void disposeExpirationTimer() { + _timer?.cancel(); + remainingTime = null; + formattedRemainingTime = ''; + expirationTime = null; + } + + void dispose() { + disposeExpirationTimer(); + } +} diff --git a/lib/view_model/dashboard/cake_features_view_model.dart b/lib/view_model/dashboard/cake_features_view_model.dart index 0a8fbc640..45f80bf80 100644 --- a/lib/view_model/dashboard/cake_features_view_model.dart +++ b/lib/view_model/dashboard/cake_features_view_model.dart @@ -1,4 +1,4 @@ -import 'package:cake_wallet/ionia/ionia_service.dart'; +import 'package:cake_wallet/cake_pay/cake_pay_service.dart'; import 'package:mobx/mobx.dart'; part 'cake_features_view_model.g.dart'; @@ -6,11 +6,11 @@ part 'cake_features_view_model.g.dart'; class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel; abstract class CakeFeaturesViewModelBase with Store { - final IoniaService _ioniaService; + final CakePayService _cakePayService; - CakeFeaturesViewModelBase(this._ioniaService); + CakeFeaturesViewModelBase(this._cakePayService); Future isIoniaUserAuthenticated() async { - return await _ioniaService.isLogined(); + return await _cakePayService.isLogged(); } } diff --git a/lib/view_model/dashboard/dropdown_filter_item.dart b/lib/view_model/dashboard/dropdown_filter_item.dart new file mode 100644 index 000000000..f7f8e593f --- /dev/null +++ b/lib/view_model/dashboard/dropdown_filter_item.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/view_model/dashboard/filter_item.dart'; + +class DropdownFilterItem extends FilterItem { + DropdownFilterItem({ + required this.items, + required this.caption, + required this.selectedItem, + required this.onItemSelected, + }) : super( + value: () => false, + caption: caption, + onChanged: (_) {}, + ); + + final List items; + final String caption; + final String selectedItem; + final Function(String) onItemSelected; +} diff --git a/lib/view_model/dashboard/dropdown_filter_item_widget.dart b/lib/view_model/dashboard/dropdown_filter_item_widget.dart new file mode 100644 index 000000000..20bf54887 --- /dev/null +++ b/lib/view_model/dashboard/dropdown_filter_item_widget.dart @@ -0,0 +1,68 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cake_wallet/themes/extensions/picker_theme.dart'; +import 'package:flutter/material.dart'; + +class DropdownFilterList extends StatefulWidget { + DropdownFilterList({ + Key? key, + required this.items, + this.itemPrefix, + this.textStyle, + required this.caption, + required this.selectedItem, + required this.onItemSelected, + }) : super(key: key); + + final List items; + final String? itemPrefix; + final TextStyle? textStyle; + final String caption; + final String selectedItem; + final Function(String) onItemSelected; + + @override + _DropdownFilterListState createState() => _DropdownFilterListState(); +} + +class _DropdownFilterListState extends State { + String? selectedValue; + + @override + void initState() { + super.initState(); + selectedValue = widget.selectedItem; + } + + @override + Widget build(BuildContext context) { + return DropdownButtonHideUnderline( + child: Container( + child: DropdownButton( + isExpanded: true, + icon: Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon(Icons.arrow_drop_down, color: Theme.of(context).extension()!.searchIconColor), + ], + ), + ), + dropdownColor: Theme.of(context).extension()!.searchBackgroundFillColor, + borderRadius: BorderRadius.circular(10), + items: widget.items + .map((item) => DropdownMenuItem( + alignment: Alignment.bottomCenter, + value: item, + child: AutoSizeText('${widget.itemPrefix ?? ''} $item', style: widget.textStyle), + )) + .toList(), + value: selectedValue, + onChanged: (newValue) { + setState(() => selectedValue = newValue); + widget.onItemSelected(newValue!); + }, + ), + ), + ); + } +} diff --git a/lib/view_model/ionia/ionia_account_view_model.dart b/lib/view_model/ionia/ionia_account_view_model.dart deleted file mode 100644 index 384b75801..000000000 --- a/lib/view_model/ionia/ionia_account_view_model.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; - -part 'ionia_account_view_model.g.dart'; - -class IoniaAccountViewModel = IoniaAccountViewModelBase with _$IoniaAccountViewModel; - -abstract class IoniaAccountViewModelBase with Store { - IoniaAccountViewModelBase({required this.ioniaService}) - : email = '', - giftCards = [], - merchantState = InitialIoniaMerchantLoadingState() { - ioniaService.getUserEmail().then((email) => this.email = email); - updateUserGiftCards(); - } - - final IoniaService ioniaService; - - @observable - String email; - - @observable - List giftCards; - - @observable - IoniaMerchantState merchantState; - - @computed - int get countOfMerch => giftCards.where((giftCard) => !giftCard.isEmpty).length; - - @computed - List get activeMechs => giftCards.where((giftCard) => !giftCard.isEmpty).toList(); - - @computed - List get redeemedMerchs => giftCards.where((giftCard) => giftCard.isEmpty).toList(); - - @action - void logout() { - ioniaService.logout(); - } - - @action - Future updateUserGiftCards() async { - merchantState = IoniaLoadingMerchantState(); - giftCards = await ioniaService.getCurrentUserGiftCardSummaries(); - merchantState = IoniaLoadedMerchantState(); - } -} diff --git a/lib/view_model/ionia/ionia_auth_view_model.dart b/lib/view_model/ionia/ionia_auth_view_model.dart deleted file mode 100644 index a0c3ef6e8..000000000 --- a/lib/view_model/ionia/ionia_auth_view_model.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:mobx/mobx.dart'; - -part 'ionia_auth_view_model.g.dart'; - -class IoniaAuthViewModel = IoniaAuthViewModelBase with _$IoniaAuthViewModel; - -abstract class IoniaAuthViewModelBase with Store { - - IoniaAuthViewModelBase({required this.ioniaService}): - createUserState = IoniaInitialCreateState(), - signInState = IoniaInitialCreateState(), - otpState = IoniaOtpSendDisabled(), - email = '', - otp = ''; - - final IoniaService ioniaService; - - @observable - IoniaCreateAccountState createUserState; - - @observable - IoniaCreateAccountState signInState; - - @observable - IoniaOtpState otpState; - - @observable - String email; - - @observable - String otp; - - @action - Future verifyEmail(String code) async { - try { - otpState = IoniaOtpValidating(); - await ioniaService.verifyEmail(code); - otpState = IoniaOtpSuccess(); - } catch (_) { - otpState = IoniaOtpFailure(error: 'Invalid OTP. Try again'); - } - } - - @action - Future createUser(String email) async { - try { - createUserState = IoniaCreateStateLoading(); - await ioniaService.createUser(email); - createUserState = IoniaCreateStateSuccess(); - } catch (e) { - createUserState = IoniaCreateStateFailure(error: e.toString()); - } - } - - - @action - Future signIn(String email) async { - try { - signInState = IoniaCreateStateLoading(); - await ioniaService.signIn(email); - signInState = IoniaCreateStateSuccess(); - } catch (e) { - signInState = IoniaCreateStateFailure(error: e.toString()); - } - } - -} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_buy_card_view_model.dart b/lib/view_model/ionia/ionia_buy_card_view_model.dart deleted file mode 100644 index 4f5fb566c..000000000 --- a/lib/view_model/ionia/ionia_buy_card_view_model.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:mobx/mobx.dart'; - -part 'ionia_buy_card_view_model.g.dart'; - -class IoniaBuyCardViewModel = IoniaBuyCardViewModelBase with _$IoniaBuyCardViewModel; - -abstract class IoniaBuyCardViewModelBase with Store { - IoniaBuyCardViewModelBase({required this.ioniaMerchant}) - : isEnablePurchase = false, - amount = 0; - - final IoniaMerchant ioniaMerchant; - - @observable - double amount; - - @observable - bool isEnablePurchase; - - @action - void onAmountChanged(String input) { - if (input.isEmpty) return; - amount = double.parse(input.replaceAll(',', '.')); - final min = ioniaMerchant.minimumCardPurchase; - final max = ioniaMerchant.maximumCardPurchase; - - isEnablePurchase = amount >= min && amount <= max; - } -} diff --git a/lib/view_model/ionia/ionia_custom_redeem_view_model.dart b/lib/view_model/ionia/ionia_custom_redeem_view_model.dart deleted file mode 100644 index 5776443ee..000000000 --- a/lib/view_model/ionia/ionia_custom_redeem_view_model.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:mobx/mobx.dart'; -part 'ionia_custom_redeem_view_model.g.dart'; - -class IoniaCustomRedeemViewModel = IoniaCustomRedeemViewModelBase with _$IoniaCustomRedeemViewModel; - -abstract class IoniaCustomRedeemViewModelBase with Store { - IoniaCustomRedeemViewModelBase({ - required this.giftCard, - required this.ioniaService, - }) : amount = 0, - redeemState = InitialExecutionState(); - - final IoniaGiftCard giftCard; - - final IoniaService ioniaService; - - @observable - ExecutionState redeemState; - - @observable - double amount; - - @computed - double get remaining => - amount <= giftCard.remainingAmount ? giftCard.remainingAmount - amount : 0; - - @computed - String get formattedRemaining => remaining.toStringAsFixed(2); - - @computed - bool get disableRedeem => amount > giftCard.remainingAmount; - - @action - void updateAmount(String text) { - amount = double.tryParse(text.replaceAll(',', '.')) ?? 0; - } - - @action - Future addCustomRedeem() async { - try { - redeemState = IsExecutingState(); - await ioniaService.redeem(giftCardId: giftCard.id, amount: amount); - redeemState = ExecutedSuccessfullyState(); - } catch (e) { - redeemState = FailureState(e.toString()); - } - } -} diff --git a/lib/view_model/ionia/ionia_custom_tip_view_model.dart b/lib/view_model/ionia/ionia_custom_tip_view_model.dart deleted file mode 100644 index 452144b24..000000000 --- a/lib/view_model/ionia/ionia_custom_tip_view_model.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_tip.dart'; -import 'package:mobx/mobx.dart'; - -part 'ionia_custom_tip_view_model.g.dart'; - -class IoniaCustomTipViewModel = IoniaCustomTipViewModelBase with _$IoniaCustomTipViewModel; - -abstract class IoniaCustomTipViewModelBase with Store { - IoniaCustomTipViewModelBase({ - required this.amount, - required this.tip, - required this.ioniaMerchant}) - : customTip = tip, - percentage = 0; - - final IoniaMerchant ioniaMerchant; - final double amount; - final IoniaTip tip; - - @observable - IoniaTip customTip; - - @observable - double percentage; - - @action - void onTipChanged(String value){ - - final _amount = value.isEmpty ? 0 : double.parse(value.replaceAll(',', '.')); - percentage = _amount/amount * 100; - customTip = IoniaTip(percentage: percentage, originalAmount: amount); - } -} \ No newline at end of file diff --git a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart b/lib/view_model/ionia/ionia_gift_card_details_view_model.dart deleted file mode 100644 index cbf5ebc78..000000000 --- a/lib/view_model/ionia/ionia_gift_card_details_view_model.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:mobx/mobx.dart'; -import 'package:device_display_brightness/device_display_brightness.dart'; - -part 'ionia_gift_card_details_view_model.g.dart'; - -class IoniaGiftCardDetailsViewModel = IoniaGiftCardDetailsViewModelBase - with _$IoniaGiftCardDetailsViewModel; - -abstract class IoniaGiftCardDetailsViewModelBase with Store { - IoniaGiftCardDetailsViewModelBase({required this.ioniaService, required this.giftCard}) - : redeemState = InitialExecutionState(), - remainingAmount = giftCard.remainingAmount, - brightness = 0; - - final IoniaService ioniaService; - - double brightness; - - @observable - IoniaGiftCard giftCard; - - @observable - double remainingAmount; - - @observable - ExecutionState redeemState; - - @action - Future redeem() async { - giftCard.remainingAmount = remainingAmount; - try { - redeemState = IsExecutingState(); - await ioniaService.redeem(giftCardId: giftCard.id, amount: giftCard.remainingAmount); - giftCard = await ioniaService.getGiftCard(id: giftCard.id); - redeemState = ExecutedSuccessfullyState(); - } catch (e) { - redeemState = FailureState(e.toString()); - } - } - - @action - Future refeshCard() async { - giftCard = await ioniaService.getGiftCard(id: giftCard.id); - remainingAmount = giftCard.remainingAmount; - } - - void increaseBrightness() async { - brightness = await DeviceDisplayBrightness.getBrightness(); - await DeviceDisplayBrightness.setBrightness(1.0); - } -} diff --git a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart b/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart deleted file mode 100644 index b4974c420..000000000 --- a/lib/view_model/ionia/ionia_gift_cards_list_view_model.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:cake_wallet/ionia/ionia_category.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/ionia/ionia_create_state.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:mobx/mobx.dart'; -part 'ionia_gift_cards_list_view_model.g.dart'; - -class IoniaGiftCardsListViewModel = IoniaGiftCardsListViewModelBase with _$IoniaGiftCardsListViewModel; - -abstract class IoniaGiftCardsListViewModelBase with Store { - IoniaGiftCardsListViewModelBase({ - required this.ioniaService, - }) : - cardState = IoniaNoCardState(), - ioniaMerchants = [], - ioniaCategories = IoniaCategory.allCategories, - selectedIndices = ObservableList.of([IoniaCategory.all]), - scrollOffsetFromTop = 0.0, - merchantState = InitialIoniaMerchantLoadingState(), - createCardState = IoniaCreateCardState(), - searchString = '', - ioniaMerchantList = [] { - } - - final IoniaService ioniaService; - - List ioniaMerchantList; - - String searchString; - - @observable - double scrollOffsetFromTop; - - @observable - IoniaCreateCardState createCardState; - - @observable - IoniaFetchCardState cardState; - - @observable - IoniaMerchantState merchantState; - - @observable - List ioniaMerchants; - - @observable - List ioniaCategories; - - @observable - ObservableList selectedIndices; - - @action - Future createCard() async { - try { - createCardState = IoniaCreateCardLoading(); - await ioniaService.createCard(); - createCardState = IoniaCreateCardSuccess(); - } catch (e) { - createCardState = IoniaCreateCardFailure(error: e.toString()); - } - } - - @action - void searchMerchant(String text) { - if (text.isEmpty) { - ioniaMerchants = ioniaMerchantList; - return; - } - searchString = text; - ioniaService.getMerchantsByFilter(search: searchString).then((value) { - ioniaMerchants = value; - }); - } - - Future _getCard() async { - cardState = IoniaFetchingCard(); - try { - final card = await ioniaService.getCard(); - - cardState = IoniaCardSuccess(card: card); - } catch (_) { - cardState = IoniaFetchCardFailure(); - } - } - - - void getMerchants() { - merchantState = IoniaLoadingMerchantState(); - ioniaService.getMerchantsByFilter(categories: selectedIndices).then((value) { - value.sort((a, b) => a.legalName.toLowerCase().compareTo(b.legalName.toLowerCase())); - ioniaMerchants = ioniaMerchantList = value; - merchantState = IoniaLoadedMerchantState(); - }); - - } - - @action - void setSelectedFilter(IoniaCategory category) { - if (category == IoniaCategory.all) { - selectedIndices.clear(); - selectedIndices.add(category); - return; - } - - if (category != IoniaCategory.all) { - selectedIndices.remove(IoniaCategory.all); - } - - if (selectedIndices.contains(category)) { - selectedIndices.remove(category); - - if (selectedIndices.isEmpty) { - selectedIndices.add(IoniaCategory.all); - } - return; - } - selectedIndices.add(category); - } - - @action - void onSearchFilter(String text) { - if (text.isEmpty) { - ioniaCategories = IoniaCategory.allCategories; - } else { - ioniaCategories = IoniaCategory.allCategories - .where((e) => e.title.toLowerCase().contains(text.toLowerCase()),) - .toList(); - } - } - - @action - void resetIoniaCategories() { - ioniaCategories = IoniaCategory.allCategories; - } - - void setScrollOffsetFromTop(double scrollOffset) { - scrollOffsetFromTop = scrollOffset; - } -} diff --git a/lib/view_model/ionia/ionia_payment_status_view_model.dart b/lib/view_model/ionia/ionia_payment_status_view_model.dart deleted file mode 100644 index 8f43e0244..000000000 --- a/lib/view_model/ionia/ionia_payment_status_view_model.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:async'; -import 'package:cake_wallet/anypay/any_pay_chain.dart'; -import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cake_wallet/ionia/ionia_service.dart'; -import 'package:cake_wallet/ionia/ionia_gift_card.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; - -part 'ionia_payment_status_view_model.g.dart'; - -class IoniaPaymentStatusViewModel = IoniaPaymentStatusViewModelBase with _$IoniaPaymentStatusViewModel; - -abstract class IoniaPaymentStatusViewModelBase with Store { - IoniaPaymentStatusViewModelBase( - this.ioniaService, { - required this.paymentInfo, - required this.committedInfo}) - : error = '' { - _timer = Timer.periodic(updateTime, (timer) async { - await updatePaymentStatus(); - - if (giftCard != null) { - timer?.cancel(); - } - }); - } - - static const updateTime = Duration(seconds: 3); - - final IoniaService ioniaService; - final IoniaAnyPayPaymentInfo paymentInfo; - final AnyPayPaymentCommittedInfo committedInfo; - - @observable - IoniaGiftCard? giftCard; - - @observable - String error; - - Timer? get timer => _timer; - - bool get payingByBitcoin => paymentInfo.anyPayPayment.chain == AnyPayChain.btc; - - Timer? _timer; - - @action - Future updatePaymentStatus() async { - try { - final giftCardId = await ioniaService.getPaymentStatus( - orderId: paymentInfo.ioniaOrder.id, - paymentId: paymentInfo.ioniaOrder.paymentId); - - if (giftCardId != null) { - giftCard = await ioniaService.getGiftCard(id: giftCardId); - } - - } catch (e) { - error = e.toString(); - } - } -} diff --git a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart b/lib/view_model/ionia/ionia_purchase_merch_view_model.dart deleted file mode 100644 index df6a23718..000000000 --- a/lib/view_model/ionia/ionia_purchase_merch_view_model.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/anypay/any_pay_payment.dart'; -import 'package:cake_wallet/anypay/any_pay_payment_committed_info.dart'; -import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/ionia/ionia_anypay.dart'; -import 'package:cake_wallet/ionia/ionia_merchant.dart'; -import 'package:cake_wallet/ionia/ionia_tip.dart'; -import 'package:cake_wallet/ionia/ionia_any_pay_payment_info.dart'; -import 'package:cake_wallet/view_model/send/send_view_model.dart'; - -part 'ionia_purchase_merch_view_model.g.dart'; - -class IoniaMerchPurchaseViewModel = IoniaMerchPurchaseViewModelBase with _$IoniaMerchPurchaseViewModel; - -abstract class IoniaMerchPurchaseViewModelBase with Store { - IoniaMerchPurchaseViewModelBase({ - required this.ioniaAnyPayService, - required this.amount, - required this.ioniaMerchant, - required this.sendViewModel, - }) : tipAmount = 0.0, - percentage = 0.0, - invoiceCreationState = InitialExecutionState(), - invoiceCommittingState = InitialExecutionState(), - tips = [ - IoniaTip(percentage: 0, originalAmount: amount), - IoniaTip(percentage: 15, originalAmount: amount), - IoniaTip(percentage: 18, originalAmount: amount), - IoniaTip(percentage: 20, originalAmount: amount), - IoniaTip(percentage: 0, originalAmount: amount, isCustom: true), - ] { - selectedTip = tips.first; - } - - final double amount; - - List tips; - - @observable - IoniaTip? selectedTip; - - final IoniaMerchant ioniaMerchant; - - final SendViewModel sendViewModel; - - final IoniaAnyPay ioniaAnyPayService; - - IoniaAnyPayPaymentInfo? paymentInfo; - - AnyPayPayment? get invoice => paymentInfo?.anyPayPayment; - - AnyPayPaymentCommittedInfo? committedInfo; - - @observable - ExecutionState invoiceCreationState; - - @observable - ExecutionState invoiceCommittingState; - - @observable - double percentage; - - @computed - double get giftCardAmount => double.parse((amount + tipAmount).toStringAsFixed(2)); - - @computed - double get billAmount => double.parse((giftCardAmount * (1 - (ioniaMerchant.discount / 100))).toStringAsFixed(2)); - - @observable - double tipAmount; - - @action - void addTip(IoniaTip tip) { - tipAmount = tip.additionalAmount; - selectedTip = tip; - } - - @action - Future createInvoice() async { - try { - invoiceCreationState = IsExecutingState(); - paymentInfo = await ioniaAnyPayService.purchase(merchId: ioniaMerchant.id.toString(), amount: giftCardAmount); - invoiceCreationState = ExecutedSuccessfullyState(); - } catch (e) { - invoiceCreationState = FailureState(e.toString()); - } - } - - @action - Future commitPaymentInvoice() async { - try { - if (invoice == null) { - throw Exception('Invoice is created. Invoince is null'); - } - - invoiceCommittingState = IsExecutingState(); - committedInfo = await ioniaAnyPayService.commitInvoice(invoice!); - invoiceCommittingState = ExecutedSuccessfullyState(payload: committedInfo!); - } catch (e) { - invoiceCommittingState = FailureState(e.toString()); - } - } -} diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index b37345ca3..25d392d6a 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -87,6 +87,7 @@ "buy": "اشتري", "buy_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Monero ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .", "buy_bitcoin": "شراء Bitcoin", + "buy_now": "اشتري الآن", "buy_provider_unavailable": "مزود حاليا غير متوفر.", "buy_with": "اشتر بواسطة", "by_cake_pay": "عن طريق Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "موضوع الكعكة الظلام", "cake_pay_account_note": "قم بالتسجيل باستخدام عنوان بريد إلكتروني فقط لمشاهدة البطاقات وشرائها. حتى أن بعضها متوفر بسعر مخفض!", "cake_pay_learn_more": "شراء واسترداد بطاقات الهدايا على الفور في التطبيق!\nاسحب من اليسار إلى اليمين لمعرفة المزيد.", - "cake_pay_subtitle": "شراء بطاقات هدايا مخفضة السعر (الولايات المتحدة فقط)", - "cake_pay_title": "بطاقات هدايا Cake Pay", + "cake_pay_subtitle": "شراء بطاقات مسبقة الدفع وبطاقات الهدايا في جميع أنحاء العالم", "cake_pay_web_cards_subtitle": "اشتري بطاقات مدفوعة مسبقا وبطاقات هدايا في جميع أنحاء العالم", "cake_pay_web_cards_title": "بطاقات Cake Pay Web", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "تغيير المحفظة الحالية", "choose_account": "اختر حساب", "choose_address": "\n\nالرجاء اختيار عنوان:", + "choose_card_value": "اختر قيمة بطاقة", "choose_derivation": "اختر اشتقاق المحفظة", "choose_from_available_options": "اختر من بين الخيارات المتاحة:", "choose_one": "اختر واحدة", @@ -166,6 +167,7 @@ "copy_address": "نسخ العنوان", "copy_id": "نسخ معرف العملية", "copyWalletConnectLink": "ﺎﻨﻫ ﻪﻘﺼﻟﺍﻭ dApp ﻦﻣ WalletConnect ﻂﺑﺍﺭ ﺦﺴﻧﺍ", + "countries": "بلدان", "create_account": "إنشاء حساب", "create_backup": "انشئ نسخة احتياطية", "create_donation_link": "إنشاء رابط التبرع", @@ -178,6 +180,7 @@ "custom": "مخصصة", "custom_drag": "مخصص (عقد وسحب)", "custom_redeem_amount": "مبلغ الاسترداد مخصص", + "custom_value": "القيمة الجمركية", "dark_theme": "داكن", "debit_card": "بطاقة ائتمان", "debit_card_terms": "يخضع تخزين واستخدام رقم بطاقة الدفع الخاصة بك (وبيانات الاعتماد المقابلة لرقم بطاقة الدفع الخاصة بك) في هذه المحفظة الرقمية لشروط وأحكام اتفاقية حامل البطاقة المعمول بها مع جهة إصدار بطاقة الدفع ، كما هو معمول به من وقت لآخر.", @@ -190,6 +193,7 @@ "delete_wallet": "حذف المحفظة", "delete_wallet_confirm_message": "هل أنت متأكد أنك تريد حذف محفظة ${wallet_name}؟", "deleteConnectionConfirmationPrompt": "ـﺑ ﻝﺎﺼﺗﻻﺍ ﻑﺬﺣ ﺪﻳﺮﺗ ﻚﻧﺃ ﺪﻛﺄﺘﻣ ﺖﻧﺃ ﻞﻫ", + "denominations": "الطوائف", "descending": "النزول", "description": "ﻒﺻﻭ", "destination_tag": "علامة الوجهة:", @@ -277,6 +281,7 @@ "expired": "منتهي الصلاحية", "expires": "تنتهي", "expiresOn": "ﻲﻓ ﻪﺘﻴﺣﻼﺻ ﻲﻬﺘﻨﺗ", + "expiry_and_validity": "انتهاء الصلاحية والصلاحية", "export_backup": "تصدير نسخة احتياطية", "extra_id": "معرف إضافي:", "extracted_address_content": "سوف ترسل الأموال إلى\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "قالب جديد", "new_wallet": "إنشاء محفظة جديدة", "newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ", + "no_cards_found": "لم يتم العثور على بطاقات", "no_id_needed": "لا حاجة لID!", "no_id_required": "لا ID مطلوب. اشحن وانفق في أي مكان", "no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ", @@ -452,6 +458,7 @@ "pre_seed_button_text": "انا أفهم. أرني سييد الخاص بي", "pre_seed_description": "في الصفحة التالية ستشاهد سلسلة من الكلمات ${words}. هذه هي سييد الفريدة والخاصة بك وهي الطريقة الوحيدة لاسترداد محفظتك في حالة فقدها أو عطلها. تقع على عاتقك مسؤولية تدوينها وتخزينها في مكان آمن خارج تطبيق Cake Wallet.", "pre_seed_title": "مهم", + "prepaid_cards": "البطاقات المدفوعة مسبقا", "prevent_screenshots": "منع لقطات الشاشة وتسجيل الشاشة", "privacy": "خصوصية", "privacy_policy": "سياسة الخصوصية", @@ -467,6 +474,7 @@ "purple_dark_theme": "موضوع الظلام الأرجواني", "qr_fullscreen": "انقر لفتح ال QR بملء الشاشة", "qr_payment_amount": "يحتوي هذا ال QR على مبلغ الدفع. هل تريد تغير المبلغ فوق القيمة الحالية؟", + "quantity": "كمية", "question_to_disable_2fa": "هل أنت متأكد أنك تريد تعطيل Cake 2FA؟ لن تكون هناك حاجة إلى رمز 2FA للوصول إلى المحفظة ووظائف معينة.", "receivable_balance": "التوازن القادم", "receive": "استلام", @@ -708,6 +716,7 @@ "tokenID": "ﻒﻳﺮﻌﺗ ﺔﻗﺎﻄﺑ", "tor_connection": "ﺭﻮﺗ ﻝﺎﺼﺗﺍ", "tor_only": "Tor فقط", + "total": "المجموع", "total_saving": "إجمالي المدخرات", "totp_2fa_failure": "شفرة خاطئة. يرجى تجربة رمز مختلف أو إنشاء مفتاح سري جديد. استخدم تطبيق 2FA متوافقًا يدعم الرموز المكونة من 8 أرقام و SHA512.", "totp_2fa_success": "نجاح! تم تمكين Cake 2FA لهذه المحفظة. تذكر حفظ بذرة ذاكري في حالة فقد الوصول إلى المحفظة.", @@ -798,6 +807,8 @@ "use_ssl": "استخدم SSL", "use_suggested": "استخدام المقترح", "use_testnet": "استخدم testnet", + "value": "قيمة", + "value_type": "نوع القيمة", "variable_pair_not_supported": "هذا الزوج المتغير غير مدعوم في التبادلات المحددة", "verification": "تَحَقّق", "verify_with_2fa": "تحقق مع Cake 2FA", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 4b145d8fa..8f4717081 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -87,6 +87,7 @@ "buy": "Купуване", "buy_alert_content": "В момента поддържаме само закупуването на Bitcoin, Ethereum, Litecoin и Monero. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum, Litecoin или Monero.", "buy_bitcoin": "Купуване на Bitcoin", + "buy_now": "Купи сега", "buy_provider_unavailable": "Понастоящем доставчик не е наличен.", "buy_with": "Купуване чрез", "by_cake_pay": "от Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Торта тъмна тема", "cake_pay_account_note": "Регистрайте се само с един имейл, за да виждате и купувате карти. За някои има дори и отстъпка!", "cake_pay_learn_more": "Купете и използвайте гифткарти директно в приложението!\nПлъзнете отляво надясно, за да научите още.", - "cake_pay_subtitle": "Купете гифткарти на намалени цени (само за САЩ)", - "cake_pay_title": "Cake Pay Gift Карти", + "cake_pay_subtitle": "Купете предплатени карти и карти за подаръци в световен мащаб", "cake_pay_web_cards_subtitle": "Купете световно признати предплатени и гифт карти", "cake_pay_web_cards_title": "Cake Pay Онлайн Карти", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Смяна на сегашния портфейл", "choose_account": "Избиране на профил", "choose_address": "\n\nМоля, изберете адреса:", + "choose_card_value": "Изберете стойност на картата", "choose_derivation": "Изберете производно на портфейла", "choose_from_available_options": "Изберете от следните опции:", "choose_one": "Изберете едно", @@ -166,6 +167,7 @@ "copy_address": "Copy Address", "copy_id": "Копиране на ID", "copyWalletConnectLink": "Копирайте връзката WalletConnect от dApp и я поставете тук", + "countries": "Държави", "create_account": "Създаване на профил", "create_backup": "Създаване на резервно копие", "create_donation_link": "Създайте връзка за дарение", @@ -178,6 +180,7 @@ "custom": "персонализирано", "custom_drag": "Персонализиране (задръжте и плъзнете)", "custom_redeem_amount": "Персонализирана сума за използване", + "custom_value": "Персонализирана стойност", "dark_theme": "Тъмно", "debit_card": "Дебитна карта", "debit_card_terms": "Съхранението и използването на данните от вашата платежна карта в този дигитален портфейл подлежат на условията на съответното съгласие за картодържец от издателя на картата.", @@ -190,6 +193,7 @@ "delete_wallet": "Изтриване на портфейл", "delete_wallet_confirm_message": "Сигурни ли сте, че искате да изтриете протфейла ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Сигурни ли сте, че искате да изтриете връзката към", + "denominations": "Деноминации", "descending": "Низходящ", "description": "Описание", "destination_tag": "Destination tag:", @@ -277,6 +281,7 @@ "expired": "Изтекло", "expires": "Изтича", "expiresOn": "Изтича на", + "expiry_and_validity": "Изтичане и валидност", "export_backup": "Експортиране на резервно копие", "extra_id": "Допълнително ID:", "extracted_address_content": "Ще изпратите средства на \n${recipient_name}", @@ -308,7 +313,7 @@ "gift_card_is_generated": "Gift Card бе създадена", "gift_card_number": "Номер на Gift Card", "gift_card_redeemed_note": "Използваните гифткарти ще се покажат тук", - "gift_cards": "Gift Карти", + "gift_cards": "Карти за подаръци", "gift_cards_unavailable": "В момента гифткарти могат да бъдат закупени само с Monero, Bitcoin и Litecoin", "got_it": "Готово", "gross_balance": "Брутен баланс", @@ -384,6 +389,7 @@ "new_template": "Нов шаблон", "new_wallet": "Нов портфейл", "newConnection": "Нова връзка", + "no_cards_found": "Не са намерени карти", "no_id_needed": "Без нужда от документ за самоличност!", "no_id_required": "Без нужда от документ за самоличност. Използвайте навсякъде", "no_relay_on_domain": "Няма реле за домейна на потребителя или релето не е налично. Моля, изберете реле, което да използвате.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Разбирам. Покажи seed", "pre_seed_description": "На следващата страница ще видите поредица от ${words} думи. Това е вашият таен личен seed и е единственият начин да възстановите портфейла си. Отговорността за съхранението му на сигурно място извън приложението на Cake Wallet е изцяло ВАША.", "pre_seed_title": "ВАЖНО", + "prepaid_cards": "Предплатени карти", "prevent_screenshots": "Предотвратете екранни снимки и запис на екрана", "privacy": "Поверителност", "privacy_policy": "Политика за поверителността", @@ -467,6 +474,7 @@ "purple_dark_theme": "Лилава тъмна тема", "qr_fullscreen": "Натиснете, за да отворите QR кода на цял екран", "qr_payment_amount": "Този QR код съдържа сума за плащане. Искате ли да промените стойността?", + "quantity": "Количество", "question_to_disable_2fa": "Сигурни ли сте, че искате да деактивирате Cake 2FA? Вече няма да е необходим 2FA код за достъп до портфейла и определени функции.", "receivable_balance": "Баланс за вземания", "receive": "Получи", @@ -708,6 +716,7 @@ "tokenID": "документ за самоличност", "tor_connection": "Tor връзка", "tor_only": "Само чрез Tor", + "total": "Обща сума", "total_saving": "Общо спестявания", "totp_2fa_failure": "Грешен код. Моля, опитайте с различен код или генерирайте нов таен ключ. Използвайте съвместимо 2FA приложение, което поддържа 8-цифрени кодове и SHA512.", "totp_2fa_success": "Успех! Cake 2FA е активиран за този портфейл. Не забравяйте да запазите мнемоничното начало, в случай че загубите достъп до портфейла.", @@ -798,6 +807,8 @@ "use_ssl": "Използване на SSL", "use_suggested": "Използване на предложеното", "use_testnet": "Използвайте TestNet", + "value": "Стойност", + "value_type": "Тип стойност", "variable_pair_not_supported": "Този variable pair не се поддържа от избраната борса", "verification": "Потвърждаване", "verify_with_2fa": "Проверете с Cake 2FA", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 35a4966a0..297a737a2 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -87,6 +87,7 @@ "buy": "Koupit", "buy_alert_content": "V současné době podporujeme pouze nákup bitcoinů, etherea, litecoinů a monero. Vytvořte nebo přepněte na svou peněženku bitcoinů, etherea, litecoinů nebo monero.", "buy_bitcoin": "Nakoupit Bitcoin", + "buy_now": "Kup nyní", "buy_provider_unavailable": "Poskytovatel aktuálně nedostupný.", "buy_with": "Nakoupit pomocí", "by_cake_pay": "od Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Dort tmavé téma", "cake_pay_account_note": "Přihlaste se svou e-mailovou adresou pro zobrazení a nákup karet. Některé jsou dostupné ve slevě!", "cake_pay_learn_more": "Okamžitý nákup a uplatnění dárkových karet v aplikaci!\nPřejeďte prstem zleva doprava pro další informace.", - "cake_pay_subtitle": "Kupte si zlevněné dárkové karty (pouze USA)", - "cake_pay_title": "Cake Pay dárkové karty", + "cake_pay_subtitle": "Kupte si celosvětové předplacené karty a dárkové karty", "cake_pay_web_cards_subtitle": "Kupte si celosvětové předplacené a dárkové karty", "cake_pay_web_cards_title": "Cake Pay webové karty", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Přepnout peněženku", "choose_account": "Zvolte částku", "choose_address": "\n\nProsím vyberte adresu:", + "choose_card_value": "Vyberte hodnotu karty", "choose_derivation": "Vyberte derivaci peněženky", "choose_from_available_options": "Zvolte si z dostupných možností:", "choose_one": "Zvolte si", @@ -166,6 +167,7 @@ "copy_address": "Zkopírovat adresu", "copy_id": "Kopírovat ID", "copyWalletConnectLink": "Zkopírujte odkaz WalletConnect z dApp a vložte jej sem", + "countries": "Země", "create_account": "Vytvořit účet", "create_backup": "Vytvořit zálohu", "create_donation_link": "Vytvořit odkaz na darování", @@ -178,6 +180,7 @@ "custom": "vlastní", "custom_drag": "Custom (Hold and Drag)", "custom_redeem_amount": "Vlastní částka pro uplatnění", + "custom_value": "Vlastní hodnota", "dark_theme": "Tmavý", "debit_card": "Debetní karta", "debit_card_terms": "Uložení a použití vašeho čísla platební karty (a přihlašovací údaje k vašemu číslu karty) v této digitální peněžence se řídí Obchodními podmínkami smlouvy příslušného držitele karty s vydavatelem karty (v jejich nejaktuálnější verzi).", @@ -190,6 +193,7 @@ "delete_wallet": "Smazat peněženku", "delete_wallet_confirm_message": "Opravdu chcete smazat ${wallet_name} peněženku?", "deleteConnectionConfirmationPrompt": "Jste si jisti, že chcete smazat připojení k?", + "denominations": "Označení", "descending": "Klesající", "description": "Popis", "destination_tag": "Destination Tag:", @@ -277,6 +281,7 @@ "expired": "Vypršelo", "expires": "Vyprší", "expiresOn": "Vyprší dne", + "expiry_and_validity": "Vypršení a platnost", "export_backup": "Exportovat zálohu", "extra_id": "Extra ID:", "extracted_address_content": "Prostředky budete posílat na\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Nová šablona", "new_wallet": "Nová peněženka", "newConnection": "Nové připojení", + "no_cards_found": "Žádné karty nenalezeny", "no_id_needed": "Žádné ID není potřeba!", "no_id_required": "Žádní ID není potřeba. Dobijte si a utrácejte kdekoliv", "no_relay_on_domain": "Pro doménu uživatele neexistuje přenos nebo je přenos nedostupný. Vyberte relé, které chcete použít.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Rozumím. Ukaž mi můj seed.", "pre_seed_description": "Na následující stránce uvidíte sérii ${words} slov. Je to váš tzv. seed a je to JEDINÁ možnost, jak můžete později obnovit svou peněženku v případě ztráty nebo poruchy. Je VAŠÍ zodpovědností zapsat si ho a uložit si ho na bezpečném místě mimo aplikaci Cake Wallet.", "pre_seed_title": "DŮLEŽITÉ", + "prepaid_cards": "Předplacené karty", "prevent_screenshots": "Zabránit vytváření snímků obrazovky a nahrávání obrazovky", "privacy": "Soukromí", "privacy_policy": "Zásady ochrany soukromí", @@ -467,6 +474,7 @@ "purple_dark_theme": "Fialové temné téma", "qr_fullscreen": "Poklepáním otevřete QR kód na celé obrazovce", "qr_payment_amount": "Tento QR kód obsahuje i částku. Chcete přepsat současnou hodnotu?", + "quantity": "Množství", "question_to_disable_2fa": "Opravdu chcete deaktivovat Cake 2FA? Pro přístup k peněžence a některým funkcím již nebude potřeba kód 2FA.", "receivable_balance": "Zůstatek pohledávek", "receive": "Přijmout", @@ -708,6 +716,7 @@ "tokenID": "ID", "tor_connection": "Připojení Tor", "tor_only": "Pouze Tor", + "total": "Celkový", "total_saving": "Celkem ušetřeno", "totp_2fa_failure": "Nesprávný kód. Zkuste prosím jiný kód nebo vygenerujte nový tajný klíč. Použijte kompatibilní aplikaci 2FA, která podporuje 8místné kódy a SHA512.", "totp_2fa_success": "Úspěch! Pro tuto peněženku povolen Cake 2FA. Nezapomeňte si uložit mnemotechnický klíč pro případ, že ztratíte přístup k peněžence.", @@ -798,6 +807,8 @@ "use_ssl": "Použít SSL", "use_suggested": "Použít doporučený", "use_testnet": "Použijte testNet", + "value": "Hodnota", + "value_type": "Typ hodnoty", "variable_pair_not_supported": "Tento pár s tržním kurzem není ve zvolené směnárně podporován", "verification": "Ověření", "verify_with_2fa": "Ověřte pomocí Cake 2FA", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 1877c162a..2ccd1919c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -87,6 +87,7 @@ "buy": "Kaufen", "buy_alert_content": "Derzeit unterstützen wir nur den Kauf von Bitcoin, Ethereum, Litecoin und Monero. Bitte erstellen Sie Ihr Bitcoin-, Ethereum-, Litecoin- oder Monero-Wallet oder wechseln Sie zu diesem.", "buy_bitcoin": "Bitcoin kaufen", + "buy_now": "Kaufe jetzt", "buy_provider_unavailable": "Anbieter derzeit nicht verfügbar.", "buy_with": "Kaufen mit", "by_cake_pay": "von Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake Dark Thema", "cake_pay_account_note": "Melden Sie sich nur mit einer E-Mail-Adresse an, um Karten anzuzeigen und zu kaufen. Einige sind sogar mit Rabatt erhältlich!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", - "cake_pay_subtitle": "Kaufen Sie ermäßigte Geschenkkarten (nur USA)", - "cake_pay_title": "Cake Pay-Geschenkkarten", + "cake_pay_subtitle": "Kaufen Sie weltweite Prepaid -Karten und Geschenkkarten", "cake_pay_web_cards_subtitle": "Kaufen Sie weltweit Prepaid-Karten und Geschenkkarten", "cake_pay_web_cards_title": "Cake Pay-Webkarten", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Aktuelle Wallet ändern", "choose_account": "Konto auswählen", "choose_address": "\n\nBitte wählen Sie die Adresse:", + "choose_card_value": "Wählen Sie einen Kartenwert", "choose_derivation": "Wählen Sie Wallet-Ableitung", "choose_from_available_options": "Wähle aus verfügbaren Optionen:", "choose_one": "Wähle ein", @@ -166,6 +167,7 @@ "copy_address": "Adresse kopieren", "copy_id": "ID kopieren", "copyWalletConnectLink": "Kopieren Sie den WalletConnect-Link von dApp und fügen Sie ihn hier ein", + "countries": "Länder", "create_account": "Konto erstellen", "create_backup": "Backup erstellen", "create_donation_link": "Spendenlink erstellen", @@ -178,6 +180,7 @@ "custom": "benutzerdefiniert", "custom_drag": "Custom (Hold and Drag)", "custom_redeem_amount": "Benutzerdefinierter Einlösungsbetrag", + "custom_value": "Benutzerdefinierten Wert", "dark_theme": "Dunkel", "debit_card": "Debitkarte", "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Geldbörse unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", @@ -190,6 +193,7 @@ "delete_wallet": "Wallet löschen", "delete_wallet_confirm_message": "Sind Sie sicher, dass Sie das ${wallet_name} Wallet löschen möchten?", "deleteConnectionConfirmationPrompt": "Sind Sie sicher, dass Sie die Verbindung zu löschen möchten?", + "denominations": "Konfessionen", "descending": "Absteigend", "description": "Beschreibung", "destination_tag": "Ziel-Tag:", @@ -277,6 +281,7 @@ "expired": "Abgelaufen", "expires": "Läuft ab", "expiresOn": "Läuft aus am", + "expiry_and_validity": "Ablauf und Gültigkeit", "export_backup": "Sicherung exportieren", "extra_id": "Extra ID:", "extracted_address_content": "Sie senden Geld an\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "neue Vorlage", "new_wallet": "Neue Wallet", "newConnection": "Neue Verbindung", + "no_cards_found": "Keine Karten gefunden", "no_id_needed": "Keine ID erforderlich!", "no_id_required": "Keine ID erforderlich. Upgraden und überall ausgeben", "no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus.", @@ -442,8 +448,8 @@ "placeholder_transactions": "Ihre Transaktionen werden hier angezeigt", "please_fill_totp": "Bitte geben Sie den 8-stelligen Code ein, der auf Ihrem anderen Gerät vorhanden ist", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", - "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", "Please_reference_document": "Weitere Informationen finden Sie in den Dokumenten unten.", + "please_reference_document": "Bitte verweisen Sie auf die folgenden Dokumente, um weitere Informationen zu erhalten.", "please_select": "Bitte auswählen:", "please_select_backup_file": "Bitte wählen Sie die Sicherungsdatei und geben Sie das Sicherungskennwort ein.", "please_try_to_connect_to_another_node": "Bitte versuchen Sie, sich mit einem anderen Knoten zu verbinden", @@ -453,6 +459,7 @@ "pre_seed_button_text": "Verstanden. Zeig mir meinen Seed", "pre_seed_description": "Auf der nächsten Seite sehen Sie eine Reihe von ${words} Wörtern. Dies ist Ihr einzigartiger und privater Seed und der EINZIGE Weg, um Ihre Wallet im Falle eines Verlusts oder einer Fehlfunktion wiederherzustellen. Es liegt in IHRER Verantwortung, ihn aufzuschreiben und an einem sicheren Ort außerhalb der Cake Wallet-App aufzubewahren.", "pre_seed_title": "WICHTIG", + "prepaid_cards": "Karten mit Guthaben", "prevent_screenshots": "Verhindern Sie Screenshots und Bildschirmaufzeichnungen", "privacy": "Datenschutz", "privacy_policy": "Datenschutzrichtlinie", @@ -468,6 +475,7 @@ "purple_dark_theme": "Lila dunkle Thema", "qr_fullscreen": "Tippen Sie hier, um den QR-Code im Vollbildmodus zu öffnen", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Menge", "question_to_disable_2fa": "Sind Sie sicher, dass Sie Cake 2FA deaktivieren möchten? Für den Zugriff auf die Wallet und bestimmte Funktionen wird kein 2FA-Code mehr benötigt.", "receivable_balance": "Forderungsbilanz", "receive": "Empfangen", @@ -709,6 +717,7 @@ "tokenID": "AUSWEIS", "tor_connection": "Tor-Verbindung", "tor_only": "Nur Tor", + "total": "Gesamt", "total_saving": "Gesamteinsparungen", "totp_2fa_failure": "Falscher Code. Bitte versuchen Sie es mit einem anderen Code oder generieren Sie einen neuen geheimen Schlüssel. Verwenden Sie eine kompatible 2FA-App, die 8-stellige Codes und SHA512 unterstützt.", "totp_2fa_success": "Erfolg! Cake 2FA für dieses Wallet aktiviert. Denken Sie daran, Ihren mnemonischen Seed zu speichern, falls Sie den Zugriff auf die Wallet verlieren.", @@ -800,6 +809,8 @@ "use_ssl": "SSL verwenden", "use_suggested": "Vorgeschlagen verwenden", "use_testnet": "TESTNET verwenden", + "value": "Wert", + "value_type": "Werttyp", "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt", "verification": "Verifizierung", "verify_with_2fa": "Verifizieren Sie mit Cake 2FA", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 13408529a..9b8662bdb 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -87,6 +87,7 @@ "buy": "Buy", "buy_alert_content": "Currently we only support the purchase of Bitcoin, Ethereum, Litecoin, and Monero. Please create or switch to your Bitcoin, Ethereum, Litecoin, or Monero wallet.", "buy_bitcoin": "Buy Bitcoin", + "buy_now": "Buy Now", "buy_provider_unavailable": "Provider currently unavailable.", "buy_with": "Buy with", "by_cake_pay": "by Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake Dark Theme", "cake_pay_account_note": "Sign up with just an email address to see and purchase cards. Some are even available at a discount!", "cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.", - "cake_pay_subtitle": "Buy discounted gift cards (USA only)", - "cake_pay_title": "Cake Pay Gift Cards", + "cake_pay_subtitle": "Buy worldwide prepaid cards and gift cards", "cake_pay_web_cards_subtitle": "Buy worldwide prepaid cards and gift cards", "cake_pay_web_cards_title": "Cake Pay Web Cards", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Change current wallet", "choose_account": "Choose account", "choose_address": "\n\nPlease choose the address:", + "choose_card_value": "Choose a card value", "choose_derivation": "Choose Wallet Derivation", "choose_from_available_options": "Choose from the available options:", "choose_one": "Choose one", @@ -166,6 +167,7 @@ "copy_address": "Copy Address", "copy_id": "Copy ID", "copyWalletConnectLink": "Copy the WalletConnect link from dApp and paste here", + "countries": "Countries", "create_account": "Create Account", "create_backup": "Create backup", "create_donation_link": "Create donation link", @@ -178,6 +180,7 @@ "custom": "Custom", "custom_drag": "Custom (Hold and Drag)", "custom_redeem_amount": "Custom Redeem Amount", + "custom_value": "Custom Value", "dark_theme": "Dark", "debit_card": "Debit Card", "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", @@ -190,6 +193,7 @@ "delete_wallet": "Delete wallet", "delete_wallet_confirm_message": "Are you sure that you want to delete ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Are you sure that you want to delete the connection to", + "denominations": "Denominations", "descending": "Descending", "description": "Description", "destination_tag": "Destination tag:", @@ -277,6 +281,7 @@ "expired": "Expired", "expires": "Expires", "expiresOn": "Expires on", + "expiry_and_validity": "Expiry and Validity", "export_backup": "Export backup", "extra_id": "Extra ID:", "extracted_address_content": "You will be sending funds to\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "New Template", "new_wallet": "New Wallet", "newConnection": "New Connection", + "no_cards_found": "No cards found", "no_id_needed": "No ID needed!", "no_id_required": "No ID required. Top up and spend anywhere", "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "I understand. Show me my seed", "pre_seed_description": "On the next page you will see a series of ${words} words. This is your unique and private seed and it is the ONLY way to recover your wallet in case of loss or malfunction. It is YOUR responsibility to write it down and store it in a safe place outside of the Cake Wallet app.", "pre_seed_title": "IMPORTANT", + "prepaid_cards": "Prepaid Cards", "prevent_screenshots": "Prevent screenshots and screen recording", "privacy": "Privacy", "privacy_policy": "Privacy Policy", @@ -467,6 +474,7 @@ "purple_dark_theme": "Purple Dark Theme", "qr_fullscreen": "Tap to open full screen QR code", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Quantity", "question_to_disable_2fa": "Are you sure that you want to disable Cake 2FA? A 2FA code will no longer be needed to access the wallet and certain functions.", "receivable_balance": "Receivable Balance", "receive": "Receive", @@ -708,6 +716,7 @@ "tokenID": "ID", "tor_connection": "Tor connection", "tor_only": "Tor only", + "total": "Total", "total_saving": "Total Savings", "totp_2fa_failure": "Incorrect code. Please try a different code or generate a new secret key. Use a compatible 2FA app that supports 8-digit codes and SHA512.", "totp_2fa_success": "Success! Cake 2FA enabled for this wallet. Remember to save your mnemonic seed in case you lose wallet access.", @@ -798,6 +807,8 @@ "use_ssl": "Use SSL", "use_suggested": "Use Suggested", "use_testnet": "Use Testnet", + "value": "Value", + "value_type": "Value Type", "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges", "verification": "Verification", "verify_with_2fa": "Verify with Cake 2FA", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 50d48bb98..45da02030 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -87,6 +87,7 @@ "buy": "Comprar", "buy_alert_content": "Actualmente solo admitimos la compra de Bitcoin, Ethereum, Litecoin y Monero. Cree o cambie a su billetera Bitcoin, Ethereum, Litecoin o Monero.", "buy_bitcoin": "Comprar Bitcoin", + "buy_now": "Comprar ahora", "buy_provider_unavailable": "Proveedor actualmente no disponible.", "buy_with": "Compra con", "by_cake_pay": "por Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Tema oscuro del pastel", "cake_pay_account_note": "Regístrese con solo una dirección de correo electrónico para ver y comprar tarjetas. ¡Algunas incluso están disponibles con descuento!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", - "cake_pay_subtitle": "Compre tarjetas de regalo con descuento (solo EE. UU.)", - "cake_pay_title": "Tarjetas de regalo Cake Pay", + "cake_pay_subtitle": "Compre tarjetas prepagas y tarjetas de regalo en todo el mundo", "cake_pay_web_cards_subtitle": "Compre tarjetas de prepago y tarjetas de regalo en todo el mundo", "cake_pay_web_cards_title": "Tarjetas Web Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Cambiar billetera actual", "choose_account": "Elegir cuenta", "choose_address": "\n\nPor favor elija la dirección:", + "choose_card_value": "Elija un valor de tarjeta", "choose_derivation": "Elija la derivación de la billetera", "choose_from_available_options": "Elija entre las opciones disponibles:", "choose_one": "Elige uno", @@ -166,6 +167,7 @@ "copy_address": "Copiar dirección ", "copy_id": "Copiar ID", "copyWalletConnectLink": "Copie el enlace de WalletConnect de dApp y péguelo aquí", + "countries": "Países", "create_account": "Crear Cuenta", "create_backup": "Crear copia de seguridad", "create_donation_link": "Crear enlace de donación", @@ -178,6 +180,7 @@ "custom": "Costumbre", "custom_drag": "Custom (mantenía y arrastre)", "custom_redeem_amount": "Cantidad de canje personalizada", + "custom_value": "Valor personalizado", "dark_theme": "Oscura", "debit_card": "Tarjeta de Débito", "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", @@ -190,6 +193,7 @@ "delete_wallet": "Eliminar billetera", "delete_wallet_confirm_message": "¿Está seguro de que desea eliminar la billetera ${wallet_name}?", "deleteConnectionConfirmationPrompt": "¿Está seguro de que desea eliminar la conexión a", + "denominations": "Denominaciones", "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", @@ -277,6 +281,7 @@ "expired": "Muerto", "expires": "Caduca", "expiresOn": "Expira el", + "expiry_and_validity": "Vencimiento y validez", "export_backup": "Exportar copia de seguridad", "extra_id": "ID adicional:", "extracted_address_content": "Enviará fondos a\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Nueva plantilla", "new_wallet": "Nueva billetera", "newConnection": "Nueva conexión", + "no_cards_found": "No se encuentran cartas", "no_id_needed": "¡No se necesita identificación!", "no_id_required": "No se requiere identificación. Recargue y gaste en cualquier lugar", "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar.", @@ -453,6 +459,7 @@ "pre_seed_button_text": "Entiendo. Muéstrame mi semilla", "pre_seed_description": "En la página siguiente verá una serie de ${words} palabras. Esta es su semilla única y privada y es la ÚNICA forma de recuperar su billetera en caso de pérdida o mal funcionamiento. Es SU responsabilidad escribirlo y guardarlo en un lugar seguro fuera de la aplicación Cake Wallet.", "pre_seed_title": "IMPORTANTE", + "prepaid_cards": "Tajetas prepagadas", "prevent_screenshots": "Evitar capturas de pantalla y grabación de pantalla", "privacy": "Privacidad", "privacy_policy": "Política de privacidad", @@ -468,6 +475,7 @@ "purple_dark_theme": "Tema morado oscuro", "qr_fullscreen": "Toque para abrir el código QR en pantalla completa", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Cantidad", "question_to_disable_2fa": "¿Está seguro de que desea deshabilitar Cake 2FA? Ya no se necesitará un código 2FA para acceder a la billetera y a ciertas funciones.", "receivable_balance": "Saldo de cuentas por cobrar", "receive": "Recibir", @@ -709,6 +717,7 @@ "tokenID": "IDENTIFICACIÓN", "tor_connection": "conexión tor", "tor_only": "solo Tor", + "total": "Total", "total_saving": "Ahorro Total", "totp_2fa_failure": "Código incorrecto. Intente con un código diferente o genere una nueva clave secreta. Use una aplicación 2FA compatible que admita códigos de 8 dígitos y SHA512.", "totp_2fa_success": "¡Éxito! Cake 2FA habilitado para esta billetera. Recuerde guardar su semilla mnemotécnica en caso de que pierda el acceso a la billetera.", @@ -799,6 +808,8 @@ "use_ssl": "Utilice SSL", "use_suggested": "Usar sugerido", "use_testnet": "Use TestNet", + "value": "Valor", + "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados", "verification": "Verificación", "verify_with_2fa": "Verificar con Cake 2FA", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 411983e8d..119cb24e4 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -87,6 +87,7 @@ "buy": "Acheter", "buy_alert_content": "Actuellement, nous ne prenons en charge que l'achat de Bitcoin, Ethereum, Litecoin et Monero. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum, Litecoin ou Monero.", "buy_bitcoin": "Acheter du Bitcoin", + "buy_now": "Acheter maintenant", "buy_provider_unavailable": "Fournisseur actuellement indisponible.", "buy_with": "Acheter avec", "by_cake_pay": "par Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Thème sombre du gâteau", "cake_pay_account_note": "Inscrivez-vous avec juste une adresse e-mail pour voir et acheter des cartes. Certaines sont même disponibles à prix réduit !", "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", - "cake_pay_subtitle": "Achetez des cartes-cadeaux à prix réduit (États-Unis uniquement)", - "cake_pay_title": "Cartes cadeaux Cake Pay", + "cake_pay_subtitle": "Achetez des cartes et des cartes-cadeaux prépayées mondiales", "cake_pay_web_cards_subtitle": "Achetez des cartes prépayées et des cartes-cadeaux dans le monde entier", "cake_pay_web_cards_title": "Cartes Web Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Changer le portefeuille (wallet) actuel", "choose_account": "Choisir le compte", "choose_address": "\n\nMerci de choisir l'adresse :", + "choose_card_value": "Choisissez une valeur de carte", "choose_derivation": "Choisissez le chemin de dérivation du portefeuille", "choose_from_available_options": "Choisissez parmi les options disponibles :", "choose_one": "Choisissez-en un", @@ -166,6 +167,7 @@ "copy_address": "Copier l'Adresse", "copy_id": "Copier l'ID", "copyWalletConnectLink": "Copiez le lien WalletConnect depuis l'application décentralisée (dApp) et collez-le ici", + "countries": "Des pays", "create_account": "Créer un compte", "create_backup": "Créer une sauvegarde", "create_donation_link": "Créer un lien de don", @@ -178,6 +180,7 @@ "custom": "personnalisé", "custom_drag": "Custom (maintenir et traîner)", "custom_redeem_amount": "Montant d'échange personnalisé", + "custom_value": "Valeur personnalisée", "dark_theme": "Sombre", "debit_card": "Carte de débit", "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille (wallet) numérique peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", @@ -190,6 +193,7 @@ "delete_wallet": "Supprimer le portefeuille (wallet)", "delete_wallet_confirm_message": "Êtes-vous sûr de vouloir supprimer le portefeuille (wallet) ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Êtes-vous sûr de vouloir supprimer la connexion à", + "denominations": "Dénominations", "descending": "Descendant", "description": "Description", "destination_tag": "Tag de destination :", @@ -277,6 +281,7 @@ "expired": "Expirée", "expires": "Expire", "expiresOn": "Expire le", + "expiry_and_validity": "Expiration et validité", "export_backup": "Exporter la sauvegarde", "extra_id": "ID supplémentaire :", "extracted_address_content": "Vous allez envoyer des fonds à\n${recipient_name}", @@ -308,7 +313,7 @@ "gift_card_is_generated": "La carte-cadeau est générée", "gift_card_number": "Numéro de carte cadeau", "gift_card_redeemed_note": "Les cartes-cadeaux que vous avez utilisées apparaîtront ici", - "gift_cards": "Cartes-Cadeaux", + "gift_cards": "Cartes cadeaux", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", "got_it": "Compris", "gross_balance": "Solde brut", @@ -384,6 +389,7 @@ "new_template": "Nouveau Modèle", "new_wallet": "Nouveau Portefeuille (Wallet)", "newConnection": "Nouvelle connexion", + "no_cards_found": "Pas de cartes trouvées", "no_id_needed": "Aucune pièce d'identité nécessaire !", "no_id_required": "Aucune pièce d'identité requise. Rechargez et dépensez n'importe où", "no_relay_on_domain": "Il n'existe pas de relais pour le domaine de l'utilisateur ou le relais n'est pas disponible. Veuillez choisir un relais à utiliser.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "J'ai compris. Montrez moi ma phrase secrète (seed)", "pre_seed_description": "Sur la page suivante vous allez voir une série de ${words} mots. Ils constituent votre phrase secrète (seed) unique et privée et sont le SEUL moyen de restaurer votre portefeuille (wallet) en cas de perte ou de dysfonctionnement. Il est de VOTRE responsabilité d'écrire cette série de mots et de la stocker dans un lieu sûr en dehors de l'application Cake Wallet.", "pre_seed_title": "IMPORTANT", + "prepaid_cards": "Cartes prépayées", "prevent_screenshots": "Empêcher les captures d'écran et l'enregistrement d'écran", "privacy": "Confidentialité", "privacy_policy": "Politique de confidentialité", @@ -467,6 +474,7 @@ "purple_dark_theme": "THÈME PURPLE DARK", "qr_fullscreen": "Appuyez pour ouvrir le QR code en mode plein écran", "qr_payment_amount": "Ce QR code contient un montant de paiement. Voulez-vous remplacer la valeur actuelle ?", + "quantity": "Quantité", "question_to_disable_2fa": "Êtes-vous sûr de vouloir désactiver Cake 2FA ? Un code 2FA ne sera plus nécessaire pour accéder au portefeuille (wallet) et à certaines fonctions.", "receivable_balance": "Solde de créances", "receive": "Recevoir", @@ -708,6 +716,7 @@ "tokenID": "IDENTIFIANT", "tor_connection": "Connexion Tor", "tor_only": "Tor uniquement", + "total": "Total", "total_saving": "Économies totales", "totp_2fa_failure": "Code incorrect. Veuillez essayer un code différent ou générer un nouveau secret TOTP. Utilisez une application 2FA compatible qui prend en charge les codes à 8 chiffres et SHA512.", "totp_2fa_success": "Succès! Cake 2FA est activé pour ce portefeuille. N'oubliez pas de sauvegarder votre phrase secrète (seed) au cas où vous perdriez l'accès au portefeuille (wallet).", @@ -798,6 +807,8 @@ "use_ssl": "Utiliser SSL", "use_suggested": "Suivre la suggestion", "use_testnet": "Utiliser TestNet", + "value": "Valeur", + "value_type": "Type de valeur", "variable_pair_not_supported": "Cette paire variable n'est pas prise en charge avec les échanges sélectionnés", "verification": "Vérification", "verify_with_2fa": "Vérifier avec Cake 2FA", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5809f3d2e..dd3350131 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -87,6 +87,7 @@ "buy": "Sayi", "buy_alert_content": "A halin yanzu muna tallafawa kawai siyan Bitcoin, Ethereum, Litecoin, da Monero. Da fatan za a ƙirƙiri ko canza zuwa Bitcoin, Ethereum, Litecoin, ko Monero walat.", "buy_bitcoin": "Sayi Bitcoin", + "buy_now": "Saya yanzu", "buy_provider_unavailable": "Mai ba da kyauta a halin yanzu babu.", "buy_with": "Saya da", "by_cake_pay": "da Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake Dark Jigo", "cake_pay_account_note": "Yi rajista tare da adireshin imel kawai don gani da siyan katunan. Wasu ma suna samuwa a rangwame!", "cake_pay_learn_more": "Nan take siya ku kwaso katunan kyaututtuka a cikin app!\nTake hagu zuwa dama don ƙarin koyo.", - "cake_pay_subtitle": "Sayi katunan kyauta masu rahusa (Amurka kawai)", - "cake_pay_title": "Cake Pay Gift Cards", + "cake_pay_subtitle": "Sayi katunan shirye-shiryen duniya da katunan kyauta", "cake_pay_web_cards_subtitle": "Sayi katunan da aka riga aka biya na duniya da katunan kyauta", "cake_pay_web_cards_title": "Cake Pay Web Cards", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Canja walat yanzu", "choose_account": "Zaɓi asusu", "choose_address": "\n\n Da fatan za a zaɓi adireshin:", + "choose_card_value": "Zabi darajar katin", "choose_derivation": "Zaɓi walatawa", "choose_from_available_options": "Zaɓi daga zaɓuɓɓukan da ake da su:", "choose_one": "Zaɓi ɗaya", @@ -166,6 +167,7 @@ "copy_address": "Kwafi Adireshin", "copy_id": "Kwafi ID", "copyWalletConnectLink": "Kwafi hanyar haɗin WalletConnect daga dApp kuma liƙa a nan", + "countries": "Kasashe", "create_account": "Kirkira ajiya", "create_backup": "Ƙirƙiri madadin", "create_donation_link": "Sanya hanyar sadaka", @@ -178,6 +180,7 @@ "custom": "al'ada", "custom_drag": "Al'ada (riƙe da ja)", "custom_redeem_amount": "Adadin Fansa na Musamman", + "custom_value": "Darajar al'ada", "dark_theme": "Duhu", "debit_card": "Katin Zare kudi", "debit_card_terms": "Adana da amfani da lambar katin kuɗin ku (da takaddun shaida masu dacewa da lambar katin kuɗin ku) a cikin wannan walat ɗin dijital suna ƙarƙashin Sharuɗɗa da Sharuɗɗa na yarjejeniya mai amfani da katin tare da mai fitar da katin biyan kuɗi, kamar yadda yake aiki daga lokaci zuwa lokaci.", @@ -190,6 +193,7 @@ "delete_wallet": "Share walat", "delete_wallet_confirm_message": "Shin kun tabbata cewa kuna son share jakar ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Shin kun tabbata cewa kuna son share haɗin zuwa", + "denominations": "Denominations", "descending": "Saukowa", "description": "Bayani", "destination_tag": "Tambarin makoma:", @@ -277,6 +281,7 @@ "expired": "Karewa", "expires": "Ya ƙare", "expiresOn": "Yana ƙarewa", + "expiry_and_validity": "Karewa da inganci", "export_backup": "Ajiyayyen fitarwa", "extra_id": "Karin ID:", "extracted_address_content": "Za ku aika da kudade zuwa\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Sabon Samfura", "new_wallet": "Sabuwar Wallet", "newConnection": "Sabuwar Haɗi", + "no_cards_found": "Babu katunan da aka samo", "no_id_needed": "Babu ID da ake buƙata!", "no_id_required": "Babu ID da ake buƙata. Yi da kuma ciyar a ko'ina", "no_relay_on_domain": "Babu gudun ba da sanda ga yankin mai amfani ko kuma ba a samu ba. Da fatan za a zaɓi gudun ba da sanda don amfani.", @@ -454,6 +460,7 @@ "pre_seed_button_text": "Ina fahimta. Nuna mini seed din nawa", "pre_seed_description": "A kan shafin nan za ku ga wata ƙungiya na ${words} kalmomi. Wannan shine tsarin daban-daban ku kuma na sirri kuma shine hanya ɗaya kadai don mai da purse dinku a cikin yanayin rasa ko rashin aiki. Yana da damar da kuke a cikin tabbatar da kuyi rubuta shi kuma kuyi ajiye shi a wuri na aminci wanda ya wuce wurin app na Cake Wallet.", "pre_seed_title": "MUHIMMANCI", + "prepaid_cards": "Katunan shirye-shirye", "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi", "privacy": "Keɓantawa", "privacy_policy": "takardar kebantawa", @@ -469,6 +476,7 @@ "purple_dark_theme": "M duhu jigo", "qr_fullscreen": "Matsa don buɗe lambar QR na cikakken allo", "qr_payment_amount": "Wannan QR code yana da adadin kuɗi. Kuna so ku overwrite wannan adadi?", + "quantity": "Yawa", "question_to_disable_2fa": "Ka tabbata cewa kana son kashe cake 2fa? Ba za a sake buƙatar lambar 2FA ba don samun damar yin walat da takamaiman ayyuka.", "receivable_balance": "Daidaituwa da daidaituwa", "receive": "Samu", @@ -710,6 +718,7 @@ "tokenID": "ID", "tor_connection": "Tor haɗin gwiwa", "tor_only": "Tor kawai", + "total": "Duka", "total_saving": "Jimlar Adana", "totp_2fa_failure": "Ba daidai ba. Da fatan za a gwada wata lamba ta daban ko samar da sabon maɓallin asirin. Yi amfani da aikace-aikacen da ya dace 2FA wanda ke tallafawa lambobin lambobi 8 da Sha512.", "totp_2fa_success": "Nasara! Cake 2FA ya dogara da wannan waljin. Ka tuna domin adana zuriyar mnemmonic naka idan ka rasa damar walat.", @@ -800,6 +809,8 @@ "use_ssl": "Yi amfani da SSL", "use_suggested": "Amfani da Shawarwari", "use_testnet": "Amfani da gwaji", + "value": "Daraja", + "value_type": "Nau'in darajar", "variable_pair_not_supported": "Ba a samun goyan bayan wannan m biyu tare da zaɓaɓɓun musayar", "verification": "tabbatar", "verify_with_2fa": "Tabbatar da Cake 2FA", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 0b21747b4..55aead5a3 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -87,6 +87,7 @@ "buy": "खरीदें", "buy_alert_content": "वर्तमान में हम केवल बिटकॉइन, एथेरियम, लाइटकॉइन और मोनेरो की खरीद का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम, लाइटकॉइन, या मोनेरो वॉलेट बनाएं या उस पर स्विच करें।", "buy_bitcoin": "बिटकॉइन खरीदें", + "buy_now": "अभी खरीदें", "buy_provider_unavailable": "वर्तमान में प्रदाता अनुपलब्ध है।", "buy_with": "के साथ खरीदें", "by_cake_pay": "केकपे द्वारा", @@ -94,8 +95,7 @@ "cake_dark_theme": "केक डार्क थीम", "cake_pay_account_note": "कार्ड देखने और खरीदने के लिए केवल एक ईमेल पते के साथ साइन अप करें। कुछ छूट पर भी उपलब्ध हैं!", "cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।", - "cake_pay_subtitle": "रियायती उपहार कार्ड खरीदें (केवल यूएसए)", - "cake_pay_title": "केक पे गिफ्ट कार्ड्स", + "cake_pay_subtitle": "दुनिया भर में प्रीपेड कार्ड और उपहार कार्ड खरीदें", "cake_pay_web_cards_subtitle": "दुनिया भर में प्रीपेड कार्ड और गिफ्ट कार्ड खरीदें", "cake_pay_web_cards_title": "केक भुगतान वेब कार्ड", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "वर्तमान बटुआ बदलें", "choose_account": "खाता चुनें", "choose_address": "\n\nकृपया पता चुनें:", + "choose_card_value": "एक कार्ड मूल्य चुनें", "choose_derivation": "वॉलेट व्युत्पत्ति चुनें", "choose_from_available_options": "उपलब्ध विकल्पों में से चुनें:", "choose_one": "एक का चयन", @@ -166,6 +167,7 @@ "copy_address": "पता कॉपी करें", "copy_id": "प्रतिलिपि ID", "copyWalletConnectLink": "dApp से वॉलेटकनेक्ट लिंक को कॉपी करें और यहां पेस्ट करें", + "countries": "देशों", "create_account": "खाता बनाएं", "create_backup": "बैकअप बनाएँ", "create_donation_link": "दान लिंक बनाएं", @@ -178,6 +180,7 @@ "custom": "कस्टम", "custom_drag": "कस्टम (पकड़ और खींचें)", "custom_redeem_amount": "कस्टम रिडीम राशि", + "custom_value": "कस्टम मूल्य", "dark_theme": "अंधेरा", "debit_card": "डेबिट कार्ड", "debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।", @@ -190,6 +193,7 @@ "delete_wallet": "वॉलेट हटाएं", "delete_wallet_confirm_message": "क्या आप वाकई ${wallet_name} वॉलेट हटाना चाहते हैं?", "deleteConnectionConfirmationPrompt": "क्या आप वाकई कनेक्शन हटाना चाहते हैं?", + "denominations": "मूल्यवर्ग", "descending": "अवरोही", "description": "विवरण", "destination_tag": "गंतव्य टैग:", @@ -277,6 +281,7 @@ "expired": "समय सीमा समाप्त", "expires": "समाप्त हो जाता है", "expiresOn": "पर समय सीमा समाप्त", + "expiry_and_validity": "समाप्ति और वैधता", "export_backup": "निर्यात बैकअप", "extra_id": "अतिरिक्त आईडी:", "extracted_address_content": "आपको धनराशि भेजी जाएगी\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "नया टेम्पलेट", "new_wallet": "नया बटुआ", "newConnection": "नया कनेक्शन", + "no_cards_found": "कोई कार्ड नहीं मिला", "no_id_needed": "कोई आईडी नहीं चाहिए!", "no_id_required": "कोई आईडी आवश्यक नहीं है। टॉप अप करें और कहीं भी खर्च करें", "no_relay_on_domain": "उपयोगकर्ता के डोमेन के लिए कोई रिले नहीं है या रिले अनुपलब्ध है। कृपया उपयोग करने के लिए एक रिले चुनें।", @@ -453,6 +459,7 @@ "pre_seed_button_text": "मै समझता हुँ। मुझे अपना बीज दिखाओ", "pre_seed_description": "अगले पेज पर आपको ${words} शब्दों की एक श्रृंखला दिखाई देगी। यह आपका अद्वितीय और निजी बीज है और नुकसान या खराबी के मामले में अपने बटुए को पुनर्प्राप्त करने का एकमात्र तरीका है। यह आपकी जिम्मेदारी है कि इसे नीचे लिखें और इसे Cake Wallet ऐप के बाहर सुरक्षित स्थान पर संग्रहीत करें।", "pre_seed_title": "महत्वपूर्ण", + "prepaid_cards": "पूर्वदत्त कार्ड", "prevent_screenshots": "स्क्रीनशॉट और स्क्रीन रिकॉर्डिंग रोकें", "privacy": "गोपनीयता", "privacy_policy": "गोपनीयता नीति", @@ -468,6 +475,7 @@ "purple_dark_theme": "पर्पल डार्क थीम", "qr_fullscreen": "फ़ुल स्क्रीन क्यूआर कोड खोलने के लिए टैप करें", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "मात्रा", "question_to_disable_2fa": "क्या आप सुनिश्चित हैं कि आप Cake 2FA को अक्षम करना चाहते हैं? वॉलेट और कुछ कार्यों तक पहुँचने के लिए अब 2FA कोड की आवश्यकता नहीं होगी।", "ready_have_account": "क्या आपके पास पहले से ही एक खाता है?", "receivable_balance": "प्राप्य शेष", @@ -710,6 +718,7 @@ "tokenID": "पहचान", "tor_connection": "टोर कनेक्शन", "tor_only": "Tor केवल", + "total": "कुल", "total_saving": "कुल बचत", "totp_2fa_failure": "गलत कोड़। कृपया एक अलग कोड का प्रयास करें या एक नई गुप्त कुंजी उत्पन्न करें। 8-अंकीय कोड और SHA512 का समर्थन करने वाले संगत 2FA ऐप का उपयोग करें।", "totp_2fa_success": "सफलता! इस वॉलेट के लिए Cake 2FA सक्षम है। यदि आप वॉलेट एक्सेस खो देते हैं तो अपने स्मरक बीज को सहेजना याद रखें।", @@ -800,6 +809,8 @@ "use_ssl": "उपयोग SSL", "use_suggested": "सुझाए गए का प्रयोग करें", "use_testnet": "टेस्टनेट का उपयोग करें", + "value": "कीमत", + "value_type": "मान प्रकार", "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है", "verification": "सत्यापन", "verify_with_2fa": "केक 2FA के साथ सत्यापित करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index dd2093a03..c999b7dfd 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -87,6 +87,7 @@ "buy": "Kupi", "buy_alert_content": "Trenutno podržavamo samo kupnju Bitcoina, Ethereuma, Litecoina i Monera. Izradite ili prijeđite na svoj Bitcoin, Ethereum, Litecoin ili Monero novčanik.", "buy_bitcoin": "Kupite Bitcoin", + "buy_now": "Kupi sada", "buy_provider_unavailable": "Davatelj trenutno nije dostupan.", "buy_with": "Kupite s", "by_cake_pay": "od Cake Paya", @@ -94,8 +95,7 @@ "cake_dark_theme": "TOKA DARKA TEMA", "cake_pay_account_note": "Prijavite se samo s adresom e-pošte da biste vidjeli i kupili kartice. Neke su čak dostupne uz popust!", "cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.", - "cake_pay_subtitle": "Kupite darovne kartice s popustom (samo SAD)", - "cake_pay_title": "Cake Pay poklon kartice", + "cake_pay_subtitle": "Kupite svjetske unaprijed plaćene kartice i poklon kartice", "cake_pay_web_cards_subtitle": "Kupujte prepaid kartice i poklon kartice diljem svijeta", "cake_pay_web_cards_title": "Cake Pay Web kartice", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Izmijeni trenutni novčanik", "choose_account": "Odaberi račun", "choose_address": "\n\nOdaberite adresu:", + "choose_card_value": "Odaberite vrijednost kartice", "choose_derivation": "Odaberite izvedbu novčanika", "choose_from_available_options": "Odaberite neku od dostupnih opcija:", "choose_one": "Izaberi jedan", @@ -166,6 +167,7 @@ "copy_address": "Kopiraj adresu", "copy_id": "Kopirati ID", "copyWalletConnectLink": "Kopirajte vezu WalletConnect iz dApp-a i zalijepite je ovdje", + "countries": "Zemalja", "create_account": "Stvori račun", "create_backup": "Stvori sigurnosnu kopiju", "create_donation_link": "Izradi poveznicu za donaciju", @@ -178,6 +180,7 @@ "custom": "prilagođeno", "custom_drag": "Prilagođeni (držite i povucite)", "custom_redeem_amount": "Prilagođeni iznos otkupa", + "custom_value": "Prilagođena vrijednost", "dark_theme": "Tamna", "debit_card": "Debitna kartica", "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", @@ -190,6 +193,7 @@ "delete_wallet": "Izbriši novčanik", "delete_wallet_confirm_message": "Jeste li sigurni da želite izbrisati ${wallet_name} novčanik?", "deleteConnectionConfirmationPrompt": "Jeste li sigurni da želite izbrisati vezu s", + "denominations": "Denominacije", "descending": "Silazni", "description": "Opis", "destination_tag": "Odredišna oznaka:", @@ -277,6 +281,7 @@ "expired": "Isteklo", "expires": "Ističe", "expiresOn": "Istječe", + "expiry_and_validity": "Istek i valjanost", "export_backup": "Izvezi sigurnosnu kopiju", "extra_id": "Dodatni ID:", "extracted_address_content": "Poslat ćete sredstva primatelju\n${recipient_name}", @@ -308,7 +313,7 @@ "gift_card_is_generated": "Poklon kartica je generirana", "gift_card_number": "Broj darovne kartice", "gift_card_redeemed_note": "Poklon kartice koje ste iskoristili pojavit će se ovdje", - "gift_cards": "Ajándékkártya", + "gift_cards": "Darovne kartice", "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", "got_it": "U redu", "gross_balance": "Bruto bilanca", @@ -384,6 +389,7 @@ "new_template": "novi predložak", "new_wallet": "Novi novčanik", "newConnection": "Nova veza", + "no_cards_found": "Nisu pronađene kartice", "no_id_needed": "Nije potreban ID!", "no_id_required": "Nije potreban ID. Nadopunite i potrošite bilo gdje", "no_relay_on_domain": "Ne postoji relej za korisničku domenu ili je relej nedostupan. Odaberite relej za korištenje.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Razumijem. Prikaži mi moj pristupni izraz", "pre_seed_description": "Na sljedećoj ćete stranici vidjeti niz ${words} riječi. Radi se o Vašem jedinstvenom i tajnom pristupnom izrazu koji je ujedno i JEDINI način na koji možete oporaviti svoj novčanik u slučaju gubitka ili kvara. VAŠA je odgovornost zapisati ga te pohraniti na sigurno mjesto izvan Cake Wallet aplikacije.", "pre_seed_title": "VAŽNO", + "prepaid_cards": "Unaprijed plaćene kartice", "prevent_screenshots": "Spriječite snimke zaslona i snimanje zaslona", "privacy": "Privatnost", "privacy_policy": "Pravila privatnosti", @@ -467,6 +474,7 @@ "purple_dark_theme": "Ljubičasta tamna tema", "qr_fullscreen": "Dodirnite za otvaranje QR koda preko cijelog zaslona", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Količina", "question_to_disable_2fa": "Jeste li sigurni da želite onemogućiti Cake 2FA? 2FA kod više neće biti potreban za pristup novčaniku i određenim funkcijama.", "receivable_balance": "Stanje potraživanja", "receive": "Primi", @@ -708,6 +716,7 @@ "tokenID": "iskaznica", "tor_connection": "Tor veza", "tor_only": "Samo Tor", + "total": "Ukupno", "total_saving": "Ukupna ušteda", "totp_2fa_failure": "Neispravan kod. Pokušajte s drugim kodom ili generirajte novi tajni ključ. Koristite kompatibilnu 2FA aplikaciju koja podržava 8-znamenkasti kod i SHA512.", "totp_2fa_success": "Uspjeh! Cake 2FA omogućen za ovaj novčanik. Ne zaboravite spremiti svoje mnemoničko sjeme u slučaju da izgubite pristup novčaniku.", @@ -798,6 +807,8 @@ "use_ssl": "Koristi SSL", "use_suggested": "Koristite predloženo", "use_testnet": "Koristite TestNet", + "value": "Vrijednost", + "value_type": "Tipa vrijednosti", "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama", "verification": "Potvrda", "verify_with_2fa": "Provjerite s Cake 2FA", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 3726d431b..24208718c 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -87,6 +87,7 @@ "buy": "Beli", "buy_alert_content": "Saat ini kami hanya mendukung pembelian Bitcoin, Ethereum, Litecoin, dan Monero. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, Litecoin, atau Monero Anda.", "buy_bitcoin": "Beli Bitcoin", + "buy_now": "Beli sekarang", "buy_provider_unavailable": "Penyedia saat ini tidak tersedia.", "buy_with": "Beli dengan", "by_cake_pay": "oleh Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Tema Kue Gelap", "cake_pay_account_note": "Daftar hanya dengan alamat email untuk melihat dan membeli kartu. Beberapa di antaranya bahkan tersedia dengan diskon!", "cake_pay_learn_more": "Beli dan tukar kartu hadiah secara instan di aplikasi!\nGeser ke kanan untuk informasi lebih lanjut.", - "cake_pay_subtitle": "Beli kartu hadiah dengan harga diskon (hanya USA)", - "cake_pay_title": "Kartu Hadiah Cake Pay", + "cake_pay_subtitle": "Beli kartu prabayar di seluruh dunia dan kartu hadiah", "cake_pay_web_cards_subtitle": "Beli kartu prabayar dan kartu hadiah secara global", "cake_pay_web_cards_title": "Kartu Web Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Ganti dompet saat ini", "choose_account": "Pilih akun", "choose_address": "\n\nSilakan pilih alamat:", + "choose_card_value": "Pilih nilai kartu", "choose_derivation": "Pilih dompet dompet", "choose_from_available_options": "Pilih dari pilihan yang tersedia:", "choose_one": "Pilih satu", @@ -166,6 +167,7 @@ "copy_address": "Salin Alamat", "copy_id": "Salin ID", "copyWalletConnectLink": "Salin tautan WalletConnect dari dApp dan tempel di sini", + "countries": "Negara", "create_account": "Buat Akun", "create_backup": "Buat cadangan", "create_donation_link": "Buat tautan donasi", @@ -178,6 +180,7 @@ "custom": "kustom", "custom_drag": "Khusus (tahan dan seret)", "custom_redeem_amount": "Jumlah Tukar Kustom", + "custom_value": "Nilai khusus", "dark_theme": "Gelap", "debit_card": "Kartu Debit", "debit_card_terms": "Penyimpanan dan penggunaan nomor kartu pembayaran Anda (dan kredensial yang sesuai dengan nomor kartu pembayaran Anda) dalam dompet digital ini tertakluk pada Syarat dan Ketentuan persetujuan pemegang kartu yang berlaku dengan penerbit kartu pembayaran, seperti yang berlaku dari waktu ke waktu.", @@ -190,6 +193,7 @@ "delete_wallet": "Hapus dompet", "delete_wallet_confirm_message": "Apakah Anda yakin ingin menghapus dompet ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Apakah Anda yakin ingin menghapus koneksi ke", + "denominations": "Denominasi", "descending": "Menurun", "description": "Keterangan", "destination_tag": "Tag tujuan:", @@ -277,6 +281,7 @@ "expired": "Kedaluwarsa", "expires": "Kadaluarsa", "expiresOn": "Kadaluarsa pada", + "expiry_and_validity": "Kedaluwarsa dan validitas", "export_backup": "Ekspor cadangan", "extra_id": "ID tambahan:", "extracted_address_content": "Anda akan mengirim dana ke\n${recipient_name}", @@ -308,7 +313,7 @@ "gift_card_is_generated": "Kartu Hadiah telah dibuat", "gift_card_number": "Nomor Kartu Hadiah", "gift_card_redeemed_note": "Kartu hadiah yang sudah Anda tukar akan muncul di sini", - "gift_cards": "Kartu Hadiah", + "gift_cards": "Kartu hadiah", "gift_cards_unavailable": "Kartu hadiah hanya tersedia untuk dibeli dengan Monero, Bitcoin, dan Litecoin saat ini", "got_it": "Sudah paham", "gross_balance": "Saldo Kotor", @@ -384,6 +389,7 @@ "new_template": "Template Baru", "new_wallet": "Dompet Baru", "newConnection": "Koneksi Baru", + "no_cards_found": "Tidak ada kartu yang ditemukan", "no_id_needed": "Tidak perlu ID!", "no_id_required": "Tidak perlu ID. Isi ulang dan belanja di mana saja", "no_relay_on_domain": "Tidak ada relai untuk domain pengguna atau relai tidak tersedia. Silakan pilih relai yang akan digunakan.", @@ -454,6 +460,7 @@ "pre_seed_button_text": "Saya mengerti. Tampilkan seed saya", "pre_seed_description": "Di halaman berikutnya Anda akan melihat serangkaian kata ${words}. Ini adalah seed unik dan pribadi Anda dan itu SATU-SATUNYA cara untuk mengembalikan dompet Anda jika hilang atau rusak. Ini adalah TANGGUNG JAWAB Anda untuk menuliskannya dan menyimpan di tempat yang aman di luar aplikasi Cake Wallet.", "pre_seed_title": "PENTING", + "prepaid_cards": "Kartu prabayar", "prevent_screenshots": "Cegah tangkapan layar dan perekaman layar", "privacy": "Privasi", "privacy_policy": "Kebijakan Privasi", @@ -469,6 +476,7 @@ "purple_dark_theme": "Tema gelap ungu", "qr_fullscreen": "Tap untuk membuka layar QR code penuh", "qr_payment_amount": "QR code ini berisi jumlah pembayaran. Apakah Anda ingin menimpa nilai saat ini?", + "quantity": "Kuantitas", "question_to_disable_2fa": "Apakah Anda yakin ingin menonaktifkan Cake 2FA? Kode 2FA tidak lagi diperlukan untuk mengakses dompet dan fungsi tertentu.", "receivable_balance": "Saldo piutang", "receive": "Menerima", @@ -711,6 +719,7 @@ "tokenID": "PENGENAL", "tor_connection": "koneksi Tor", "tor_only": "Hanya Tor", + "total": "Total", "total_saving": "Total Pembayaran", "totp_2fa_failure": "Kode salah. Silakan coba kode lain atau buat kunci rahasia baru. Gunakan aplikasi 2FA yang kompatibel yang mendukung kode 8 digit dan SHA512.", "totp_2fa_success": "Kesuksesan! Cake 2FA diaktifkan untuk dompet ini. Ingatlah untuk menyimpan benih mnemonik Anda jika Anda kehilangan akses dompet.", @@ -801,6 +810,8 @@ "use_ssl": "Gunakan SSL", "use_suggested": "Gunakan yang Disarankan", "use_testnet": "Gunakan TestNet", + "value": "Nilai", + "value_type": "Jenis Nilai", "variable_pair_not_supported": "Pasangan variabel ini tidak didukung dengan bursa yang dipilih", "verification": "Verifikasi", "verify_with_2fa": "Verifikasi dengan Cake 2FA", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 92c893fd2..5509c0067 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -87,6 +87,7 @@ "buy": "Comprare", "buy_alert_content": "Attualmente supportiamo solo l'acquisto di Bitcoin, Ethereum, Litecoin e Monero. Crea o passa al tuo portafoglio Bitcoin, Ethereum, Litecoin o Monero.", "buy_bitcoin": "Acquista Bitcoin", + "buy_now": "Acquista ora", "buy_provider_unavailable": "Provider attualmente non disponibile.", "buy_with": "Acquista con", "by_cake_pay": "da Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Tema oscuro della torta", "cake_pay_account_note": "Iscriviti con solo un indirizzo email per vedere e acquistare le carte. Alcune sono anche disponibili con uno sconto!", "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", - "cake_pay_subtitle": "Acquista buoni regalo scontati (solo USA)", - "cake_pay_title": "Carte regalo Cake Pay", + "cake_pay_subtitle": "Acquista carte prepagate in tutto il mondo e carte regalo", "cake_pay_web_cards_subtitle": "Acquista carte prepagate e carte regalo in tutto il mondo", "cake_pay_web_cards_title": "Carte Web Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Cambia portafoglio attuale", "choose_account": "Scegli account", "choose_address": "\n\nSi prega di scegliere l'indirizzo:", + "choose_card_value": "Scegli un valore della carta", "choose_derivation": "Scegli la derivazione del portafoglio", "choose_from_available_options": "Scegli tra le opzioni disponibili:", "choose_one": "Scegline uno", @@ -167,6 +168,7 @@ "copy_address": "Copia Indirizzo", "copy_id": "Copia ID", "copyWalletConnectLink": "Copia il collegamento WalletConnect dalla dApp e incollalo qui", + "countries": "Paesi", "create_account": "Crea account", "create_backup": "Crea backup", "create_donation_link": "Crea un link per la donazione", @@ -179,6 +181,7 @@ "custom": "personalizzato", "custom_drag": "Custom (Hold and Drag)", "custom_redeem_amount": "Importo di riscatto personalizzato", + "custom_value": "Valore personalizzato", "dark_theme": "Scuro", "debit_card": "Carta di debito", "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore da tempo al tempo.", @@ -191,6 +194,7 @@ "delete_wallet": "Elimina portafoglio", "delete_wallet_confirm_message": "Sei sicuro di voler eliminare il portafoglio ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Sei sicuro di voler eliminare la connessione a", + "denominations": "Denominazioni", "descending": "Discendente", "description": "Descrizione", "destination_tag": "Tag destinazione:", @@ -278,6 +282,7 @@ "expired": "Scaduta", "expires": "Scade", "expiresOn": "Scade il", + "expiry_and_validity": "Scadenza e validità", "export_backup": "Esporta backup", "extra_id": "Extra ID:", "extracted_address_content": "Invierai i tuoi fondi a\n${recipient_name}", @@ -385,6 +390,7 @@ "new_template": "Nuovo modello", "new_wallet": "Nuovo Portafoglio", "newConnection": "Nuova connessione", + "no_cards_found": "Nessuna carta trovata", "no_id_needed": "Nessun ID necessario!", "no_id_required": "Nessun ID richiesto. Ricarica e spendi ovunque", "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relè da utilizzare.", @@ -454,6 +460,7 @@ "pre_seed_button_text": "Ho capito. Mostrami il seme", "pre_seed_description": "Nella pagina seguente ti sarà mostrata una serie di parole ${words}. Questo è il tuo seme unico e privato ed è l'UNICO modo per recuperare il tuo portafoglio in caso di perdita o malfunzionamento. E' TUA responsabilità trascriverlo e conservarlo in un posto sicuro fuori dall'app Cake Wallet.", "pre_seed_title": "IMPORTANTE", + "prepaid_cards": "Carte prepagata", "prevent_screenshots": "Impedisci screenshot e registrazione dello schermo", "privacy": "Privacy", "privacy_policy": "Informativa sulla privacy", @@ -469,6 +476,7 @@ "purple_dark_theme": "Tema oscuro viola", "qr_fullscreen": "Tocca per aprire il codice QR a schermo intero", "qr_payment_amount": "Questo codice QR contiene l'ammontare del pagamento. Vuoi sovrascrivere il varlore attuale?", + "quantity": "Quantità", "question_to_disable_2fa": "Sei sicuro di voler disabilitare Cake 2FA? Non sarà più necessario un codice 2FA per accedere al portafoglio e ad alcune funzioni.", "receivable_balance": "Bilanciamento creditizio", "receive": "Ricevi", @@ -710,6 +718,7 @@ "tokenID": "ID", "tor_connection": "Connessione Tor", "tor_only": "Solo Tor", + "total": "Totale", "total_saving": "Risparmio totale", "totp_2fa_failure": "Codice non corretto. Prova un codice diverso o genera una nuova chiave segreta. Utilizza un'app 2FA compatibile che supporti codici a 8 cifre e SHA512.", "totp_2fa_success": "Successo! Cake 2FA abilitato per questo portafoglio. Ricordati di salvare il tuo seme mnemonico nel caso in cui perdi l'accesso al portafoglio.", @@ -800,6 +809,8 @@ "use_ssl": "Usa SSL", "use_suggested": "Usa suggerito", "use_testnet": "Usa TestNet", + "value": "Valore", + "value_type": "Tipo di valore", "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati", "verification": "Verifica", "verify_with_2fa": "Verifica con Cake 2FA", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index e3e105d4a..9204ffdf4 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -87,6 +87,7 @@ "buy": "購入", "buy_alert_content": "現在、ビットコイン、イーサリアム、ライトコイン、モネロの購入のみをサポートしています。ビットコイン、イーサリアム、ライトコイン、またはモネロのウォレットを作成するか、これらのウォレットに切り替えてください。", "buy_bitcoin": "ビットコインを購入する", + "buy_now": "今すぐ購入", "buy_provider_unavailable": "現在、プロバイダーは利用できません。", "buy_with": "で購入", "by_cake_pay": "by Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "ケーキ暗いテーマ", "cake_pay_account_note": "メールアドレスだけでサインアップして、カードを表示して購入できます。割引価格で利用できるカードもあります!", "cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。", - "cake_pay_subtitle": "割引ギフトカードを購入する (米国のみ)", - "cake_pay_title": "ケーキペイギフトカード", + "cake_pay_subtitle": "世界中のプリペイドカードとギフトカードを購入します", "cake_pay_web_cards_subtitle": "世界中のプリペイド カードとギフト カードを購入する", "cake_pay_web_cards_title": "Cake Pay ウェブカード", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "現在のウォレットを変更する", "choose_account": "アカウントを選択", "choose_address": "\n\n住所を選択してください:", + "choose_card_value": "カード値を選択します", "choose_derivation": "ウォレット派生を選択します", "choose_from_available_options": "利用可能なオプションから選択してください:", "choose_one": "1 つ選択してください", @@ -166,6 +167,7 @@ "copy_address": "住所をコピー", "copy_id": "IDをコピー", "copyWalletConnectLink": "dApp から WalletConnect リンクをコピーし、ここに貼り付けます", + "countries": "国", "create_account": "アカウントの作成", "create_backup": "バックアップを作成", "create_donation_link": "寄付リンクを作成", @@ -178,6 +180,7 @@ "custom": "カスタム", "custom_drag": "カスタム(ホールドとドラッグ)", "custom_redeem_amount": "カスタム交換金額", + "custom_value": "カスタム値", "dark_theme": "闇", "debit_card": "デビットカード", "debit_card_terms": "このデジタルウォレットでの支払いカード番号(および支払いカード番号に対応する資格情報)の保存と使用には、支払いカード発行者との該当するカード所有者契約の利用規約が適用されます。時々。", @@ -190,6 +193,7 @@ "delete_wallet": "ウォレットを削除", "delete_wallet_confirm_message": "${wallet_name} ウォレットを削除してもよろしいですか?", "deleteConnectionConfirmationPrompt": "への接続を削除してもよろしいですか?", + "denominations": "宗派", "descending": "下降", "description": "説明", "destination_tag": "宛先タグ:", @@ -277,6 +281,7 @@ "expired": "期限切れ", "expires": "Expires", "expiresOn": "有効期限は次のとおりです", + "expiry_and_validity": "有効期限と有効性", "export_backup": "バックアップのエクスポート", "extra_id": "追加ID:", "extracted_address_content": "に送金します\n${recipient_name}", @@ -385,6 +390,7 @@ "new_template": "新しいテンプレート", "new_wallet": "新しいウォレット", "newConnection": "新しい接続", + "no_cards_found": "カードは見つかりません", "no_id_needed": "IDは必要ありません!", "no_id_required": "IDは必要ありません。どこにでも補充して使用できます", "no_relay_on_domain": "ユーザーのドメインのリレーが存在しないか、リレーが使用できません。使用するリレーを選択してください。", @@ -453,6 +459,7 @@ "pre_seed_button_text": "わかります。 種を見せて", "pre_seed_description": "次のページでは、一連の${words}語が表示されます。 これはあなたのユニークでプライベートなシードであり、紛失や誤動作が発生した場合にウォレットを回復する唯一の方法です。 それを書き留めて、Cake Wallet アプリの外の安全な場所に保管するのはあなたの責任です。", "pre_seed_title": "重要", + "prepaid_cards": "プリペイドカード", "prevent_screenshots": "スクリーンショットと画面録画を防止する", "privacy": "プライバシー", "privacy_policy": "プライバシーポリシー", @@ -468,6 +475,7 @@ "purple_dark_theme": "紫色の暗いテーマ", "qr_fullscreen": "タップして全画面QRコードを開く", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "量", "question_to_disable_2fa": "Cake 2FA を無効にしてもよろしいですか?ウォレットと特定の機能にアクセスするために 2FA コードは必要なくなります。", "receivable_balance": "売掛金残高", "receive": "受け取る", @@ -709,6 +717,7 @@ "tokenID": "ID", "tor_connection": "Tor接続", "tor_only": "Torのみ", + "total": "合計", "total_saving": "合計節約額", "totp_2fa_failure": "コードが正しくありません。 別のコードを試すか、新しい秘密鍵を生成してください。 8 桁のコードと SHA512 をサポートする互換性のある 2FA アプリを使用してください。", "totp_2fa_success": "成功!このウォレットでは Cake 2FA が有効になっています。ウォレットへのアクセスを失った場合に備えて、ニーモニック シードを忘れずに保存してください。", @@ -799,6 +808,8 @@ "use_ssl": "SSLを使用する", "use_suggested": "推奨を使用", "use_testnet": "テストネットを使用します", + "value": "価値", + "value_type": "値タイプ", "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません", "verification": "検証", "verify_with_2fa": "Cake 2FA で検証する", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index bf1e4ee61..99d138de1 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -87,6 +87,7 @@ "buy": "구입", "buy_alert_content": "현재 Bitcoin, Ethereum, Litecoin 및 Monero 구매만 지원합니다. Bitcoin, Ethereum, Litecoin 또는 Monero 지갑을 생성하거나 전환하십시오.", "buy_bitcoin": "비트 코인 구매", + "buy_now": "지금 구매하십시오", "buy_provider_unavailable": "제공자는 현재 사용할 수 없습니다.", "buy_with": "구매", "by_cake_pay": "Cake Pay로", @@ -94,8 +95,7 @@ "cake_dark_theme": "케이크 다크 테마", "cake_pay_account_note": "이메일 주소로 가입하면 카드를 보고 구매할 수 있습니다. 일부는 할인된 가격으로 사용 가능합니다!", "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", - "cake_pay_subtitle": "할인된 기프트 카드 구매(미국만 해당)", - "cake_pay_title": "케이크 페이 기프트 카드", + "cake_pay_subtitle": "전세계 선불 카드와 기프트 카드를 구입하십시오", "cake_pay_web_cards_subtitle": "전 세계 선불 카드 및 기프트 카드 구매", "cake_pay_web_cards_title": "케이크페이 웹카드", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "현재 지갑 변경", "choose_account": "계정을 선택하십시오", "choose_address": "\n\n주소를 선택하십시오:", + "choose_card_value": "카드 값을 선택하십시오", "choose_derivation": "지갑 파생을 선택하십시오", "choose_from_available_options": "사용 가능한 옵션에서 선택:", "choose_one": "하나 선택", @@ -166,6 +167,7 @@ "copy_address": "주소 복사", "copy_id": "부 ID", "copyWalletConnectLink": "dApp에서 WalletConnect 링크를 복사하여 여기에 붙여넣으세요.", + "countries": "국가", "create_account": "계정 만들기", "create_backup": "백업 생성", "create_donation_link": "기부 링크 만들기", @@ -178,6 +180,7 @@ "custom": "커스텀", "custom_drag": "사용자 정의 (홀드 앤 드래그)", "custom_redeem_amount": "사용자 지정 상환 금액", + "custom_value": "맞춤 가치", "dark_theme": "어두운", "debit_card": "직불 카드", "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", @@ -190,6 +193,7 @@ "delete_wallet": "지갑 삭제", "delete_wallet_confirm_message": "${wallet_name} 지갑을 삭제하시겠습니까?", "deleteConnectionConfirmationPrompt": "다음 연결을 삭제하시겠습니까?", + "denominations": "교파", "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", @@ -277,6 +281,7 @@ "expired": "만료", "expires": "만료", "expiresOn": "만료 날짜", + "expiry_and_validity": "만료와 타당성", "export_backup": "백업 내보내기", "extra_id": "추가 ID:", "extracted_address_content": "당신은에 자금을 보낼 것입니다\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "새 템플릿", "new_wallet": "새 월렛", "newConnection": "새로운 연결", + "no_cards_found": "카드를 찾지 못했습니다", "no_id_needed": "ID가 필요하지 않습니다!", "no_id_required": "신분증이 필요하지 않습니다. 충전하고 어디에서나 사용하세요", "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택해주세요.", @@ -453,6 +459,7 @@ "pre_seed_button_text": "이해 했어요. 내 씨앗을 보여줘", "pre_seed_description": "다음 페이지에서 ${words} 개의 단어를 볼 수 있습니다. 이것은 귀하의 고유하고 개인적인 시드이며 분실 또는 오작동시 지갑을 복구하는 유일한 방법입니다. 기록해두고 Cake Wallet 앱 외부의 안전한 장소에 보관하는 것은 귀하의 책임입니다.", "pre_seed_title": "중대한", + "prepaid_cards": "선불 카드", "prevent_screenshots": "스크린샷 및 화면 녹화 방지", "privacy": "프라이버시", "privacy_policy": "개인 정보 보호 정책", @@ -468,6 +475,7 @@ "purple_dark_theme": "보라색 어두운 테마", "qr_fullscreen": "전체 화면 QR 코드를 열려면 탭하세요.", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "수량", "question_to_disable_2fa": "Cake 2FA를 비활성화하시겠습니까? 지갑 및 특정 기능에 액세스하는 데 더 이상 2FA 코드가 필요하지 않습니다.", "receivable_balance": "채권 잔액", "receive": "받다", @@ -709,6 +717,7 @@ "tokenID": "ID", "tor_connection": "토르 연결", "tor_only": "Tor 뿐", + "total": "총", "total_saving": "총 절감액", "totp_2fa_failure": "잘못된 코드입니다. 다른 코드를 시도하거나 새 비밀 키를 생성하십시오. 8자리 코드와 SHA512를 지원하는 호환되는 2FA 앱을 사용하세요.", "totp_2fa_success": "성공! 이 지갑에 케이크 2FA가 활성화되었습니다. 지갑 액세스 권한을 잃을 경우를 대비하여 니모닉 시드를 저장하는 것을 잊지 마십시오.", @@ -799,6 +808,8 @@ "use_ssl": "SSL 사용", "use_suggested": "추천 사용", "use_testnet": "TestNet을 사용하십시오", + "value": "값", + "value_type": "가치 유형", "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.", "verification": "검증", "verify_with_2fa": "케이크 2FA로 확인", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 7530b6678..41b082457 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -87,6 +87,7 @@ "buy": "ဝယ်ပါ။", "buy_alert_content": "လက်ရှိတွင် ကျွန်ုပ်တို့သည် Bitcoin၊ Ethereum၊ Litecoin နှင့် Monero တို့ကိုသာ ဝယ်ယူမှုကို ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Ethereum၊ Litecoin သို့မဟုတ် Monero ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", "buy_bitcoin": "Bitcoin ကိုဝယ်ပါ။", + "buy_now": "အခုဝယ်ပါ", "buy_provider_unavailable": "လက်ရှိတွင်လက်ရှိမရနိုင်ပါ။", "buy_with": "အတူဝယ်ပါ။", "by_cake_pay": "Cake Pay ဖြင့်", @@ -94,8 +95,7 @@ "cake_dark_theme": "ကိတ်မုန့် Dark Theme", "cake_pay_account_note": "ကတ်များကြည့်ရှုဝယ်ယူရန် အီးမေးလ်လိပ်စာတစ်ခုဖြင့် စာရင်းသွင်းပါ။ အချို့ကို လျှော့ဈေးဖြင့်ပင် ရနိုင်သည်။", "cake_pay_learn_more": "အက်ပ်ရှိ လက်ဆောင်ကတ်များကို ချက်ချင်းဝယ်ယူပြီး ကူပွန်ဖြင့် လဲလှယ်ပါ။\nပိုမိုလေ့လာရန် ဘယ်မှညာသို့ ပွတ်ဆွဲပါ။", - "cake_pay_subtitle": "လျှော့စျေးလက်ဆောင်ကတ်များဝယ်ပါ (USA သာ)", - "cake_pay_title": "ကိတ်မုန့်လက်ဆောင်ကတ်များ", + "cake_pay_subtitle": "Worldwide ကြိုတင်ငွေဖြည့်ကဒ်များနှင့်လက်ဆောင်ကဒ်များကို 0 ယ်ပါ", "cake_pay_web_cards_subtitle": "ကမ္ဘာတစ်ဝှမ်း ကြိုတင်ငွေပေးကတ်များနှင့် လက်ဆောင်ကတ်များကို ဝယ်ယူပါ။", "cake_pay_web_cards_title": "Cake Pay ဝဘ်ကတ်များ", "cake_wallet": "Cake ပိုက်ဆံအိတ်", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "လက်ရှိပိုက်ဆံအိတ်ကို ပြောင်းပါ။", "choose_account": "အကောင့်ကို ရွေးပါ။", "choose_address": "\n\nလိပ်စာကို ရွေးပါ-", + "choose_card_value": "ကဒ်တန်ဖိုးတစ်ခုရွေးပါ", "choose_derivation": "ပိုက်ဆံအိတ်ကိုရွေးချယ်ပါ", "choose_from_available_options": "ရနိုင်သောရွေးချယ်မှုများမှ ရွေးပါ-", "choose_one": "တစ်ခုရွေးပါ။", @@ -166,6 +167,7 @@ "copy_address": "လိပ်စာကို ကူးယူပါ။", "copy_id": "ID ကူးယူပါ။", "copyWalletConnectLink": "dApp မှ WalletConnect လင့်ခ်ကို ကူးယူပြီး ဤနေရာတွင် ကူးထည့်ပါ။", + "countries": "နိုင်ငံများ", "create_account": "အကောင့်ပြုလုပ်ပါ", "create_backup": "အရန်သိမ်းခြင်းကို ဖန်တီးပါ။", "create_donation_link": "လှူဒါန်းမှုလင့်ခ်ကို ဖန်တီးပါ။", @@ -178,6 +180,7 @@ "custom": "စိတ်ကြိုက်", "custom_drag": "စိတ်ကြိုက် (Drag)", "custom_redeem_amount": "စိတ်ကြိုက်သုံးငွေပမာဏ", + "custom_value": "စိတ်ကြိုက်တန်ဖိုး", "dark_theme": "မှောငျမိုကျသော", "debit_card": "ဒက်ဘစ်ကတ်", "debit_card_terms": "ဤဒစ်ဂျစ်တယ်ပိုက်ဆံအိတ်ရှိ သင့်ငွေပေးချေမှုကတ်နံပါတ် (နှင့် သင့်ငွေပေးချေကတ်နံပါတ်နှင့် သက်ဆိုင်သောအထောက်အထားများ) ၏ သိုလှောင်မှုနှင့် အသုံးပြုမှုသည် အချိန်အခါနှင့်အမျှ သက်ရောက်မှုရှိသကဲ့သို့ ကတ်ကိုင်ဆောင်ထားသူ၏ သဘောတူညီချက်၏ စည်းကမ်းသတ်မှတ်ချက်များနှင့် ကိုက်ညီပါသည်။", @@ -190,6 +193,7 @@ "delete_wallet": "ပိုက်ဆံအိတ်ကို ဖျက်ပါ။", "delete_wallet_confirm_message": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖျက်လိုသည်မှာ သေချာပါသလား။", "deleteConnectionConfirmationPrompt": "ချိတ်ဆက်မှုကို ဖျက်လိုသည်မှာ သေချာပါသလား။", + "denominations": "ဂိုဏ်းချုပ်ပစ္စည်းများ", "descending": "ဆင်း", "description": "ဖော်ပြချက်", "destination_tag": "ခရီးဆုံးအမှတ်-", @@ -277,6 +281,7 @@ "expired": "သက်တမ်းကုန်သွားပြီ", "expires": "သက်တမ်းကုန်သည်။", "expiresOn": "သက်တမ်းကုန်သည်။", + "expiry_and_validity": "သက်တမ်းကုန်ဆုံးခြင်းနှင့်တရားဝင်မှု", "export_backup": "အရန်ကူးထုတ်ရန်", "extra_id": "အပို ID-", "extracted_address_content": "သင်သည် \n${recipient_name} သို့ ရန်ပုံငွေများ ပေးပို့ပါမည်", @@ -384,6 +389,7 @@ "new_template": "ပုံစံအသစ်", "new_wallet": "ပိုက်ဆံအိတ်အသစ်", "newConnection": "ချိတ်ဆက်မှုအသစ်", + "no_cards_found": "ကဒ်များမရှိပါ", "no_id_needed": "ID မလိုအပ်ပါ။", "no_id_required": "ID မလိုအပ်ပါ။ ငွေဖြည့်ပြီး ဘယ်နေရာမဆို သုံးစွဲပါ။", "no_relay_on_domain": "အသုံးပြုသူ၏ဒိုမိန်းအတွက် ထပ်ဆင့်လွှင့်ခြင်း မရှိပါ သို့မဟုတ် ထပ်ဆင့်လွှင့်ခြင်း မရနိုင်ပါ။ အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", @@ -452,6 +458,7 @@ "pre_seed_button_text": "ကျွန်တော်နားလည်ပါတယ်။ ငါ့အမျိုးအနွယ်ကို ပြလော့", "pre_seed_description": "နောက်စာမျက်နှာတွင် ${words} စကားလုံးများ အတွဲလိုက်ကို တွေ့ရပါမည်။ ၎င်းသည် သင်၏ထူးခြားပြီး သီးသန့်မျိုးစေ့ဖြစ်ပြီး ပျောက်ဆုံးခြင်း သို့မဟုတ် ချွတ်ယွင်းမှုရှိပါက သင့်ပိုက်ဆံအိတ်ကို ပြန်လည်ရယူရန် တစ်ခုတည်းသောနည်းလမ်းဖြစ်သည်။ ၎င်းကို Cake Wallet အက်ပ်၏အပြင်ဘက်တွင် လုံခြုံသောနေရာတွင် သိမ်းဆည်းရန်မှာ သင်၏တာဝန်ဖြစ်သည်။", "pre_seed_title": "အရေးကြီးသည်။", + "prepaid_cards": "ကြိုတင်ငွေဖြည့်ကဒ်များ", "prevent_screenshots": "ဖန်သားပြင်ဓာတ်ပုံများနှင့် မျက်နှာပြင်ရိုက်ကူးခြင်းကို တားဆီးပါ။", "privacy": "ကိုယ်ရေးကိုယ်တာ", "privacy_policy": "ကိုယ်ရေးအချက်အလက်မူဝါဒ", @@ -467,6 +474,7 @@ "purple_dark_theme": "ခရမ်းရောင် Drwing Theme", "qr_fullscreen": "မျက်နှာပြင်အပြည့် QR ကုဒ်ကိုဖွင့်ရန် တို့ပါ။", "qr_payment_amount": "ဤ QR ကုဒ်တွင် ငွေပေးချေမှုပမာဏတစ်ခုပါရှိသည်။ လက်ရှိတန်ဖိုးကို ထပ်ရေးလိုပါသလား။", + "quantity": "အရေအတွက်", "question_to_disable_2fa": "Cake 2FA ကို ပိတ်လိုသည်မှာ သေချာပါသလား။ ပိုက်ဆံအိတ်နှင့် အချို့သောလုပ်ဆောင်ချက်များကို အသုံးပြုရန်အတွက် 2FA ကုဒ်တစ်ခု မလိုအပ်တော့ပါ။", "receivable_balance": "လက်ကျန်ငွေ", "receive": "လက်ခံသည်။", @@ -708,6 +716,7 @@ "tokenID": "အမှတ်သညာ", "tor_connection": "Tor ချိတ်ဆက်မှု", "tor_only": "Tor သာ", + "total": "လုံးဝသော", "total_saving": "စုစုပေါင်းစုဆောင်းငွေ", "totp_2fa_failure": "ကုဒ်မမှန်ပါ။ ကျေးဇူးပြု၍ အခြားကုဒ်တစ်ခုကို စမ်းကြည့်ပါ သို့မဟုတ် လျှို့ဝှက်သော့အသစ်တစ်ခု ဖန်တီးပါ။ ဂဏန်း ၈ လုံးကုဒ်များနှင့် SHA512 ကို ပံ့ပိုးပေးသည့် တွဲဖက်အသုံးပြုနိုင်သော 2FA အက်ပ်ကို အသုံးပြုပါ။", "totp_2fa_success": "အောင်မြင် ဤပိုက်ဆံအိတ်အတွက် ကိတ်မုန့် 2FA ကို ဖွင့်ထားသည်။ ပိုက်ဆံအိတ်ဝင်ရောက်ခွင့်ဆုံးရှုံးသွားသောအခါတွင် သင်၏ mnemonic မျိုးစေ့များကို သိမ်းဆည်းရန် မမေ့ပါနှင့်။", @@ -798,6 +807,8 @@ "use_ssl": "SSL ကိုသုံးပါ။", "use_suggested": "အကြံပြုထားသည်ကို အသုံးပြုပါ။", "use_testnet": "testnet ကိုသုံးပါ", + "value": "အဘိုး", + "value_type": "Value အမျိုးအစား", "variable_pair_not_supported": "ရွေးချယ်ထားသော ဖလှယ်မှုများဖြင့် ဤပြောင်းလဲနိုင်သောအတွဲကို ပံ့ပိုးမထားပါ။", "verification": "စိစစ်ခြင်း။", "verify_with_2fa": "Cake 2FA ဖြင့် စစ်ဆေးပါ။", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 7764cb659..a7a3a20a5 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -87,6 +87,7 @@ "buy": "Kopen", "buy_alert_content": "Momenteel ondersteunen we alleen de aankoop van Bitcoin, Ethereum, Litecoin en Monero. Maak of schakel over naar uw Bitcoin-, Ethereum-, Litecoin- of Monero-portemonnee.", "buy_bitcoin": "Koop Bitcoin", + "buy_now": "Koop nu", "buy_provider_unavailable": "Provider momenteel niet beschikbaar.", "buy_with": "Koop met", "by_cake_pay": "door Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake Dark Theme", "cake_pay_account_note": "Meld u aan met alleen een e-mailadres om kaarten te bekijken en te kopen. Sommige zijn zelfs met korting verkrijgbaar!", "cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.", - "cake_pay_subtitle": "Koop cadeaubonnen met korting (alleen VS)", - "cake_pay_title": "Cake Pay-cadeaubonnen", + "cake_pay_subtitle": "Koop wereldwijde prepaid -kaarten en cadeaubonnen", "cake_pay_web_cards_subtitle": "Koop wereldwijd prepaidkaarten en cadeaubonnen", "cake_pay_web_cards_title": "Cake Pay-webkaarten", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Wijzig huidige portemonnee", "choose_account": "Kies account", "choose_address": "\n\nKies het adres:", + "choose_card_value": "Kies een kaartwaarde", "choose_derivation": "Kies portemonnee -afleiding", "choose_from_available_options": "Kies uit de beschikbare opties:", "choose_one": "Kies er een", @@ -166,6 +167,7 @@ "copy_address": "Adres kopiëren", "copy_id": "ID kopiëren", "copyWalletConnectLink": "Kopieer de WalletConnect-link van dApp en plak deze hier", + "countries": "Landen", "create_account": "Account aanmaken", "create_backup": "Maak een back-up", "create_donation_link": "Maak een donatielink aan", @@ -178,6 +180,7 @@ "custom": "aangepast", "custom_drag": "Custom (vasthouden en slepen)", "custom_redeem_amount": "Aangepast inwisselbedrag", + "custom_value": "Aangepaste waarde", "dark_theme": "Donker", "debit_card": "Debetkaart", "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", @@ -190,6 +193,7 @@ "delete_wallet": "Portemonnee verwijderen", "delete_wallet_confirm_message": "Weet u zeker dat u de portemonnee van ${wallet_name} wilt verwijderen?", "deleteConnectionConfirmationPrompt": "Weet u zeker dat u de verbinding met", + "denominations": "Denominaties", "descending": "Aflopend", "description": "Beschrijving", "destination_tag": "Bestemmingstag:", @@ -277,6 +281,7 @@ "expired": "Verlopen", "expires": "Verloopt", "expiresOn": "Verloopt op", + "expiry_and_validity": "Vervallen en geldigheid", "export_backup": "Back-up exporteren", "extra_id": "Extra ID:", "extracted_address_content": "U stuurt geld naar\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Nieuwe sjabloon", "new_wallet": "Nieuwe portemonnee", "newConnection": "Nieuwe verbinding", + "no_cards_found": "Geen kaarten gevonden", "no_id_needed": "Geen ID nodig!", "no_id_required": "Geen ID vereist. Opwaarderen en overal uitgeven", "no_relay_on_domain": "Er is geen relay voor het domein van de gebruiker of de relay is niet beschikbaar. Kies een relais dat u wilt gebruiken.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Ik begrijp het. Laat me mijn zaad zien", "pre_seed_description": "Op de volgende pagina ziet u een reeks van ${words} woorden. Dit is uw unieke en persoonlijke zaadje en het is de ENIGE manier om uw portemonnee te herstellen in geval van verlies of storing. Het is JOUW verantwoordelijkheid om het op te schrijven en op een veilige plaats op te slaan buiten de Cake Wallet app.", "pre_seed_title": "BELANGRIJK", + "prepaid_cards": "Prepaid-kaarten", "prevent_screenshots": "Voorkom screenshots en schermopname", "privacy": "Privacy", "privacy_policy": "Privacybeleid", @@ -467,6 +474,7 @@ "purple_dark_theme": "Paars donker thema", "qr_fullscreen": "Tik om de QR-code op volledig scherm te openen", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Hoeveelheid", "question_to_disable_2fa": "Weet je zeker dat je Cake 2FA wilt uitschakelen? Er is geen 2FA-code meer nodig om toegang te krijgen tot de portemonnee en bepaalde functies.", "receivable_balance": "Het saldo", "receive": "Krijgen", @@ -708,6 +716,7 @@ "tokenID": "ID kaart", "tor_connection": "Tor-verbinding", "tor_only": "Alleen Tor", + "total": "Totaal", "total_saving": "Totale besparingen", "totp_2fa_failure": "Foute code. Probeer een andere code of genereer een nieuwe geheime sleutel. Gebruik een compatibele 2FA-app die 8-cijferige codes en SHA512 ondersteunt.", "totp_2fa_success": "Succes! Cake 2FA ingeschakeld voor deze portemonnee. Vergeet niet om uw geheugensteuntje op te slaan voor het geval u de toegang tot de portemonnee kwijtraakt.", @@ -798,6 +807,8 @@ "use_ssl": "Gebruik SSL", "use_suggested": "Gebruik aanbevolen", "use_testnet": "Gebruik testnet", + "value": "Waarde", + "value_type": "Waarde type", "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen", "verification": "Verificatie", "verify_with_2fa": "Controleer met Cake 2FA", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index ef502d31b..e3cea5cad 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -87,6 +87,7 @@ "buy": "Kup", "buy_alert_content": "Obecnie obsługujemy tylko zakup Bitcoin, Ethereum, Litecoin i Monero. Utwórz lub przełącz się na swój portfel Bitcoin, Ethereum, Litecoin lub Monero.", "buy_bitcoin": "Kup Bitcoin", + "buy_now": "Kup Teraz", "buy_provider_unavailable": "Dostawca obecnie niedostępny.", "buy_with": "Kup za pomocą", "by_cake_pay": "przez Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake Dark Temat", "cake_pay_account_note": "Zarejestruj się, używając tylko adresu e-mail, aby przeglądać i kupować karty. Niektóre są nawet dostępne ze zniżką!", "cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.", - "cake_pay_subtitle": "Kup karty upominkowe ze zniżką (tylko USA)", - "cake_pay_title": "Karty podarunkowe Cake Pay", + "cake_pay_subtitle": "Kup na całym świecie karty przedpłacone i karty podarunkowe", "cake_pay_web_cards_subtitle": "Kupuj na całym świecie karty przedpłacone i karty podarunkowe", "cake_pay_web_cards_title": "Cake Pay Web Cards", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Zmień obecny portfel", "choose_account": "Wybierz konto", "choose_address": "\n\nWybierz adres:", + "choose_card_value": "Wybierz wartość karty", "choose_derivation": "Wybierz wyprowadzenie portfela", "choose_from_available_options": "Wybierz z dostępnych opcji:", "choose_one": "Wybierz jeden", @@ -166,6 +167,7 @@ "copy_address": "Skopiuj adress", "copy_id": "skopiuj ID", "copyWalletConnectLink": "Skopiuj link do WalletConnect z dApp i wklej tutaj", + "countries": "Kraje", "create_account": "Utwórz konto", "create_backup": "Utwórz kopię zapasową", "create_donation_link": "Utwórz link do darowizny", @@ -178,6 +180,7 @@ "custom": "niestandardowy", "custom_drag": "Niestandardowe (trzymaj i przeciągnij)", "custom_redeem_amount": "Niestandardowa kwota wykorzystania", + "custom_value": "Wartość niestandardowa", "dark_theme": "Ciemny", "debit_card": "Karta debetowa", "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", @@ -190,6 +193,7 @@ "delete_wallet": "Usuń portfel", "delete_wallet_confirm_message": "Czy na pewno chcesz usunąć portfel ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Czy na pewno chcesz usunąć połączenie z", + "denominations": "Wyznaczenia", "descending": "Schodzenie", "description": "Opis", "destination_tag": "Tag docelowy:", @@ -277,6 +281,7 @@ "expired": "Przedawniony", "expires": "Wygasa", "expiresOn": "Upływa w dniu", + "expiry_and_validity": "Wygaśnięcie i ważność", "export_backup": "Eksportuj kopię zapasową", "extra_id": "Dodatkowy ID:", "extracted_address_content": "Wysyłasz środki na\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Nowy szablon", "new_wallet": "Nowy portfel", "newConnection": "Nowe połączenie", + "no_cards_found": "Nie znaleziono żadnych kart", "no_id_needed": "Nie potrzeba Dowodu!", "no_id_required": "Nie wymagamy Dowodu. Doładuj i wydawaj gdziekolwiek", "no_relay_on_domain": "Brak przekaźnika dla domeny użytkownika lub przekaźnik jest niedostępny. Wybierz przekaźnik, którego chcesz użyć.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Rozumiem. Pokaż mi moją fraze seed", "pre_seed_description": "Na następnej stronie zobaczysz serię ${words} słów. To jest Twoja unikalna i prywatna fraza seed i jest to JEDYNY sposób na odzyskanie portfela w przypadku utraty lub awarii telefonu. Twoim obowiązkiem jest zapisanie go i przechowywanie w bezpiecznym miejscu (np. na kartce w SEJFIE).", "pre_seed_title": "WAŻNY", + "prepaid_cards": "Karty przedpłacone", "prevent_screenshots": "Zapobiegaj zrzutom ekranu i nagrywaniu ekranu", "privacy": "Prywatność", "privacy_policy": "Polityka prywatności", @@ -467,6 +474,7 @@ "purple_dark_theme": "Purple Dark Temat", "qr_fullscreen": "Dotknij, aby otworzyć pełnoekranowy kod QR", "qr_payment_amount": "Ten kod QR zawiera kwotę do zapłaty. Czy chcesz nadpisać obecną wartość?", + "quantity": "Ilość", "question_to_disable_2fa": "Czy na pewno chcesz wyłączyć Cake 2FA? Kod 2FA nie będzie już potrzebny do uzyskania dostępu do portfela i niektórych funkcji.", "receivable_balance": "Saldo należności", "receive": "Otrzymaj", @@ -708,6 +716,7 @@ "tokenID": "ID", "tor_connection": "Połączenie Torem", "tor_only": "Tylko sieć Tor", + "total": "Całkowity", "total_saving": "Całkowite oszczędności", "totp_2fa_failure": "Błędny kod. Spróbuj użyć innego kodu lub wygeneruj nowy tajny klucz. Użyj kompatybilnej aplikacji 2FA, która obsługuje 8-cyfrowe kody i SHA512.", "totp_2fa_success": "Powodzenie! Cake 2FA włączony dla tego portfela. Pamiętaj, aby zapisać swoje mnemoniczne ziarno na wypadek utraty dostępu do portfela.", @@ -798,6 +807,8 @@ "use_ssl": "Użyj SSL", "use_suggested": "Użyj sugerowane", "use_testnet": "Użyj testne", + "value": "Wartość", + "value_type": "Typ wartości", "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach", "verification": "Weryfikacja", "verify_with_2fa": "Sprawdź za pomocą Cake 2FA", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 02fcf11a5..45241e5a0 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -87,6 +87,7 @@ "buy": "Comprar", "buy_alert_content": "Atualmente, oferecemos suporte apenas à compra de Bitcoin, Ethereum, Litecoin e Monero. Crie ou troque para sua carteira Bitcoin, Ethereum, Litecoin ou Monero.", "buy_bitcoin": "Compre Bitcoin", + "buy_now": "Comprar agora", "buy_provider_unavailable": "Provedor atualmente indisponível.", "buy_with": "Compre com", "by_cake_pay": "por Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Bolo tema escuro", "cake_pay_account_note": "Inscreva-se com apenas um endereço de e-mail para ver e comprar cartões. Alguns estão até com desconto!", "cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.", - "cake_pay_subtitle": "Compre vales-presente com desconto (somente nos EUA)", - "cake_pay_title": "Cartões de presente de CakePay", + "cake_pay_subtitle": "Compre cartões pré -pagos em todo o mundo e cartões -presente", "cake_pay_web_cards_subtitle": "Compre cartões pré-pagos e cartões-presente em todo o mundo", "cake_pay_web_cards_title": "Cartões Cake Pay Web", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Alterar carteira atual", "choose_account": "Escolha uma conta", "choose_address": "\n\nEscolha o endereço:", + "choose_card_value": "Escolha um valor de cartão", "choose_derivation": "Escolha a derivação da carteira", "choose_from_available_options": "Escolha entre as opções disponíveis:", "choose_one": "Escolha um", @@ -166,6 +167,7 @@ "copy_address": "Copiar endereço", "copy_id": "Copiar ID", "copyWalletConnectLink": "Copie o link WalletConnect do dApp e cole aqui", + "countries": "Países", "create_account": "Criar conta", "create_backup": "Criar backup", "create_donation_link": "Criar link de doação", @@ -178,6 +180,7 @@ "custom": "personalizado", "custom_drag": "Personalizado (segure e arraste)", "custom_redeem_amount": "Valor de resgate personalizado", + "custom_value": "Valor customizado", "dark_theme": "Sombria", "debit_card": "Cartão de débito", "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", @@ -190,6 +193,7 @@ "delete_wallet": "Excluir carteira", "delete_wallet_confirm_message": "Tem certeza de que deseja excluir a carteira ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Tem certeza de que deseja excluir a conexão com", + "denominations": "Denominações", "descending": "descendente", "description": "Descrição", "destination_tag": "Tag de destino:", @@ -277,6 +281,7 @@ "expired": "Expirada", "expires": "Expira", "expiresOn": "Expira em", + "expiry_and_validity": "Expiração e validade", "export_backup": "Backup de exportação", "extra_id": "ID extra:", "extracted_address_content": "Você enviará fundos para\n${recipient_name}", @@ -385,6 +390,7 @@ "new_template": "Novo modelo", "new_wallet": "Nova carteira", "newConnection": "Nova conexão", + "no_cards_found": "Nenhum cartão encontrado", "no_id_needed": "Nenhum ID necessário!", "no_id_required": "Não é necessário ID. Recarregue e gaste em qualquer lugar", "no_relay_on_domain": "Não há uma retransmissão para o domínio do usuário ou a retransmissão está indisponível. Escolha um relé para usar.", @@ -454,6 +460,7 @@ "pre_seed_button_text": "Compreendo. Me mostre minha semente", "pre_seed_description": "Na próxima página, você verá uma série de ${words} palavras. Esta é a sua semente única e privada e é a ÚNICA maneira de recuperar sua carteira em caso de perda ou mau funcionamento. É SUA responsabilidade anotá-lo e armazená-lo em um local seguro fora do aplicativo Cake Wallet.", "pre_seed_title": "IMPORTANTE", + "prepaid_cards": "Cartões pré-pagos", "prevent_screenshots": "Evite capturas de tela e gravação de tela", "privacy": "Privacidade", "privacy_policy": "Política de privacidade", @@ -469,6 +476,7 @@ "purple_dark_theme": "Tema escuro roxo", "qr_fullscreen": "Toque para abrir o código QR em tela cheia", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Quantidade", "question_to_disable_2fa": "Tem certeza de que deseja desativar o Cake 2FA? Um código 2FA não será mais necessário para acessar a carteira e certas funções.", "receivable_balance": "Saldo a receber", "receive": "Receber", @@ -710,6 +718,7 @@ "tokenID": "EU IA", "tor_connection": "Conexão Tor", "tor_only": "Tor apenas", + "total": "Total", "total_saving": "Economia total", "totp_2fa_failure": "Código incorreto. Tente um código diferente ou gere uma nova chave secreta. Use um aplicativo 2FA compatível com códigos de 8 dígitos e SHA512.", "totp_2fa_success": "Sucesso! Cake 2FA ativado para esta carteira. Lembre-se de salvar sua semente mnemônica caso perca o acesso à carteira.", @@ -800,6 +809,8 @@ "use_ssl": "Use SSL", "use_suggested": "Uso sugerido", "use_testnet": "Use testNet", + "value": "Valor", + "value_type": "Tipo de valor", "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas", "verification": "Verificação", "verify_with_2fa": "Verificar com Cake 2FA", @@ -864,4 +875,4 @@ "you_will_get": "Converter para", "you_will_send": "Converter de", "yy": "aa" -} +} \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index bb0abbc08..281673326 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -87,6 +87,7 @@ "buy": "Купить", "buy_alert_content": "В настоящее время мы поддерживаем только покупку биткойнов, Ethereum, Litecoin и Monero. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Ethereum, Litecoin или Monero.", "buy_bitcoin": "Купить Bitcoin", + "buy_now": "Купить сейчас", "buy_provider_unavailable": "Поставщик в настоящее время недоступен.", "buy_with": "Купить с помощью", "by_cake_pay": "от Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Тейт темная тема", "cake_pay_account_note": "Зарегистрируйтесь, указав только адрес электронной почты, чтобы просматривать и покупать карты. Некоторые даже доступны со скидкой!", "cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.", - "cake_pay_subtitle": "Покупайте подарочные карты со скидкой (только для США)", - "cake_pay_title": "Подарочные карты Cake Pay", + "cake_pay_subtitle": "Купить карты с предоплатой и подарочными картами по всему миру", "cake_pay_web_cards_subtitle": "Покупайте карты предоплаты и подарочные карты по всему миру", "cake_pay_web_cards_title": "Веб-карты Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Изменить текущий кошелек", "choose_account": "Выберите аккаунт", "choose_address": "\n\nПожалуйста, выберите адрес:", + "choose_card_value": "Выберите значение карты", "choose_derivation": "Выберите вывод кошелька", "choose_from_available_options": "Выберите из доступных вариантов:", "choose_one": "Выбери один", @@ -166,6 +167,7 @@ "copy_address": "Cкопировать адрес", "copy_id": "Скопировать ID", "copyWalletConnectLink": "Скопируйте ссылку WalletConnect из dApp и вставьте сюда.", + "countries": "Страны", "create_account": "Создать аккаунт", "create_backup": "Создать резервную копию", "create_donation_link": "Создать ссылку для пожертвований", @@ -178,6 +180,7 @@ "custom": "обычай", "custom_drag": "Пользователь (удерживайте и перетаскивайте)", "custom_redeem_amount": "Пользовательская сумма погашения", + "custom_value": "Пользовательское значение", "dark_theme": "Темная", "debit_card": "Дебетовая карта", "debit_card_terms": "Хранение и использование номера вашей платежной карты (и учетных данных, соответствующих номеру вашей платежной карты) в этом цифровом кошельке регулируются положениями и условиями применимого соглашения держателя карты с эмитентом платежной карты, действующим с время от времени.", @@ -190,6 +193,7 @@ "delete_wallet": "Удалить кошелек", "delete_wallet_confirm_message": "Вы уверены, что хотите удалить кошелек ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Вы уверены, что хотите удалить подключение к", + "denominations": "Деноминации", "descending": "Нисходящий", "description": "Описание", "destination_tag": "Целевой тег:", @@ -277,6 +281,7 @@ "expired": "Истекает", "expires": "Истекает", "expiresOn": "Годен до", + "expiry_and_validity": "Истечение и достоверность", "export_backup": "Экспорт резервной копии", "extra_id": "Дополнительный ID:", "extracted_address_content": "Вы будете отправлять средства\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Новый шаблон", "new_wallet": "Новый кошелёк", "newConnection": "Новое соединение", + "no_cards_found": "Карт не найдено", "no_id_needed": "Идентификатор не нужен!", "no_id_required": "Идентификатор не требуется. Пополняйте и тратьте где угодно", "no_relay_on_domain": "Для домена пользователя реле не существует или реле недоступно. Пожалуйста, выберите реле для использования.", @@ -453,6 +459,7 @@ "pre_seed_button_text": "Понятно. Покажите мнемоническую фразу", "pre_seed_description": "На следующей странице вы увидите серию из ${words} слов. Это ваша уникальная и личная мнемоническая фраза, и это ЕДИНСТВЕННЫЙ способ восстановить свой кошелек в случае потери или неисправности. ВАМ необходимо записать ее и хранить в надежном месте вне приложения Cake Wallet.", "pre_seed_title": "ВАЖНО", + "prepaid_cards": "Предоплаченные карты", "prevent_screenshots": "Предотвратить скриншоты и запись экрана", "privacy": "Конфиденциальность", "privacy_policy": "Политика конфиденциальности", @@ -468,6 +475,7 @@ "purple_dark_theme": "Пурпурная темная тема", "qr_fullscreen": "Нажмите, чтобы открыть полноэкранный QR-код", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Количество", "question_to_disable_2fa": "Вы уверены, что хотите отключить Cake 2FA? Код 2FA больше не потребуется для доступа к кошельку и некоторым функциям.", "receivable_balance": "Баланс дебиторской задолженности", "receive": "Получить", @@ -709,6 +717,7 @@ "tokenID": "ИДЕНТИФИКАТОР", "tor_connection": "Тор соединение", "tor_only": "Только Tor", + "total": "Общий", "total_saving": "Общая экономия", "totp_2fa_failure": "Неверный код. Пожалуйста, попробуйте другой код или создайте новый секретный ключ. Используйте совместимое приложение 2FA, которое поддерживает 8-значные коды и SHA512.", "totp_2fa_success": "Успех! Для этого кошелька включена двухфакторная аутентификация Cake. Не забудьте сохранить мнемоническое семя на случай, если вы потеряете доступ к кошельку.", @@ -799,6 +808,8 @@ "use_ssl": "Использовать SSL", "use_suggested": "Использовать предложенный", "use_testnet": "Используйте Testnet", + "value": "Ценить", + "value_type": "Тип значения", "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.", "verification": "Проверка", "verify_with_2fa": "Подтвердить с помощью Cake 2FA", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 990c92cb1..98dfed3ea 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -87,6 +87,7 @@ "buy": "ซื้อ", "buy_alert_content": "ขณะนี้เรารองรับการซื้อ Bitcoin, Ethereum, Litecoin และ Monero เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Ethereum, Litecoin หรือ Monero", "buy_bitcoin": "ซื้อ Bitcoin", + "buy_now": "ซื้อตอนนี้", "buy_provider_unavailable": "ผู้ให้บริการไม่สามารถใช้งานได้ในปัจจุบัน", "buy_with": "ซื้อด้วย", "by_cake_pay": "โดย Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "ธีมเค้กมืด", "cake_pay_account_note": "ลงทะเบียนด้วยอีเมลเพียงอย่างเดียวเพื่อดูและซื้อบัตร บางบัตรอาจมีส่วนลด!", "cake_pay_learn_more": "ซื้อและเบิกบัตรของขวัญในแอพพลิเคชันทันที!\nกระแทกขวาไปซ้ายเพื่อเรียนรู้เพิ่มเติม", - "cake_pay_subtitle": "ซื้อบัตรของขวัญราคาถูก (สำหรับสหรัฐอเมริกาเท่านั้น)", - "cake_pay_title": "บัตรของขวัญ Cake Pay", + "cake_pay_subtitle": "ซื้อบัตรเติมเงินและบัตรของขวัญทั่วโลก", "cake_pay_web_cards_subtitle": "ซื้อบัตรพร้อมเงินระดับโลกและบัตรของขวัญ", "cake_pay_web_cards_title": "Cake Pay Web Cards", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "เปลี่ยนกระเป๋าปัจจุบัน", "choose_account": "เลือกบัญชี", "choose_address": "\n\nโปรดเลือกที่อยู่:", + "choose_card_value": "เลือกค่าบัตร", "choose_derivation": "เลือก Wallet Derivation", "choose_from_available_options": "เลือกจากตัวเลือกที่มีอยู่:", "choose_one": "เลือกหนึ่งรายการ", @@ -166,6 +167,7 @@ "copy_address": "คัดลอกที่อยู่", "copy_id": "คัดลอก ID", "copyWalletConnectLink": "คัดลอกลิงก์ WalletConnect จาก dApp แล้ววางที่นี่", + "countries": "ประเทศ", "create_account": "สร้างบัญชี", "create_backup": "สร้างการสำรองข้อมูล", "create_donation_link": "สร้างลิงค์บริจาค", @@ -178,6 +180,7 @@ "custom": "กำหนดเอง", "custom_drag": "กำหนดเอง (ค้างและลาก)", "custom_redeem_amount": "จำนวนรับคืนที่กำหนดเอง", + "custom_value": "ค่าที่กำหนดเอง", "dark_theme": "เข้ม", "debit_card": "บัตรเดบิต", "debit_card_terms": "การเก็บรักษาและใช้หมายเลขบัตรจ่ายเงิน (และข้อมูลประจำตัวที่เกี่ยวข้องกับหมายเลขบัตรจ่ายเงิน) ในกระเป๋าดิจิทัลนี้ จะต้องยึดถือข้อกำหนดและเงื่อนไขของข้อตกลงผู้ใช้บัตรของผู้ถือบัตรที่เกี่ยวข้องกับบัตรผู้ถือบัตร ซึ่งจะมีผลตั้งแต่เวลานั้น", @@ -190,6 +193,7 @@ "delete_wallet": "ลบกระเป๋า", "delete_wallet_confirm_message": "คุณแน่ใจหรือว่าต้องการลบกระเป๋า${wallet_name}?", "deleteConnectionConfirmationPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการเชื่อมต่อไปยัง", + "denominations": "นิกาย", "descending": "ลงมา", "description": "คำอธิบาย", "destination_tag": "แท็กปลายทาง:", @@ -277,6 +281,7 @@ "expired": "หมดอายุ", "expires": "หมดอายุ", "expiresOn": "หมดอายุวันที่", + "expiry_and_validity": "หมดอายุและถูกต้อง", "export_backup": "ส่งออกข้อมูลสำรอง", "extra_id": "ไอดีเพิ่มเติม:", "extracted_address_content": "คุณกำลังจะส่งเงินไปยัง\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "แม่แบบใหม่", "new_wallet": "กระเป๋าใหม่", "newConnection": "การเชื่อมต่อใหม่", + "no_cards_found": "ไม่พบการ์ด", "no_id_needed": "ไม่จำเป็นต้องใช้บัตรประชาชน!", "no_id_required": "ไม่จำเป็นต้องใช้บัตรประจำตัว ฝากเงินและใช้งานได้ทุกที่", "no_relay_on_domain": "ไม่มีการส่งต่อสำหรับโดเมนของผู้ใช้ หรือการส่งต่อไม่พร้อมใช้งาน กรุณาเลือกรีเลย์ที่จะใช้", @@ -452,6 +458,7 @@ "pre_seed_button_text": "ฉันเข้าใจ แสดง seed ของฉัน", "pre_seed_description": "บนหน้าถัดไปคุณจะเห็นชุดของคำ ${words} คำ นี่คือ seed ของคุณที่ไม่ซ้ำใดๆ และเป็นความลับเพียงของคุณ และนี่คือเพียงวิธีเดียวที่จะกู้กระเป๋าของคุณในกรณีที่สูญหายหรือมีปัญหา มันเป็นความรับผิดชอบของคุณเพื่อเขียนมันลงบนกระดาษและจัดเก็บไว้ในที่ปลอดภัยนอกแอป Cake Wallet", "pre_seed_title": "สำคัญ", + "prepaid_cards": "บัตรเติมเงิน", "prevent_screenshots": "ป้องกันภาพหน้าจอและการบันทึกหน้าจอ", "privacy": "ความเป็นส่วนตัว", "privacy_policy": "นโยบายความเป็นส่วนตัว", @@ -467,6 +474,7 @@ "purple_dark_theme": "ธีมสีม่วงเข้ม", "qr_fullscreen": "แตะเพื่อเปิดหน้าจอ QR code แบบเต็มจอ", "qr_payment_amount": "QR code นี้มีจำนวนการชำระเงิน คุณต้องการเขียนทับค่าปัจจุบันหรือไม่?", + "quantity": "ปริมาณ", "question_to_disable_2fa": "คุณแน่ใจหรือไม่ว่าต้องการปิดการใช้งาน Cake 2FA ไม่จำเป็นต้องใช้รหัส 2FA ในการเข้าถึงกระเป๋าเงินและฟังก์ชั่นบางอย่างอีกต่อไป", "receivable_balance": "ยอดลูกหนี้", "receive": "รับ", @@ -708,6 +716,7 @@ "tokenID": "บัตรประจำตัวประชาชน", "tor_connection": "การเชื่อมต่อทอร์", "tor_only": "Tor เท่านั้น", + "total": "ทั้งหมด", "total_saving": "ประหยัดรวม", "totp_2fa_failure": "รหัสไม่ถูกต้อง. โปรดลองใช้รหัสอื่นหรือสร้างรหัสลับใหม่ ใช้แอพ 2FA ที่เข้ากันได้ซึ่งรองรับรหัส 8 หลักและ SHA512", "totp_2fa_success": "ความสำเร็จ! Cake 2FA เปิดใช้งานสำหรับกระเป๋าเงินนี้ อย่าลืมบันทึกเมล็ดช่วยจำของคุณในกรณีที่คุณสูญเสียการเข้าถึงกระเป๋าเงิน", @@ -798,6 +807,8 @@ "use_ssl": "ใช้ SSL", "use_suggested": "ใช้ที่แนะนำ", "use_testnet": "ใช้ testnet", + "value": "ค่า", + "value_type": "ประเภทค่า", "variable_pair_not_supported": "คู่ความสัมพันธ์ที่เปลี่ยนแปลงได้นี้ไม่สนับสนุนกับหุ้นที่เลือก", "verification": "การตรวจสอบ", "verify_with_2fa": "ตรวจสอบกับ Cake 2FA", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 8c3347d6a..7506a7e2f 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -87,6 +87,7 @@ "buy": "Bilhin", "buy_alert_content": "Sa kasalukuyan ay sinusuportahan lamang namin ang pagbili ng Bitcoin, Ethereum, Litecoin, at Monero. Mangyaring lumikha o lumipat sa iyong Bitcoin, Ethereum, Litecoin, o Monero Wallet.", "buy_bitcoin": "Bumili ng bitcoin", + "buy_now": "Bumili ka na ngayon", "buy_provider_unavailable": "Kasalukuyang hindi available ang provider.", "buy_with": "Bumili ka", "by_cake_pay": "sa pamamagitan ng cake pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Cake madilim na tema", "cake_pay_account_note": "Mag -sign up na may isang email address lamang upang makita at bumili ng mga kard. Ang ilan ay magagamit kahit sa isang diskwento!", "cake_pay_learn_more": "Agad na bumili at tubusin ang mga kard ng regalo sa app!\nMag -swipe pakaliwa sa kanan upang matuto nang higit pa.", - "cake_pay_subtitle": "Bumili ng mga diskwento na gift card (USA lamang)", - "cake_pay_title": "Cake pay card card", + "cake_pay_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card", "cake_pay_web_cards_subtitle": "Bumili ng mga pandaigdigang prepaid card at gift card", "cake_pay_web_cards_title": "Cake pay web card", "cake_wallet": "Cake wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Baguhin ang kasalukuyang pitaka", "choose_account": "Pumili ng account", "choose_address": "Mangyaring piliin ang address:", + "choose_card_value": "Pumili ng isang halaga ng card", "choose_derivation": "Piliin ang derivation ng Wallet", "choose_from_available_options": "Pumili mula sa magagamit na mga pagpipilian:", "choose_one": "Pumili ng isa", @@ -166,6 +167,7 @@ "copy_address": "Kopyahin ang address", "copy_id": "Kopyahin ang id", "copyWalletConnectLink": "Kopyahin ang link ng WalletConnect mula sa dApp at i-paste dito", + "countries": "Mga bansa", "create_account": "Lumikha ng account", "create_backup": "Gumawa ng backup", "create_donation_link": "Lumikha ng link ng donasyon", @@ -178,6 +180,7 @@ "custom": "pasadya", "custom_drag": "Pasadyang (hawakan at i -drag)", "custom_redeem_amount": "Pasadyang tinubos ang halaga", + "custom_value": "Pasadyang halaga", "dark_theme": "Madilim", "debit_card": "Debit card", "debit_card_terms": "Ang pag -iimbak at paggamit ng numero ng iyong card ng pagbabayad (at mga kredensyal na naaayon sa iyong numero ng card ng pagbabayad) sa digital na pitaka na ito ay napapailalim sa mga termino at kundisyon ng naaangkop na kasunduan sa cardholder kasama ang nagbigay ng card ng pagbabayad, tulad ng sa oras -oras.", @@ -190,6 +193,7 @@ "delete_wallet": "Tanggalin ang pitaka", "delete_wallet_confirm_message": "Sigurado ka bang nais mong tanggalin ang ${wallet_name} wallet?", "deleteConnectionConfirmationPrompt": "Sigurado ka bang gusto mong tanggalin ang koneksyon sa", + "denominations": "Denominasyon", "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", @@ -277,6 +281,7 @@ "expired": "Nag -expire", "expires": "Mag -expire", "expiresOn": "Mag-e-expire sa", + "expiry_and_validity": "Pag -expire at bisa", "export_backup": "I -export ang backup", "extra_id": "Dagdag na ID:", "extracted_address_content": "Magpapadala ka ng pondo sa\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Bagong template", "new_wallet": "Bagong pitaka", "newConnection": "Bagong Koneksyon", + "no_cards_found": "Walang nahanap na mga kard", "no_id_needed": "Hindi kailangan ng ID!", "no_id_required": "Walang kinakailangang ID. I -top up at gumastos kahit saan", "no_relay_on_domain": "Walang relay para sa domain ng user o hindi available ang relay. Mangyaring pumili ng relay na gagamitin.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Naiintindihan ko. Ipakita sa akin ang aking binhi", "pre_seed_description": "Sa susunod na pahina makikita mo ang isang serye ng mga ${words} na mga salita. Ito ang iyong natatangi at pribadong binhi at ito ang tanging paraan upang mabawi ang iyong pitaka kung sakaling mawala o madepektong paggawa. Responsibilidad mong isulat ito at itago ito sa isang ligtas na lugar sa labas ng cake wallet app.", "pre_seed_title": "Mahalaga", + "prepaid_cards": "Prepaid card", "prevent_screenshots": "Maiwasan ang mga screenshot at pag -record ng screen", "privacy": "Privacy", "privacy_policy": "Patakaran sa Pagkapribado", @@ -467,6 +474,7 @@ "purple_dark_theme": "Purple Madilim na Tema", "qr_fullscreen": "Tapikin upang buksan ang buong screen QR code", "qr_payment_amount": "Ang QR code na ito ay naglalaman ng isang halaga ng pagbabayad. Nais mo bang i -overwrite ang kasalukuyang halaga?", + "quantity": "Dami", "question_to_disable_2fa": "Sigurado ka bang nais mong huwag paganahin ang cake 2fa? Ang isang 2FA code ay hindi na kinakailangan upang ma -access ang pitaka at ilang mga pag -andar.", "receivable_balance": "Natatanggap na balanse", "receive": "Tumanggap", @@ -708,6 +716,7 @@ "tokenID": "ID", "tor_connection": "Koneksyon ng Tor", "tor_only": "Tor lang", + "total": "Kabuuan", "total_saving": "Kabuuang pagtitipid", "totp_2fa_failure": "Maling code. Mangyaring subukan ang ibang code o makabuo ng isang bagong lihim na susi. Gumamit ng isang katugmang 2FA app na sumusuporta sa 8-digit na mga code at SHA512.", "totp_2fa_success": "Tagumpay! Pinagana ang cake 2FA para sa pitaka na ito. Tandaan na i -save ang iyong mnemonic seed kung sakaling mawalan ka ng pag -access sa pitaka.", @@ -798,6 +807,8 @@ "use_ssl": "Gumamit ng SSL", "use_suggested": "Gumamit ng iminungkahing", "use_testnet": "Gumamit ng testnet", + "value": "Halaga", + "value_type": "Uri ng halaga", "variable_pair_not_supported": "Ang variable na pares na ito ay hindi suportado sa mga napiling palitan", "verification": "Pag -verify", "verify_with_2fa": "Mag -verify sa cake 2FA", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index a346d874f..b03e36a1b 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -87,6 +87,7 @@ "buy": "Alış", "buy_alert_content": "Şu anda yalnızca Bitcoin, Ethereum, Litecoin ve Monero satın alımını destekliyoruz. Lütfen Bitcoin, Ethereum, Litecoin veya Monero cüzdanınızı oluşturun veya cüzdanınıza geçin.", "buy_bitcoin": "Bitcoin Satın Al", + "buy_now": "Şimdi al", "buy_provider_unavailable": "Sağlayıcı şu anda kullanılamıyor.", "buy_with": "Şunun ile al: ", "by_cake_pay": "Cake Pay tarafından", @@ -94,8 +95,7 @@ "cake_dark_theme": "Kek Koyu Tema", "cake_pay_account_note": "Kartları görmek ve satın almak için sadece bir e-posta adresiyle kaydolun. Hatta bazıları indirimli olarak bile mevcut!", "cake_pay_learn_more": "Uygulamada anında hediye kartları satın alın ve harcayın!\nDaha fazla öğrenmek için soldan sağa kaydır.", - "cake_pay_subtitle": "İndirimli hediye kartları satın alın (yalnızca ABD)", - "cake_pay_title": "Cake Pay Hediye Kartları", + "cake_pay_subtitle": "Dünya çapında ön ödemeli kartlar ve hediye kartları satın alın", "cake_pay_web_cards_subtitle": "Dünya çapında ön ödemeli kartlar ve hediye kartları satın alın", "cake_pay_web_cards_title": "Cake Pay Web Kartları", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Şimdiki cüzdanı değiştir", "choose_account": "Hesabı seç", "choose_address": "\n\nLütfen adresi seçin:", + "choose_card_value": "Bir kart değeri seçin", "choose_derivation": "Cüzdan türevini seçin", "choose_from_available_options": "Mevcut seçenekler arasından seçim yap:", "choose_one": "Birini seç", @@ -166,6 +167,7 @@ "copy_address": "Adresi kopyala", "copy_id": "ID'yi kopyala", "copyWalletConnectLink": "WalletConnect bağlantısını dApp'ten kopyalayıp buraya yapıştırın", + "countries": "Ülkeler", "create_account": "Hesap oluştur", "create_backup": "Yedek oluştur", "create_donation_link": "Bağış bağlantısı oluştur", @@ -178,6 +180,7 @@ "custom": "özel", "custom_drag": "Özel (Bekle ve Sürükle)", "custom_redeem_amount": "Özel Harcama Tutarı", + "custom_value": "Özel değer", "dark_theme": "Karanlık", "debit_card": "Ön ödemeli Kart", "debit_card_terms": "Ödeme kartı numaranızın (ve kart numaranıza karşılık gelen kimlik bilgilerinin) bu dijital cüzdanda saklanması ve kullanılması, zaman zaman yürürlükte olan ödeme kartı veren kuruluşla yapılan ilgili kart sahibi sözleşmesinin Hüküm ve Koşullarına tabidir.", @@ -190,6 +193,7 @@ "delete_wallet": "Cüzdanı sil", "delete_wallet_confirm_message": "${wallet_name} isimli cüzdanını silmek istediğinden emin misin?", "deleteConnectionConfirmationPrompt": "Bağlantıyı silmek istediğinizden emin misiniz?", + "denominations": "Mezhepler", "descending": "Azalan", "description": "Tanım", "destination_tag": "Hedef Etiketi:", @@ -277,6 +281,7 @@ "expired": "Süresi doldu", "expires": "Son kullanma tarihi", "expiresOn": "Tarihinde sona eriyor", + "expiry_and_validity": "Sona erme ve geçerlilik", "export_backup": "Yedeği dışa aktar", "extra_id": "Ekstra ID:", "extracted_address_content": "Parayı buraya gönderceksin:\n${recipient_name}", @@ -308,7 +313,7 @@ "gift_card_is_generated": "Hediye Kartı oluşturuldu", "gift_card_number": "Hediye kartı numarası", "gift_card_redeemed_note": "Harcadığın hediye kartları burada görünecek", - "gift_cards": "Hediye kartları", + "gift_cards": "Hediye Kartları", "gift_cards_unavailable": "Hediye kartları şu anda yalnızca Monero, Bitcoin ve Litecoin ile satın alınabilir", "got_it": "Tamamdır", "gross_balance": "Brüt Bakiye", @@ -384,6 +389,7 @@ "new_template": "Yeni Şablon", "new_wallet": "Yeni Cüzdan", "newConnection": "Yeni bağlantı", + "no_cards_found": "Kart bulunamadı", "no_id_needed": "Kimlik gerekmez!", "no_id_required": "Kimlik gerekmez. Para yükleyin ve istediğiniz yerde harcayın", "no_relay_on_domain": "Kullanıcının alanı için bir geçiş yok veya geçiş kullanılamıyor. Lütfen kullanmak için bir röle seçin.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Anladım. Bana tohumumu göster.", "pre_seed_description": "Bir sonraki sayfada ${words} kelime göreceksin. Bu senin benzersiz ve özel tohumundur, kaybetmen veya silinmesi durumunda cüzdanını kurtarmanın TEK YOLUDUR. Bunu yazmak ve Cake Wallet uygulaması dışında güvenli bir yerde saklamak tamamen SENİN sorumluluğunda.", "pre_seed_title": "UYARI", + "prepaid_cards": "Ön ödemeli kartlar", "prevent_screenshots": "Ekran görüntülerini ve ekran kaydını önleyin", "privacy": "Gizlilik", "privacy_policy": "Gizlilik Politikası", @@ -467,6 +474,7 @@ "purple_dark_theme": "Mor karanlık tema", "qr_fullscreen": "QR kodunu tam ekranda açmak için dokun", "qr_payment_amount": "Bu QR kodu ödeme tutarını içeriyor. Geçerli miktarın üzerine yazmak istediğine emin misin?", + "quantity": "Miktar", "question_to_disable_2fa": "Cake 2FA'yı devre dışı bırakmak istediğinizden emin misiniz? M-cüzdana ve belirli işlevlere erişmek için artık 2FA koduna gerek kalmayacak.", "receivable_balance": "Alacak bakiyesi", "receive": "Para Al", @@ -708,6 +716,7 @@ "tokenID": "İD", "tor_connection": "Tor bağlantısı", "tor_only": "Yalnızca Tor", + "total": "Toplam", "total_saving": "Toplam Tasarruf", "totp_2fa_failure": "Yanlış kod. Lütfen farklı bir kod deneyin veya yeni bir gizli anahtar oluşturun. 8 basamaklı kodları ve SHA512'yi destekleyen uyumlu bir 2FA uygulaması kullanın.", "totp_2fa_success": "Başarı! Bu cüzdan için Cake 2FA etkinleştirildi. Mnemonic seed'inizi cüzdan erişiminizi kaybetme ihtimaline karşı kaydetmeyi unutmayın.", @@ -798,6 +807,8 @@ "use_ssl": "SSL kullan", "use_suggested": "Önerileni Kullan", "use_testnet": "TestNet kullanın", + "value": "Değer", + "value_type": "Değer türü", "variable_pair_not_supported": "Bu değişken paritesi seçilen borsalarda desteklenmemekte", "verification": "Doğrulama", "verify_with_2fa": "Cake 2FA ile Doğrulayın", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 33c4ca67b..5738bd13d 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -87,6 +87,7 @@ "buy": "Купити", "buy_alert_content": "Наразі ми підтримуємо купівлю лише Bitcoin, Ethereum, Litecoin і Monero. Створіть або перейдіть на свій гаманець Bitcoin, Ethereum, Litecoin або Monero.", "buy_bitcoin": "Купити Bitcoin", + "buy_now": "Купити зараз", "buy_provider_unavailable": "В даний час постачальник недоступний.", "buy_with": "Купити за допомогою", "by_cake_pay": "від Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Темна тема торта", "cake_pay_account_note": "Зареєструйтеся, використовуючи лише адресу електронної пошти, щоб переглядати та купувати картки. Деякі навіть доступні зі знижкою!", "cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.", - "cake_pay_subtitle": "Купуйте подарункові картки зі знижкою (тільки для США)", - "cake_pay_title": "Подарункові картки Cake Pay", + "cake_pay_subtitle": "Купіть у всьому світі передплачені картки та подарункові картки", "cake_pay_web_cards_subtitle": "Купуйте передоплачені та подарункові картки по всьому світу", "cake_pay_web_cards_title": "Веб-картки Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Змінити поточний гаманець", "choose_account": "Оберіть акаунт", "choose_address": "\n\nБудь ласка, оберіть адресу:", + "choose_card_value": "Виберіть значення картки", "choose_derivation": "Виберіть деривацію гаманця", "choose_from_available_options": "Виберіть із доступних варіантів:", "choose_one": "Вибери один", @@ -166,6 +167,7 @@ "copy_address": "Cкопіювати адресу", "copy_id": "Скопіювати ID", "copyWalletConnectLink": "Скопіюйте посилання WalletConnect із dApp і вставте сюди", + "countries": "Країни", "create_account": "Створити обліковий запис", "create_backup": "Створити резервну копію", "create_donation_link": "Створити посилання для пожертв", @@ -178,6 +180,7 @@ "custom": "на замовлення", "custom_drag": "На замовлення (утримуйте та перетягується)", "custom_redeem_amount": "Власна сума викупу", + "custom_value": "Спеціальне значення", "dark_theme": "Темна", "debit_card": "Дебетова картка", "debit_card_terms": "Зберігання та використання номера вашої платіжної картки (та облікових даних, які відповідають номеру вашої платіжної картки) у цьому цифровому гаманці регулюються Умовами відповідної угоди власника картки з емітентом платіжної картки, що діє з час від часу.", @@ -190,6 +193,7 @@ "delete_wallet": "Видалити гаманець", "delete_wallet_confirm_message": "Ви впевнені, що хочете видалити гаманець ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ви впевнені, що хочете видалити з’єднання з", + "denominations": "Конфесія", "descending": "Низхідний", "description": "опис", "destination_tag": "Тег призначення:", @@ -277,6 +281,7 @@ "expired": "Закінчується", "expires": "Закінчується", "expiresOn": "Термін дії закінчується", + "expiry_and_validity": "Закінчення та обгрунтованість", "export_backup": "Експортувати резервну копію", "extra_id": "Додатковий ID:", "extracted_address_content": "Ви будете відправляти кошти\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "Новий шаблон", "new_wallet": "Новий гаманець", "newConnection": "Нове підключення", + "no_cards_found": "Карт не знайдено", "no_id_needed": "Ідентифікатор не потрібен!", "no_id_required": "Ідентифікатор не потрібен. Поповнюйте та витрачайте будь-де", "no_relay_on_domain": "Немає ретранслятора для домену користувача або ретранслятор недоступний. Будь ласка, виберіть реле для використання.", @@ -452,6 +458,7 @@ "pre_seed_button_text": "Зрозуміло. Покажіть мнемонічну фразу", "pre_seed_description": "На наступній сторінці ви побачите серію з ${words} слів. Це ваша унікальна та приватна мнемонічна фраза, і це ЄДИНИЙ спосіб відновити ваш гаманець на випадок втрати або несправності. ВАМ необхідно записати її та зберігати в безпечному місці поза програмою Cake Wallet.", "pre_seed_title": "ВАЖЛИВО", + "prepaid_cards": "Передплачені картки", "prevent_screenshots": "Запобігати знімкам екрана та запису екрана", "privacy": "Конфіденційність", "privacy_policy": "Політика конфіденційності", @@ -467,6 +474,7 @@ "purple_dark_theme": "Фіолетова темна тема", "qr_fullscreen": "Торкніться, щоб відкрити QR-код на весь екран", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "Кількість", "question_to_disable_2fa": "Ви впевнені, що хочете вимкнути Cake 2FA? Код 2FA більше не потрібен для доступу до гаманця та певних функцій.", "receivable_balance": "Баланс дебіторської заборгованості", "receive": "Отримати", @@ -709,6 +717,7 @@ "tokenID": "ID", "tor_connection": "Підключення Tor", "tor_only": "Тільки Tor", + "total": "Загальний", "total_saving": "Загальна економія", "totp_2fa_failure": "Невірний код. Спробуйте інший код або створіть новий секретний ключ. Використовуйте сумісний додаток 2FA, який підтримує 8-значні коди та SHA512.", "totp_2fa_success": "Успіх! Cake 2FA увімкнено для цього гаманця. Пам’ятайте про збереження мнемоніки на випадок, якщо ви втратите доступ до гаманця.", @@ -799,6 +808,8 @@ "use_ssl": "Використати SSL", "use_suggested": "Використати запропоноване", "use_testnet": "Використовуйте тестову мережу", + "value": "Цінність", + "value_type": "Тип значення", "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами", "verification": "Перевірка", "verify_with_2fa": "Перевірте за допомогою Cake 2FA", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 49c187426..6a971383d 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -87,6 +87,7 @@ "buy": "خریدنے", "buy_alert_content": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﭧﯿﻟﺍﻭ Monero ﺎﯾ ،Bitcoin، Ethereum، Litecoin ﺎﻨﭘﺍ ﻡ", "buy_bitcoin": "Bitcoin خریدیں۔", + "buy_now": "ابھی خریدئے", "buy_provider_unavailable": "فراہم کنندہ فی الحال دستیاب نہیں ہے۔", "buy_with": "کے ساتھ خریدیں۔", "by_cake_pay": "Cake پے کے ذریعے", @@ -94,8 +95,7 @@ "cake_dark_theme": "کیک ڈارک تھیم", "cake_pay_account_note": "کارڈز دیکھنے اور خریدنے کے لیے صرف ایک ای میل ایڈریس کے ساتھ سائن اپ کریں۔ کچھ رعایت پر بھی دستیاب ہیں!", "cake_pay_learn_more": "ایپ میں فوری طور پر گفٹ کارڈز خریدیں اور بھنائیں!\\nمزید جاننے کے لیے بائیں سے دائیں سوائپ کریں۔", - "cake_pay_subtitle": "رعایتی گفٹ کارڈز خریدیں (صرف امریکہ)", - "cake_pay_title": "Cake پے گفٹ کارڈز", + "cake_pay_subtitle": "دنیا بھر میں پری پیڈ کارڈز اور گفٹ کارڈ خریدیں", "cake_pay_web_cards_subtitle": "دنیا بھر میں پری پیڈ کارڈز اور گفٹ کارڈز خریدیں۔", "cake_pay_web_cards_title": "Cake پے ویب کارڈز", "cake_wallet": "Cake والیٹ", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "موجودہ پرس تبدیل کریں۔", "choose_account": "اکاؤنٹ کا انتخاب کریں۔", "choose_address": "\\n\\nبراہ کرم پتہ منتخب کریں:", + "choose_card_value": "کارڈ کی قیمت کا انتخاب کریں", "choose_derivation": "پرس سے ماخوذ منتخب کریں", "choose_from_available_options": "دستیاب اختیارات میں سے انتخاب کریں:", "choose_one": "ایک کا انتخاب کریں", @@ -166,6 +167,7 @@ "copy_address": "ایڈریس کاپی کریں۔", "copy_id": "کاپی ID", "copyWalletConnectLink": "dApp ﮯﺳ WalletConnect ۔ﮟﯾﺮﮐ ﭧﺴﯿﭘ ﮞﺎﮩﯾ ﺭﻭﺍ ﮟﯾﺮﮐ ﯽﭘﺎﮐ ﻮﮐ ﮏﻨﻟ", + "countries": "ممالک", "create_account": "اکاؤنٹ بنائیں", "create_backup": "بیک اپ بنائیں", "create_donation_link": "عطیہ کا لنک بنائیں", @@ -178,6 +180,7 @@ "custom": "اپنی مرضی کے مطابق", "custom_drag": "کسٹم (ہولڈ اینڈ ڈریگ)", "custom_redeem_amount": "حسب ضرورت چھڑانے کی رقم", + "custom_value": "کسٹم ویلیو", "dark_theme": "اندھیرا", "debit_card": "ڈیبٹ کارڈ", "debit_card_terms": "اس ڈیجیٹل والیٹ میں آپ کے ادائیگی کارڈ نمبر (اور آپ کے ادائیگی کارڈ نمبر سے متعلقہ اسناد) کا ذخیرہ اور استعمال ادائیگی کارڈ جاری کنندہ کے ساتھ قابل اطلاق کارڈ ہولڈر کے معاہدے کی شرائط و ضوابط کے ساتھ مشروط ہے، جیسا کہ وقتاً فوقتاً نافذ ہوتا ہے۔", @@ -190,6 +193,7 @@ "delete_wallet": "پرس کو حذف کریں۔", "delete_wallet_confirm_message": "کیا آپ واقعی ${wallet_name} والیٹ کو حذف کرنا چاہتے ہیں؟", "deleteConnectionConfirmationPrompt": "۔ﮟﯿﮨ ﮯﺘﮨﺎﭼ ﺎﻧﺮﮐ ﻑﺬﺣ ﻮﮐ ﻦﺸﮑﻨﮐ ﭖﺁ ﮧﮐ ﮯﮨ ﻦﯿﻘﯾ ﻮﮐ ﭖﺁ ﺎﯿﮐ", + "denominations": "فرق", "descending": "اترتے ہوئے", "description": "ﻞﯿﺼﻔﺗ", "destination_tag": "منزل کا ٹیگ:", @@ -277,6 +281,7 @@ "expired": "میعاد ختم", "expires": "میعاد ختم", "expiresOn": "ﺩﺎﻌﯿﻣ ﯽﻣﺎﺘﺘﺧﺍ", + "expiry_and_validity": "میعاد ختم اور صداقت", "export_backup": "بیک اپ برآمد کریں۔", "extra_id": "اضافی ID:", "extracted_address_content": "آپ فنڈز بھیج رہے ہوں گے\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "نیا سانچہ", "new_wallet": "نیا پرس", "newConnection": "ﻦﺸﮑﻨﮐ ﺎﯿﻧ", + "no_cards_found": "کوئی کارڈ نہیں ملا", "no_id_needed": "شناخت کی ضرورت نہیں!", "no_id_required": "کوئی ID درکار نہیں۔ ٹاپ اپ اور کہیں بھی خرچ کریں۔", "no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ", @@ -454,6 +460,7 @@ "pre_seed_button_text": "میں سمجھتا ہوں۔ مجھے میرا بیج دکھاؤ", "pre_seed_description": "اگلے صفحے پر آپ کو ${words} الفاظ کا ایک سلسلہ نظر آئے گا۔ یہ آپ کا انوکھا اور نجی بیج ہے اور یہ آپ کے بٹوے کو ضائع یا خرابی کی صورت میں بازیافت کرنے کا واحد طریقہ ہے۔ اسے لکھنا اور اسے کیک والیٹ ایپ سے باہر کسی محفوظ جگہ پر اسٹور کرنا آپ کی ذمہ داری ہے۔", "pre_seed_title": "اہم", + "prepaid_cards": "پری پیڈ کارڈز", "prevent_screenshots": "اسکرین شاٹس اور اسکرین ریکارڈنگ کو روکیں۔", "privacy": "رازداری", "privacy_policy": "رازداری کی پالیسی", @@ -469,6 +476,7 @@ "purple_dark_theme": "ارغوانی ڈارک تھیم", "qr_fullscreen": "فل سکرین QR کوڈ کھولنے کے لیے تھپتھپائیں۔", "qr_payment_amount": "اس QR کوڈ میں ادائیگی کی رقم شامل ہے۔ کیا آپ موجودہ قدر کو اوور رائٹ کرنا چاہتے ہیں؟", + "quantity": "مقدار", "question_to_disable_2fa": "کیا آپ واقعی کیک 2FA کو غیر فعال کرنا چاہتے ہیں؟ بٹوے اور بعض افعال تک رسائی کے لیے اب 2FA کوڈ کی ضرورت نہیں ہوگی۔", "receivable_balance": "قابل وصول توازن", "receive": "وصول کریں۔", @@ -710,6 +718,7 @@ "tokenID": "ID", "tor_connection": "ﻦﺸﮑﻨﮐ ﺭﻮﭨ", "tor_only": "صرف Tor", + "total": "کل", "total_saving": "کل بچت", "totp_2fa_failure": "غلط کوڈ. براہ کرم ایک مختلف کوڈ آزمائیں یا ایک نئی خفیہ کلید بنائیں۔ ایک ہم آہنگ 2FA ایپ استعمال کریں جو 8 ہندسوں کے کوڈز اور SHA512 کو سپورٹ کرتی ہو۔", "totp_2fa_success": "کامیابی! کیک 2FA اس بٹوے کے لیے فعال ہے۔ بٹوے تک رسائی سے محروم ہونے کی صورت میں اپنے یادداشت کے بیج کو محفوظ کرنا یاد رکھیں۔", @@ -800,6 +809,8 @@ "use_ssl": "SSL استعمال کریں۔", "use_suggested": "تجویز کردہ استعمال کریں۔", "use_testnet": "ٹیسٹ نیٹ استعمال کریں", + "value": "قدر", + "value_type": "قدر کی قسم", "variable_pair_not_supported": "یہ متغیر جوڑا منتخب ایکسچینجز کے ساتھ تعاون یافتہ نہیں ہے۔", "verification": "تصدیق", "verify_with_2fa": "کیک 2FA سے تصدیق کریں۔", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 0a5d66afe..1efc25216 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -87,6 +87,7 @@ "buy": "Rà", "buy_alert_content": "Lọwọlọwọ a ṣe atilẹyin rira Bitcoin, Ethereum, Litecoin, ati Monero. Jọwọ ṣẹda tabi yipada si Bitcoin, Ethereum, Litecoin, tabi apamọwọ Monero.", "buy_bitcoin": "Ra Bitcoin", + "buy_now": "Ra Bayibayi", "buy_provider_unavailable": "Olupese lọwọlọwọ ko si.", "buy_with": "Rà pẹ̀lú", "by_cake_pay": "láti ọwọ́ Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "Akara oyinbo dudu koko", "cake_pay_account_note": "Ẹ fi àdírẹ́sì ímeèlì nìkan forúkọ sílẹ̀ k'ẹ́ rí àti ra àwọn káàdì. Ẹ lè fi owó tó kéré jù ra àwọn káàdì kan!", "cake_pay_learn_more": "Láìpẹ́ ra àti lo àwọn káàdí ìrajà t'á lò nínú irú kan ìtajà nínú áàpù!\nẸ tẹ̀ òsì de ọ̀tún láti kọ́ jù.", - "cake_pay_subtitle": "Ra àwọn káàdì ìrajà t'á lò nínú ìtajà kan fún owó tí kò pọ̀ (USA nìkan)", - "cake_pay_title": "Àwọn káàdì ìrajà t'á lò nínú ìtajà kan ti Cake Pay", + "cake_pay_subtitle": "Ra awọn kaadi ti a san ni agbaye ati awọn kaadi ẹbun", "cake_pay_web_cards_subtitle": "Ra àwọn káàdì ìrajà t'á lò nínú ìtajà kan àti àwọn káàdì náà t'á lè lò níbikíbi", "cake_pay_web_cards_title": "Àwọn káàdì wẹ́ẹ̀bù ti Cake Pay", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "Ẹ pààrọ̀ àpamọ́wọ́ yìí", "choose_account": "Yan àkáǹtì", "choose_address": "\n\nẸ jọ̀wọ́ yan àdírẹ́sì:", + "choose_card_value": "Yan iye kaadi", "choose_derivation": "Yan awọn apamọwọ apamọwọ", "choose_from_available_options": "Ẹ yàn láti àwọn ìyàn yìí:", "choose_one": "Ẹ yàn kan", @@ -166,6 +167,7 @@ "copy_address": "Ṣẹ̀dà àdírẹ́sì", "copy_id": "Ṣẹ̀dà àmì ìdánimọ̀", "copyWalletConnectLink": "Daakọ ọna asopọ WalletConnect lati dApp ki o si lẹẹmọ nibi", + "countries": "Awọn orilẹ-ede", "create_account": "Dá àkáǹtì", "create_backup": "Ṣẹ̀dà nípamọ́", "create_donation_link": "Ṣe kọọkan alabara asopọ", @@ -178,6 +180,7 @@ "custom": "Ohun t'á ti pààrọ̀", "custom_drag": "Aṣa (mu ati fa)", "custom_redeem_amount": "Iye owó l'á máa ná", + "custom_value": "Iye aṣa", "dark_theme": "Dúdú", "debit_card": "Káàdì ìrajà", "debit_card_terms": "Òfin ti olùṣe àjọrò káàdì ìrajà bójú irú ọ̀nà t'á pamọ́ àti a lo òǹkà ti káàdì ìrajà yín (àti ọ̀rọ̀ ìdánimọ̀ tí káàdì náà) nínú àpamọ́wọ́ yìí.", @@ -190,6 +193,7 @@ "delete_wallet": "Pa àpamọ́wọ́", "delete_wallet_confirm_message": "Ṣó dá ẹ lójú pé ẹ fẹ́ pa àpamọ́wọ́ ${wallet_name}?", "deleteConnectionConfirmationPrompt": "Ṣe o da ọ loju pe o fẹ paarẹ asopọ si", + "denominations": "Awọn ede", "descending": "Sọkalẹ", "description": "Apejuwe", "destination_tag": "Orúkọ tí ìbí tó a ránṣẹ́ sí:", @@ -278,6 +282,7 @@ "expired": "Kíkú", "expires": "Ó parí", "expiresOn": "Ipari lori", + "expiry_and_validity": "Ipari ati idaniloju", "export_backup": "Sún ẹ̀dà nípamọ́ síta", "extra_id": "Àmì ìdánimọ̀ tó fikún:", "extracted_address_content": "Ẹ máa máa fi owó ránṣẹ́ sí\n${recipient_name}", @@ -309,7 +314,7 @@ "gift_card_is_generated": "A ti dá káàdí ìrajà t'á lò nínú irú kan ìtajà", "gift_card_number": "Òǹkà káàdì ìrajì", "gift_card_redeemed_note": "Àwọn káàdì ìrajà t'á lò nínú irú kan ìtajà t'ẹ́ ti lò máa fihàn ḿbí", - "gift_cards": "Àwọn káàdì ìrajà t'á lò nínú iye kan ìtajà", + "gift_cards": "Awọn kaadi ẹbun", "gift_cards_unavailable": "A lè fi Monero, Bitcoin, àti Litecoin nìkan ra káàdí ìrajà t'á lò nínú irú kan ìtajà lọ́wọ́lọ́wọ́", "got_it": "Ó dáa", "gross_balance": "Iwontunws.funfun apapọ", @@ -385,6 +390,7 @@ "new_template": "Àwòṣe títun", "new_wallet": "Àpamọ́wọ́ títun", "newConnection": "Tuntun Asopọ", + "no_cards_found": "Ko si awọn kaadi ti a rii", "no_id_needed": "Ẹ kò nílò àmì ìdánimọ̀!", "no_id_required": "Ẹ kò nílò àmì ìdánimọ̀. Ẹ lè fikún owó àti san níbikíbi", "no_relay_on_domain": "Ko si iṣipopada fun agbegbe olumulo tabi yiyi ko si. Jọwọ yan yii lati lo.", @@ -453,6 +459,7 @@ "pre_seed_button_text": "Mo ti gbọ́. O fi hóró mi hàn mi", "pre_seed_description": "Ẹ máa wo àwọn ọ̀rọ̀ ${words} lórí ojú tó ń bọ̀. Èyí ni hóró aládàáni yín tó kì í jọra. Ẹ lè fi í nìkan dá àpamọ́wọ́ yín padà sípò tí àṣìṣe tàbí ìbàjẹ́ bá ṣẹlẹ̀. Hóró yín ni ẹ gbọ́dọ̀ kọ sílẹ̀ àti pamọ́ síbí tó kò léwu níta Cake Wallet.", "pre_seed_title": "Ó TI ṢE PÀTÀKÌ", + "prepaid_cards": "Awọn kaadi ti a ti sanwo", "prevent_screenshots": "Pese asapọ ti awọn ẹrọ eto aṣa", "privacy": "Ìdáwà", "privacy_policy": "Òfin Aládàáni", @@ -468,6 +475,7 @@ "purple_dark_theme": "Akọle dudu dudu", "qr_fullscreen": "Àmì ìlujá túbọ̀ máa tóbi tí o bá tẹ̀", "qr_payment_amount": "Iye owó t'á ránṣé wà nínú àmì ìlujá yìí. Ṣé ẹ fẹ́ pààrọ̀ ẹ̀?", + "quantity": "Ọpọ", "question_to_disable_2fa": "Ṣe o wa daadaa pe o fẹ ko 2FA Cake? Ko si itumọ ti a yoo nilo lati ranse si iwe iwe naa ati eyikeyi iṣẹ ti o ni.", "receivable_balance": "Iwontunws.funfun ti o gba", "receive": "Gbà", @@ -709,6 +717,7 @@ "tokenID": "ID", "tor_connection": "Tor asopọ", "tor_only": "Tor nìkan", + "total": "Apapọ", "total_saving": "Owó t'ẹ́ ti pamọ́", "totp_2fa_failure": "Koodu ti o daju ko ri. Jọwọ jẹ koodu miiran tabi ṣiṣẹ iwe kiakia. Lo fun 2FA eto ti o ba ṣe ni jẹ 2FA ti o gba idaniloju 8-digits ati SHA512.", "totp_2fa_success": "Pelu ogo! Cake 2FA ti fi sii lori iwe iwe yii. Tọ, mọ iye ẹrọ miiran akojọrọ jẹki o kọ ipin eto.", @@ -799,6 +808,8 @@ "use_ssl": "Lo SSL", "use_suggested": "Lo àbá", "use_testnet": "Lo tele", + "value": "Iye", + "value_type": "Iru iye", "variable_pair_not_supported": "A kì í ṣe k'á fi àwọn ilé pàṣípààrọ̀ yìí ṣe pàṣípààrọ̀ irú owó méji yìí", "verification": "Ìjẹ́rìísí", "verify_with_2fa": "Ṣeẹda pẹlu Cake 2FA", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index be1234325..297f7ef28 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -87,6 +87,7 @@ "buy": "购买", "buy_alert_content": "目前我们仅支持购买比特币、以太坊、莱特币和门罗币。请创建或切换到您的比特币、以太坊、莱特币或门罗币钱包。", "buy_bitcoin": "购买比特币", + "buy_now": "立即购买", "buy_provider_unavailable": "提供者目前不可用。", "buy_with": "一起购买", "by_cake_pay": "通过 Cake Pay", @@ -94,8 +95,7 @@ "cake_dark_theme": "蛋糕黑暗主题", "cake_pay_account_note": "只需使用電子郵件地址註冊即可查看和購買卡片。有些甚至可以打折!", "cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。", - "cake_pay_subtitle": "购买打折礼品卡(仅限美国)", - "cake_pay_title": "Cake Pay 礼品卡", + "cake_pay_subtitle": "购买全球预付费卡和礼品卡", "cake_pay_web_cards_subtitle": "购买全球预付卡和礼品卡", "cake_pay_web_cards_title": "蛋糕支付网络卡", "cake_wallet": "Cake Wallet", @@ -123,6 +123,7 @@ "change_wallet_alert_title": "更换当前钱包", "choose_account": "选择账户", "choose_address": "\n\n請選擇地址:", + "choose_card_value": "选择卡值", "choose_derivation": "选择钱包推导", "choose_from_available_options": "从可用选项中选择:", "choose_one": "选一个", @@ -166,6 +167,7 @@ "copy_address": "复制地址", "copy_id": "复制ID", "copyWalletConnectLink": "从 dApp 复制 WalletConnect 链接并粘贴到此处", + "countries": "国家", "create_account": "创建账户", "create_backup": "创建备份", "create_donation_link": "创建捐赠链接", @@ -178,6 +180,7 @@ "custom": "自定义", "custom_drag": "定制(保持和拖动)", "custom_redeem_amount": "自定义兑换金额", + "custom_value": "自定义值", "dark_theme": "黑暗", "debit_card": "借记卡", "debit_card_terms": "您的支付卡号(以及与您的支付卡号对应的凭证)在此数字钱包中的存储和使用受适用的持卡人与支付卡发卡机构签订的协议的条款和条件的约束,自时不时。", @@ -190,6 +193,7 @@ "delete_wallet": "删除钱包", "delete_wallet_confirm_message": "您确定要删除 ${wallet_name} 钱包吗?", "deleteConnectionConfirmationPrompt": "您确定要删除与", + "denominations": "教派", "descending": "下降", "description": "描述", "destination_tag": "目标Tag:", @@ -277,6 +281,7 @@ "expired": "已过期", "expires": "过期", "expiresOn": "到期", + "expiry_and_validity": "到期和有效性", "export_backup": "导出备份", "extra_id": "额外ID:", "extracted_address_content": "您将汇款至\n${recipient_name}", @@ -384,6 +389,7 @@ "new_template": "新模板", "new_wallet": "新钱包", "newConnection": "新连接", + "no_cards_found": "找不到卡", "no_id_needed": "不需要 ID!", "no_id_required": "不需要身份证。充值并在任何地方消费", "no_relay_on_domain": "用户域没有中继或中继不可用。请选择要使用的继电器。", @@ -452,6 +458,7 @@ "pre_seed_button_text": "我明白。 查看种子", "pre_seed_description": "在下一页上,您将看到${words}个文字。 这是您独有的种子,是丟失或出现故障时恢复钱包的唯一方法。 您有必须将其写下并储存在Cake Wallet应用程序以外的安全地方。", "pre_seed_title": "重要", + "prepaid_cards": "预付费卡", "prevent_screenshots": "防止截屏和录屏", "privacy": "隐私", "privacy_policy": "隐私政策", @@ -467,6 +474,7 @@ "purple_dark_theme": "紫色的黑暗主题", "qr_fullscreen": "点击打开全屏二维码", "qr_payment_amount": "This QR code contains a payment amount. Do you want to overwrite the current value?", + "quantity": "数量", "question_to_disable_2fa": "您确定要禁用 Cake 2FA 吗?访问钱包和某些功能将不再需要 2FA 代码。", "receivable_balance": "应收余额", "receive": "接收", @@ -708,6 +716,7 @@ "tokenID": "ID", "tor_connection": "Tor连接", "tor_only": "仅限 Tor", + "total": "全部的", "total_saving": "总储蓄", "totp_2fa_failure": "不正确的代码。 请尝试不同的代码或生成新的密钥。 使用支持 8 位代码和 SHA512 的兼容 2FA 应用程序。", "totp_2fa_success": "成功!为此钱包启用了 Cake 2FA。请记住保存您的助记词种子,以防您无法访问钱包。", @@ -798,6 +807,8 @@ "use_ssl": "使用SSL", "use_suggested": "使用建议", "use_testnet": "使用TestNet", + "value": "价值", + "value_type": "值类型", "variable_pair_not_supported": "所选交易所不支持此变量对", "verification": "验证", "verify_with_2fa": "用 Cake 2FA 验证", diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 27b7efa39..3fa9ef921 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -6,7 +6,7 @@ if [ -z "$APP_ANDROID_TYPE" ]; then fi cd ../.. -sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml +sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml sed -i "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml sed -i "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml sed -i "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml From 1dd2c7da56f8e63958188d4b59b5018bf05a1346 Mon Sep 17 00:00:00 2001 From: Rafael Date: Mon, 10 Jun 2024 04:22:57 -0300 Subject: [PATCH 05/20] Sp fixes (#1487) * feat: missing desktop setting menu * fix: sp utxo pending * fix: change to electrs only scanning, initial migration, and btc-electrum as null ssl --- cw_bitcoin/lib/electrum.dart | 2 +- cw_bitcoin/lib/electrum_wallet.dart | 33 +++++++++++++----- cw_core/lib/get_height_by_date.dart | 1 + lib/bitcoin/cw_bitcoin.dart | 24 +++----------- lib/entities/default_settings_migration.dart | 35 ++++++++++++++++++++ lib/main.dart | 2 +- 6 files changed, 67 insertions(+), 30 deletions(-) diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index afd5e2440..b52015794 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -64,7 +64,7 @@ class ElectrumClient { await socket?.close(); } catch (_) {} - if (useSSL == false) { + if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) { socket = await Socket.connect(host, port, timeout: connectionTimeout); } else { socket = await SecureSocket.connect(host, port, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 43bae2e19..64aa81722 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -197,7 +197,7 @@ abstract class ElectrumWalletBase bool silentPaymentsScanningActive = false; @action - Future setSilentPaymentsScanning(bool active) async { + Future setSilentPaymentsScanning(bool active, bool usingElectrs) async { silentPaymentsScanningActive = active; if (active) { @@ -210,7 +210,11 @@ abstract class ElectrumWalletBase } if (tip > walletInfo.restoreHeight) { - _setListeners(walletInfo.restoreHeight, chainTipParam: _currentChainTip); + _setListeners( + walletInfo.restoreHeight, + chainTipParam: _currentChainTip, + usingElectrs: usingElectrs, + ); } } else { alwaysScan = false; @@ -277,7 +281,12 @@ abstract class ElectrumWalletBase } @action - Future _setListeners(int height, {int? chainTipParam, bool? doSingleScan}) async { + Future _setListeners( + int height, { + int? chainTipParam, + bool? doSingleScan, + bool? usingElectrs, + }) async { final chainTip = chainTipParam ?? await getUpdatedChainTip(); if (chainTip == height) { @@ -303,7 +312,7 @@ abstract class ElectrumWalletBase chainTip: chainTip, electrumClient: ElectrumClient(), transactionHistoryIds: transactionHistory.transactions.keys.toList(), - node: ScanNode(node!.uri, node!.useSSL), + node: usingElectrs == true ? ScanNode(node!.uri, node!.useSSL) : null, labels: walletAddresses.labels, labelIndexes: walletAddresses.silentAddresses .where((addr) => addr.type == SilentPaymentsAddresType.p2sp && addr.index >= 1) @@ -1122,8 +1131,13 @@ abstract class ElectrumWalletBase @action @override - Future rescan( - {required int height, int? chainTip, ScanData? scanData, bool? doSingleScan}) async { + Future rescan({ + required int height, + int? chainTip, + ScanData? scanData, + bool? doSingleScan, + bool? usingElectrs, + }) async { silentPaymentsScanningActive = true; _setListeners(height, doSingleScan: doSingleScan); } @@ -1820,7 +1834,7 @@ class ScanData { final SendPort sendPort; final SilentPaymentOwner silentAddress; final int height; - final ScanNode node; + final ScanNode? node; final BasedUtxoNetwork network; final int chainTip; final ElectrumClient electrumClient; @@ -1881,7 +1895,10 @@ Future startRefresh(ScanData scanData) async { scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus)); final electrumClient = scanData.electrumClient; - await electrumClient.connectToUri(scanData.node.uri, useSSL: scanData.node.useSSL); + await electrumClient.connectToUri( + scanData.node?.uri ?? Uri.parse("tcp://electrs.cakewallet.com:50001"), + useSSL: scanData.node?.useSSL ?? false, + ); if (tweaksSubscription == null) { final count = scanData.isSingleScan ? 1 : TWEAKS_COUNT; diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index a3dd51b68..d62a78468 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -245,6 +245,7 @@ Future getHavenCurrentHeight() async { // Data taken from https://timechaincalendar.com/ const bitcoinDates = { + "2024-06": 846005, "2024-05": 841590, "2024-04": 837182, "2024-03": 832623, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 634919952..bdefe2ea9 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -514,18 +514,10 @@ class CWBitcoin extends Bitcoin { @override Future setScanningActive(Object wallet, bool active) async { final bitcoinWallet = wallet as ElectrumWallet; - - if (active && !(await getNodeIsElectrsSPEnabled(wallet))) { - final node = Node( - useSSL: false, - uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}', - ); - node.type = WalletType.bitcoin; - - await bitcoinWallet.connectToNode(node: node); - } - - bitcoinWallet.setSilentPaymentsScanning(active); + bitcoinWallet.setSilentPaymentsScanning( + active, + active && (await getNodeIsElectrsSPEnabled(wallet)), + ); } @override @@ -540,14 +532,6 @@ class CWBitcoin extends Bitcoin { @override Future rescan(Object wallet, {required int height, bool? doSingleScan}) async { final bitcoinWallet = wallet as ElectrumWallet; - if (!(await getNodeIsElectrsSPEnabled(wallet))) { - final node = Node( - useSSL: false, - uri: 'electrs.cakewallet.com:${(wallet.network == BitcoinNetwork.testnet ? 50002 : 50001)}', - ); - node.type = WalletType.bitcoin; - await bitcoinWallet.connectToNode(node: node); - } bitcoinWallet.rescan(height: height, doSingleScan: doSingleScan); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 697685767..806285a81 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -227,6 +227,8 @@ Future defaultSettingsMigration( break; case 34: await _addElectRsNode(nodes, sharedPreferences); + case 35: + await _switchElectRsNode(nodes, sharedPreferences); break; default: break; @@ -825,6 +827,39 @@ Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPrefe } } +Future _switchElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { + final currentBitcoinNodeId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final currentBitcoinNode = + nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); + final needToReplaceCurrentBitcoinNode = + currentBitcoinNode.uri.toString().contains('electrs.cakewallet.com'); + + if (!needToReplaceCurrentBitcoinNode) return; + + final btcElectrumNode = nodeSource.values.firstWhereOrNull( + (node) => node.uri.toString().contains('btc-electrum.cakewallet.com'), + ); + + if (btcElectrumNode == null) { + final newBtcElectrumBitcoinNode = Node( + uri: newCakeWalletBitcoinUri, + type: WalletType.bitcoin, + useSSL: false, + ); + await nodeSource.add(newBtcElectrumBitcoinNode); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + newBtcElectrumBitcoinNode.key as int, + ); + } else { + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + btcElectrumNode.key as int, + ); + } +} + Future checkCurrentNodes( Box nodeSource, Box powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); diff --git a/lib/main.dart b/lib/main.dart index 776c2aa69..09f0cf432 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 34, + initialMigrationVersion: 35, ); } From 5a6502a35a27b9b611f24e3cc0d86a7953bceb88 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 10 Jun 2024 09:30:58 +0200 Subject: [PATCH 06/20] SP Enhancments (#1483) * fixes and minor enhancements for SP flow * fix build * change dfx text * minor fixes * pass use electrs to setListeners * comment out connecting on failure for now --- cw_bitcoin/lib/electrum_wallet.dart | 40 +++++++++++++++++------------ lib/bitcoin/cw_bitcoin.dart | 14 +++++++--- lib/src/screens/root/root.dart | 8 +++--- lib/utils/exception_handler.dart | 40 +++++++++++++++-------------- res/values/strings_ar.arb | 2 +- res/values/strings_bg.arb | 2 +- res/values/strings_cs.arb | 2 +- res/values/strings_de.arb | 2 +- res/values/strings_en.arb | 2 +- res/values/strings_es.arb | 2 +- res/values/strings_fr.arb | 2 +- res/values/strings_ha.arb | 2 +- res/values/strings_hi.arb | 2 +- res/values/strings_hr.arb | 2 +- res/values/strings_id.arb | 2 +- res/values/strings_it.arb | 2 +- res/values/strings_ja.arb | 2 +- res/values/strings_ko.arb | 2 +- res/values/strings_my.arb | 2 +- res/values/strings_nl.arb | 2 +- res/values/strings_pl.arb | 2 +- res/values/strings_pt.arb | 2 +- res/values/strings_ru.arb | 2 +- res/values/strings_th.arb | 2 +- res/values/strings_tl.arb | 2 +- res/values/strings_tr.arb | 2 +- res/values/strings_uk.arb | 2 +- res/values/strings_ur.arb | 2 +- res/values/strings_yo.arb | 2 +- res/values/strings_zh.arb | 2 +- 30 files changed, 86 insertions(+), 68 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 64aa81722..d3736e076 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -96,13 +96,17 @@ abstract class ElectrumWalletBase this.walletInfo = walletInfo; transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); - reaction((_) => syncStatus, (SyncStatus syncStatus) { - if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) + reaction((_) => syncStatus, (SyncStatus syncStatus) async { + if (syncStatus is! AttemptingSyncStatus && syncStatus is! SyncedTipSyncStatus) { silentPaymentsScanningActive = syncStatus is SyncingSyncStatus; + } if (syncStatus is NotConnectedSyncStatus) { // Needs to re-subscribe to all scripthashes when reconnected _scripthashesUpdateSubject = {}; + + // TODO: double check this and make sure it doesn't cause any un-necessary calls + // await this.electrumClient.connectToUri(node!.uri, useSSL: node!.useSSL); } // Message is shown on the UI for 3 seconds, revert to synced @@ -219,13 +223,13 @@ abstract class ElectrumWalletBase } else { alwaysScan = false; - (await _isolate)?.kill(priority: Isolate.immediate); + _isolate?.then((value) => value.kill(priority: Isolate.immediate)); if (electrumClient.isConnected) { syncStatus = SyncedSyncStatus(); } else { if (electrumClient.uri != null) { - await electrumClient.connectToUri(electrumClient.uri!); + await electrumClient.connectToUri(electrumClient.uri!, useSSL: electrumClient.useSSL); startSync(); } } @@ -463,17 +467,7 @@ abstract class ElectrumWalletBase await electrumClient.close(); - electrumClient.onConnectionStatusChange = (bool? isConnected) async { - if (syncStatus is SyncingSyncStatus) return; - - if (isConnected == true && syncStatus is! SyncedSyncStatus) { - syncStatus = ConnectedSyncStatus(); - } else if (isConnected == false) { - syncStatus = LostConnectionSyncStatus(); - } else if (!(isConnected ?? false) && syncStatus is! ConnectingSyncStatus) { - syncStatus = NotConnectedSyncStatus(); - } - }; + electrumClient.onConnectionStatusChange = _onConnectionStatusChange; await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); } catch (e) { @@ -1139,7 +1133,7 @@ abstract class ElectrumWalletBase bool? usingElectrs, }) async { silentPaymentsScanningActive = true; - _setListeners(height, doSingleScan: doSingleScan); + _setListeners(height, doSingleScan: doSingleScan, usingElectrs: usingElectrs); } @override @@ -1657,6 +1651,7 @@ abstract class ElectrumWalletBase if (_isTransactionUpdating) { return; } + await getCurrentChainTip(); transactionHistory.transactions.values.forEach((tx) async { if (tx.unspents != null && tx.unspents!.isNotEmpty && tx.height > 0) { @@ -1821,6 +1816,19 @@ abstract class ElectrumWalletBase static String _hardenedDerivationPath(String derivationPath) => derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); + + @action + void _onConnectionStatusChange(bool? isConnected) { + if (syncStatus is SyncingSyncStatus) return; + + if (isConnected == true && syncStatus is! SyncedSyncStatus) { + syncStatus = ConnectedSyncStatus(); + } else if (isConnected == false) { + syncStatus = LostConnectionSyncStatus(); + } else if (isConnected != true && syncStatus is! ConnectingSyncStatus) { + syncStatus = NotConnectedSyncStatus(); + } + } } class ScanNode { diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index bdefe2ea9..15d2ffadf 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -306,7 +306,7 @@ class CWBitcoin extends Bitcoin { } final electrumClient = ElectrumClient(); - await electrumClient.connectToUri(node.uri); + await electrumClient.connectToUri(node.uri, useSSL: node.useSSL); late BasedUtxoNetwork network; btc.NetworkType networkType; @@ -560,10 +560,16 @@ class CWBitcoin extends Bitcoin { } final bitcoinWallet = wallet as ElectrumWallet; - final tweaksResponse = await bitcoinWallet.electrumClient.getTweaks(height: 0); + try { + final tweaksResponse = await bitcoinWallet.electrumClient.getTweaks(height: 0); - if (tweaksResponse != null) { - return true; + if (tweaksResponse != null) { + return true; + } + } on RequestFailedTimeoutException { + return false; + } catch (_) { + rethrow; } return false; diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index b6406dfbd..7ad8af4c5 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -136,9 +136,11 @@ class RootState extends State with WidgetsBindingObserver { break; case AppLifecycleState.resumed: widget.authService.requireAuth().then((value) { - setState(() { - _requestAuth = value; - }); + if (mounted) { + setState(() { + _requestAuth = value; + }); + } }); break; default: diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 6e93fc5cd..5a07ab0f0 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -118,25 +118,27 @@ class ExceptionHandler { WidgetsBinding.instance.addPostFrameCallback( (timeStamp) async { - await showPopUp( - context: navigatorKey.currentContext!, - builder: (context) { - return AlertWithTwoActions( - isDividerExist: true, - alertTitle: S.of(context).error, - alertContent: S.of(context).error_dialog_content, - rightButtonText: S.of(context).send, - leftButtonText: S.of(context).do_not_send, - actionRightButton: () { - Navigator.of(context).pop(); - _sendExceptionFile(); - }, - actionLeftButton: () { - Navigator.of(context).pop(); - }, - ); - }, - ); + if (navigatorKey.currentContext != null) { + await showPopUp( + context: navigatorKey.currentContext!, + builder: (context) { + return AlertWithTwoActions( + isDividerExist: true, + alertTitle: S.of(context).error, + alertContent: S.of(context).error_dialog_content, + rightButtonText: S.of(context).send, + leftButtonText: S.of(context).do_not_send, + actionRightButton: () { + Navigator.of(context).pop(); + _sendExceptionFile(); + }, + actionLeftButton: () { + Navigator.of(context).pop(); + }, + ); + }, + ); + } _hasError = false; }, diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 25d392d6a..fa2747230 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -197,7 +197,7 @@ "descending": "النزول", "description": "ﻒﺻﻭ", "destination_tag": "علامة الوجهة:", - "dfx_option_description": "ﺎﺑﻭﺭﻭﺃ ﻲﻓ ﺕﺎﻛﺮﺸﻟﺍﻭ ﺔﺋﺰﺠﺘﻟﺍ ءﻼﻤﻌﻟ .ﻲﻓﺎﺿﺇ KYC ﻥﻭﺪﺑ ﻭﺭﻮﻳ 990 ﻰﻟﺇ ﻞﺼﻳ ﺎﻣ .ﻱﺮﺴﻳﻮﺴﻟﺍ", + "dfx_option_description": "شراء التشفير مع EUR & CHF. لعملاء البيع بالتجزئة والشركات في أوروبا", "didnt_get_code": "لم تحصل على رمز؟", "digit_pin": "-رقم PIN", "digital_and_physical_card": " بطاقة ائتمان رقمية ومادية مسبقة الدفع", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 8f4717081..430a26134 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -197,7 +197,7 @@ "descending": "Низходящ", "description": "Описание", "destination_tag": "Destination tag:", - "dfx_option_description": "Купете крипто с EUR и CHF. До 990 € без допълнителен KYC. За клиенти на дребно и корпоративни клиенти в Европа", + "dfx_option_description": "Купете криптовалута с Eur & CHF. За търговски и корпоративни клиенти в Европа", "didnt_get_code": "Не получихте код?", "digit_pin": "-цифрен PIN", "digital_and_physical_card": " дигитална или физическа предплатена дебитна карта", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 297a737a2..6a92cb2e5 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -197,7 +197,7 @@ "descending": "Klesající", "description": "Popis", "destination_tag": "Destination Tag:", - "dfx_option_description": "Nakupujte kryptoměny za EUR a CHF. Až 990 € bez dalších KYC. Pro maloobchodní a firemní zákazníky v Evropě", + "dfx_option_description": "Koupit krypto s EUR & CHF. Pro maloobchodní a firemní zákazníky v Evropě", "didnt_get_code": "Nepřišel Vám kód?", "digit_pin": "-číselný PIN", "digital_and_physical_card": " digitální a fyzické předplacené debetní karty,", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2ccd1919c..2e4203edd 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -197,7 +197,7 @@ "descending": "Absteigend", "description": "Beschreibung", "destination_tag": "Ziel-Tag:", - "dfx_option_description": "Krypto mit EUR und CHF kaufen. Bis zu 990€ ohne zusätzliches KYC. Für Privat- und Firmenkunden in Europa", + "dfx_option_description": "Kaufen Sie Krypto mit EUR & CHF. Für Einzelhandel und Unternehmenskunden in Europa", "didnt_get_code": "Kein Code?", "digit_pin": "-stellige PIN", "digital_and_physical_card": "digitale und physische Prepaid-Debitkarte", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 9b8662bdb..1b3da32ab 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -197,7 +197,7 @@ "descending": "Descending", "description": "Description", "destination_tag": "Destination tag:", - "dfx_option_description": "Buy crypto with EUR & CHF. Up to 990€ without additional KYC. For retail and corporate customers in Europe", + "dfx_option_description": "Buy crypto with EUR & CHF. For retail and corporate customers in Europe", "didnt_get_code": "Didn't get code?", "digit_pin": "-digit PIN", "digital_and_physical_card": " digital and physical prepaid debit card", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 45da02030..215cece6e 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -197,7 +197,7 @@ "descending": "Descendente", "description": "Descripción", "destination_tag": "Etiqueta de destino:", - "dfx_option_description": "Compre criptomonedas con EUR y CHF. Hasta 990€ sin KYC adicional. Para clientes minoristas y corporativos en Europa", + "dfx_option_description": "Compre criptografía con EUR y CHF. Para clientes minoristas y corporativos en Europa", "didnt_get_code": "¿No recibiste el código?", "digit_pin": "-dígito PIN", "digital_and_physical_card": " tarjeta de débito prepago digital y física", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 119cb24e4..997dbc38c 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -197,7 +197,7 @@ "descending": "Descendant", "description": "Description", "destination_tag": "Tag de destination :", - "dfx_option_description": "Achetez des crypto-monnaies avec EUR et CHF. Jusqu'à 990€ sans KYC supplémentaire. Pour les clients particuliers et entreprises en Europe", + "dfx_option_description": "Achetez de la crypto avec EUR & CHF. Pour les clients de la vente au détail et des entreprises en Europe", "didnt_get_code": "Vous n'avez pas reçu le code ?", "digit_pin": " chiffres", "digital_and_physical_card": "carte de débit prépayée numérique et physique", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index dd3350131..478bc458d 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -197,7 +197,7 @@ "descending": "Saukowa", "description": "Bayani", "destination_tag": "Tambarin makoma:", - "dfx_option_description": "Sayi crypto tare da EUR & CHF. Har zuwa € 990 ba tare da ƙarin KYC ba. Don 'yan kasuwa da abokan ciniki na kamfanoni a Turai", + "dfx_option_description": "Buy crypto tare da Eur & Chf. Don Retail da abokan ciniki na kamfanoni a Turai", "didnt_get_code": "Ba a samun code?", "digit_pin": "-lambar PIN", "digital_and_physical_card": "katin zare kudi na dijital da na zahiri", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 55aead5a3..4f3a77fdb 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -197,7 +197,7 @@ "descending": "अवरोही", "description": "विवरण", "destination_tag": "गंतव्य टैग:", - "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। अतिरिक्त केवाईसी के बिना 990€ तक। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए", + "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए", "didnt_get_code": "कोड नहीं मिला?", "digit_pin": "-अंक पिन", "digital_and_physical_card": "डिजिटल और भौतिक प्रीपेड डेबिट कार्ड", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index c999b7dfd..faef9cd78 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -197,7 +197,7 @@ "descending": "Silazni", "description": "Opis", "destination_tag": "Odredišna oznaka:", - "dfx_option_description": "Kupujte kripto s EUR i CHF. Do 990 € bez dodatnog KYC-a. Za maloprodajne i poslovne korisnike u Europi", + "dfx_option_description": "Kupite kriptovalute s Eur & CHF. Za maloprodajne i korporativne kupce u Europi", "didnt_get_code": "Ne dobivate kod?", "digit_pin": "-znamenkasti PIN", "digital_and_physical_card": "digitalna i fizička unaprijed plaćena debitna kartica", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 24208718c..e718f24da 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -197,7 +197,7 @@ "descending": "Menurun", "description": "Keterangan", "destination_tag": "Tag tujuan:", - "dfx_option_description": "Beli kripto dengan EUR & CHF. Hingga 990€ tanpa KYC tambahan. Untuk pelanggan ritel dan korporat di Eropa", + "dfx_option_description": "Beli crypto dengan EUR & CHF. Untuk pelanggan ritel dan perusahaan di Eropa", "didnt_get_code": "Tidak mendapatkan kode?", "digit_pin": "-digit PIN", "digital_and_physical_card": " kartu debit pra-bayar digital dan fisik", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 5509c0067..6a25ad9e0 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -198,7 +198,7 @@ "descending": "Discendente", "description": "Descrizione", "destination_tag": "Tag destinazione:", - "dfx_option_description": "Acquista criptovalute con EUR e CHF. Fino a 990€ senza KYC aggiuntivi. Per clienti al dettaglio e aziendali in Europa", + "dfx_option_description": "Acquista Crypto con EUR & CHF. Per i clienti al dettaglio e aziendali in Europa", "didnt_get_code": "Non ricevi il codice?", "digit_pin": "-cifre PIN", "digital_and_physical_card": "carta di debito prepagata digitale e fisica", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 9204ffdf4..56786cabc 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -197,7 +197,7 @@ "descending": "下降", "description": "説明", "destination_tag": "宛先タグ:", - "dfx_option_description": "EUR と CHF で暗号通貨を購入します。追加のKYCなしで最大990ユーロ。ヨーロッパの小売および法人顧客向け", + "dfx_option_description": "EUR&CHFで暗号を購入します。ヨーロッパの小売および企業の顧客向け", "didnt_get_code": "コードを取得しませんか?", "digit_pin": "桁ピン", "digital_and_physical_card": "デジタルおよび物理プリペイドデビットカード", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 99d138de1..5de972a6e 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -197,7 +197,7 @@ "descending": "내림차순", "description": "설명", "destination_tag": "목적지 태그:", - "dfx_option_description": "EUR 및 CHF로 암호화폐를 구매하세요. 추가 KYC 없이 최대 990€. 유럽의 소매 및 기업 고객용", + "dfx_option_description": "EUR & CHF로 암호화를 구입하십시오. 유럽의 소매 및 기업 고객을 위해", "didnt_get_code": "코드를 받지 못하셨습니까?", "digit_pin": "숫자 PIN", "digital_and_physical_card": " 디지털 및 실제 선불 직불 카드", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 41b082457..497e1bb0c 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -197,7 +197,7 @@ "descending": "ဆင်း", "description": "ဖော်ပြချက်", "destination_tag": "ခရီးဆုံးအမှတ်-", - "dfx_option_description": "EUR & CHF ဖြင့် crypto ကိုဝယ်ပါ။ အပို KYC မပါဘဲ 990€ အထိ။ ဥရောပရှိ လက်လီရောင်းချသူများနှင့် ကော်ပိုရိတ်ဖောက်သည်များအတွက်", + "dfx_option_description": "Crypto ကို EUR & CHF ဖြင့် 0 ယ်ပါ။ လက်လီရောင်းဝယ်မှုနှင့်ဥရောပရှိကော်ပိုရိတ်ဖောက်သည်များအတွက်", "didnt_get_code": "ကုဒ်ကို မရဘူးလား?", "digit_pin": "-ဂဏန်း PIN", "digital_and_physical_card": " ဒစ်ဂျစ်တယ်နှင့် ရုပ်ပိုင်းဆိုင်ရာ ကြိုတင်ငွေပေးချေသော ဒက်ဘစ်ကတ်", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index a7a3a20a5..1bc1f8cd7 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -197,7 +197,7 @@ "descending": "Aflopend", "description": "Beschrijving", "destination_tag": "Bestemmingstag:", - "dfx_option_description": "Koop crypto met EUR & CHF. Tot 990€ zonder extra KYC. Voor particuliere en zakelijke klanten in Europa", + "dfx_option_description": "Koop crypto met EUR & CHF. Voor retail- en zakelijke klanten in Europa", "didnt_get_code": "Geen code?", "digit_pin": "-cijferige PIN", "digital_and_physical_card": "digitale en fysieke prepaid debetkaart", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index e3cea5cad..ff8826c1e 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -197,7 +197,7 @@ "descending": "Schodzenie", "description": "Opis", "destination_tag": "Tag docelowy:", - "dfx_option_description": "Kupuj kryptowaluty za EUR i CHF. Do 990 € bez dodatkowego KYC. Dla klientów detalicznych i korporacyjnych w Europie", + "dfx_option_description": "Kup krypto z EUR & CHF. Dla klientów detalicznych i korporacyjnych w Europie", "didnt_get_code": "Nie dostałeś kodu?", "digit_pin": "-znakowy PIN", "digital_and_physical_card": " cyfrowa i fizyczna przedpłacona karta debetowa", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 45241e5a0..67ce2980a 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -197,7 +197,7 @@ "descending": "descendente", "description": "Descrição", "destination_tag": "Tag de destino:", - "dfx_option_description": "Compre criptografia com EUR e CHF. Até 990€ sem KYC adicional. Para clientes de varejo e corporativos na Europa", + "dfx_option_description": "Compre criptografia com EUR & CHF. Para clientes de varejo e corporativo na Europa", "didnt_get_code": "Não recebeu o código?", "digit_pin": "dígitos", "digital_and_physical_card": "cartão de débito pré-pago digital e físico", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 281673326..673f1472d 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -197,7 +197,7 @@ "descending": "Нисходящий", "description": "Описание", "destination_tag": "Целевой тег:", - "dfx_option_description": "Покупайте криптовалюту за EUR и CHF. До 990€ без дополнительного KYC. Для розничных и корпоративных клиентов в Европе", + "dfx_option_description": "Купить крипто с Eur & CHF. Для розничных и корпоративных клиентов в Европе", "didnt_get_code": "Не получить код?", "digit_pin": "-значный PIN", "digital_and_physical_card": "цифровая и физическая предоплаченная дебетовая карта", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 98dfed3ea..1779b9da1 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -197,7 +197,7 @@ "descending": "ลงมา", "description": "คำอธิบาย", "destination_tag": "แท็กปลายทาง:", - "dfx_option_description": "ซื้อ crypto ด้วย EUR และ CHF สูงถึง 990€ โดยไม่มี KYC เพิ่มเติม สำหรับลูกค้ารายย่อยและลูกค้าองค์กรในยุโรป", + "dfx_option_description": "ซื้อ crypto ด้วย Eur & CHF สำหรับลูกค้ารายย่อยและลูกค้าในยุโรป", "didnt_get_code": "ไม่ได้รับรหัส?", "digit_pin": "-หลัก PIN", "digital_and_physical_card": "บัตรเดบิตดิจิตอลและบัตรพื้นฐาน", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7506a7e2f..e358d6daf 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -197,7 +197,7 @@ "descending": "Pababang", "description": "Paglalarawan", "destination_tag": "Tag ng patutunguhan:", - "dfx_option_description": "Bumili ng crypto gamit ang EUR at CHF. Hanggang 990€ nang walang karagdagang KYC. Para sa retail at corporate na mga customer sa Europe", + "dfx_option_description": "Bumili ng crypto kasama ang EUR & CHF. Para sa mga customer at corporate customer sa Europa", "didnt_get_code": "Hindi nakuha ang code?", "digit_pin": "-digit pin", "digital_and_physical_card": "Digital at Physical Prepaid Debit Card", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index b03e36a1b..76bd79d8a 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -197,7 +197,7 @@ "descending": "Azalan", "description": "Tanım", "destination_tag": "Hedef Etiketi:", - "dfx_option_description": "EUR ve CHF ile kripto satın alın. Ek KYC olmadan 990 €'ya kadar. Avrupa'daki perakende ve kurumsal müşteriler için", + "dfx_option_description": "Eur & chf ile kripto satın alın. Avrupa'daki perakende ve kurumsal müşteriler için", "didnt_get_code": "Kod gelmedi mi?", "digit_pin": " haneli PIN", "digital_and_physical_card": " Dijital para birimleri ile para yükleyebileceğiniz ve ek bilgiye gerek olmayan", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 5738bd13d..e1d470e1e 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -197,7 +197,7 @@ "descending": "Низхідний", "description": "опис", "destination_tag": "Тег призначення:", - "dfx_option_description": "Купуйте криптовалюту за EUR і CHF. До 990 євро без додаткового KYC. Для роздрібних і корпоративних клієнтів у Європі", + "dfx_option_description": "Купуйте криптовалюту з EUR & CHF. Для роздрібних та корпоративних клієнтів у Європі", "didnt_get_code": "Не отримуєте код?", "digit_pin": "-значний PIN", "digital_and_physical_card": " цифрова та фізична передплачена дебетова картка", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 6a971383d..02c5216ef 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -197,7 +197,7 @@ "descending": "اترتے ہوئے", "description": "ﻞﯿﺼﻔﺗ", "destination_tag": "منزل کا ٹیگ:", - "dfx_option_description": "EUR ﺭﻭﺍ CHF ﯽﻓﺎﺿﺍ ۔ﮟﯾﺪﯾﺮﺧ ﻮﭩﭘﺮﮐ ﮫﺗﺎﺳ ﮯﮐ KYC ﮯﯿﻟ ﮯﮐ ﻦﯿﻓﺭﺎﺻ ﭧﯾﺭﻮﭘﺭﺎﮐ ﺭﻭﺍ ﮦﺩﺭﻮﺧ ﮟ", + "dfx_option_description": "یورو اور سی ایچ ایف کے ساتھ کرپٹو خریدیں۔ یورپ میں خوردہ اور کارپوریٹ صارفین کے لئے", "didnt_get_code": "کوڈ نہیں ملتا؟", "digit_pin": "-ہندسوں کا پن", "digital_and_physical_card": " ڈیجیٹل اور فزیکل پری پیڈ ڈیبٹ کارڈ", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 1efc25216..221b189e1 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -197,7 +197,7 @@ "descending": "Sọkalẹ", "description": "Apejuwe", "destination_tag": "Orúkọ tí ìbí tó a ránṣẹ́ sí:", - "dfx_option_description": "Ra crypto pẹlu EUR & CHF. Titi di 990 € laisi afikun KYC. Fun soobu ati awọn onibara ile-iṣẹ ni Yuroopu", + "dfx_option_description": "Ra Crypto pẹlu EUR & CHF. Fun soobu ati awọn alabara ile-iṣẹ ni Yuroopu", "didnt_get_code": "Ko gba koodu?", "digit_pin": "-díjíìtì òǹkà ìdánimọ̀ àdáni", "digital_and_physical_card": " káàdì ìrajà t'ara àti ti ayélujára", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 297f7ef28..d98801cb2 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -197,7 +197,7 @@ "descending": "下降", "description": "描述", "destination_tag": "目标Tag:", - "dfx_option_description": "用欧元和瑞士法郎购买加密货币。高达 990 欧元,无需额外 KYC。对于欧洲的零售和企业客户", + "dfx_option_description": "用Eur&Chf购买加密货币。对于欧洲的零售和企业客户", "didnt_get_code": "没有获取代码?", "digit_pin": "位 PIN", "digital_and_physical_card": "数字和物理预付借记卡", From 245ac5ae3cab6c53430db49b826424c27bb1f481 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Mon, 10 Jun 2024 17:24:52 +0200 Subject: [PATCH 07/20] Switch tron default node (#1488) * fixes and minor enhancements for SP flow * fix build * change dfx text * minor fixes * pass use electrs to setListeners * comment out connecting on failure for now * Switch tron default node --- assets/tron_node_list.yml | 4 ++-- lib/entities/default_settings_migration.dart | 5 ++++- lib/main.dart | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index d28e38f2e..b12a82dbe 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -1,8 +1,8 @@ - uri: tron-rpc.publicnode.com:443 - is_default: true + is_default: false useSSL: true - uri: api.trongrid.io - is_default: false + is_default: true useSSL: true \ No newline at end of file diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 806285a81..dcde1d3ce 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -37,7 +37,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; -const tronDefaultNodeUri = 'tron-rpc.publicnode.com:443'; +const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; Future defaultSettingsMigration( @@ -230,6 +230,9 @@ Future defaultSettingsMigration( case 35: await _switchElectRsNode(nodes, sharedPreferences); break; + case 36: + await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; default: break; } diff --git a/lib/main.dart b/lib/main.dart index 09f0cf432..5bcd9ffdb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 35, + initialMigrationVersion: 36, ); } From ffa0b416e98e6c9ab618c484642a444e8931c1d7 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Tue, 11 Jun 2024 00:54:46 +0200 Subject: [PATCH 08/20] v4.18.2 (#1489) * v4.18.2 * update the actual app_env and remove .fish [skip ci] --- assets/text/Monerocom_Release_Notes.txt | 1 + assets/text/Release_Notes.txt | 2 + scripts/android/app_env.fish | 75 ------------------------- scripts/android/app_env.sh | 8 +-- scripts/ios/app_env.sh | 8 +-- scripts/macos/app_env.sh | 8 +-- 6 files changed, 15 insertions(+), 87 deletions(-) delete mode 100644 scripts/android/app_env.fish diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index faad67777..34f805b90 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1 +1,2 @@ +In-app Cake Pay is Back Bug fixes and generic enhancements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index faad67777..dcaf59665 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1 +1,3 @@ +In-app Cake Pay is Back +Bitcoin nodes stability enhancements Bug fixes and generic enhancements \ No newline at end of file diff --git a/scripts/android/app_env.fish b/scripts/android/app_env.fish deleted file mode 100644 index 2268fe4dc..000000000 --- a/scripts/android/app_env.fish +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env fish - -set APP_ANDROID_NAME "" -set APP_ANDROID_VERSION "" -set APP_ANDROID_BUILD_VERSION "" -set APP_ANDROID_ID "" -set APP_ANDROID_PACKAGE "" -set APP_ANDROID_SCHEME "" - -set MONERO_COM "monero.com" -set CAKEWALLET "cakewallet" -set HAVEN "haven" - -set -l TYPES $MONERO_COM $CAKEWALLET $HAVEN -set APP_ANDROID_TYPE $argv[1] - -set MONERO_COM_NAME "Monero.com" -set MONERO_COM_VERSION "1.12.3" -set MONERO_COM_BUILD_NUMBER 84 -set MONERO_COM_BUNDLE_ID "com.monero.app" -set MONERO_COM_PACKAGE "com.monero.app" -set MONERO_COM_SCHEME "monero.com" - -set CAKEWALLET_NAME "Cake Wallet" -set CAKEWALLET_VERSION "4.15.6" -set CAKEWALLET_BUILD_NUMBER 207 -set CAKEWALLET_BUNDLE_ID "com.cakewallet.cake_wallet" -set CAKEWALLET_PACKAGE "com.cakewallet.cake_wallet" -set CAKEWALLET_SCHEME "cakewallet" - -set HAVEN_NAME "Haven" -set HAVEN_VERSION "1.0.0" -set HAVEN_BUILD_NUMBER 1 -set HAVEN_BUNDLE_ID "com.cakewallet.haven" -set HAVEN_PACKAGE "com.cakewallet.haven" - -if not contains $APP_ANDROID_TYPE $TYPES - echo "Wrong app type." - return 1 - exit 1 -end - -switch $APP_ANDROID_TYPE - case $MONERO_COM - set APP_ANDROID_NAME $MONERO_COM_NAME - set APP_ANDROID_VERSION $MONERO_COM_VERSION - set APP_ANDROID_BUILD_NUMBER $MONERO_COM_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $MONERO_COM_BUNDLE_ID - set APP_ANDROID_PACKAGE $MONERO_COM_PACKAGE - set APP_ANDROID_SCHEME $MONERO_COM_SCHEME - ;; - case $CAKEWALLET - set APP_ANDROID_NAME $CAKEWALLET_NAME - set APP_ANDROID_VERSION $CAKEWALLET_VERSION - set APP_ANDROID_BUILD_NUMBER $CAKEWALLET_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $CAKEWALLET_BUNDLE_ID - set APP_ANDROID_PACKAGE $CAKEWALLET_PACKAGE - set APP_ANDROID_SCHEME $CAKEWALLET_SCHEME - ;; - case $HAVEN - set APP_ANDROID_NAME $HAVEN_NAME - set APP_ANDROID_VERSION $HAVEN_VERSION - set APP_ANDROID_BUILD_NUMBER $HAVEN_BUILD_NUMBER - set APP_ANDROID_BUNDLE_ID $HAVEN_BUNDLE_ID - set APP_ANDROID_PACKAGE $HAVEN_PACKAGE - ;; -end - -export APP_ANDROID_TYPE -export APP_ANDROID_NAME -export APP_ANDROID_VERSION -export APP_ANDROID_BUILD_NUMBER -export APP_ANDROID_BUNDLE_ID -export APP_ANDROID_PACKAGE -export APP_ANDROID_SCHEME diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index deceec53e..99703a079 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.15.1" -MONERO_COM_BUILD_NUMBER=91 +MONERO_COM_VERSION="1.15.2" +MONERO_COM_BUILD_NUMBER=92 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.18.1" -CAKEWALLET_BUILD_NUMBER=217 +CAKEWALLET_VERSION="4.18.2" +CAKEWALLET_BUILD_NUMBER=218 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 8893d4842..974e44bc4 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.15.1" -MONERO_COM_BUILD_NUMBER=89 +MONERO_COM_VERSION="1.15.2" +MONERO_COM_BUILD_NUMBER=90 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.18.1" -CAKEWALLET_BUILD_NUMBER=249 +CAKEWALLET_VERSION="4.18.2" +CAKEWALLET_BUILD_NUMBER=250 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index e648f1aa0..eae2fe886 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.5.1" -MONERO_COM_BUILD_NUMBER=22 +MONERO_COM_VERSION="1.5.2" +MONERO_COM_BUILD_NUMBER=23 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.11.1" -CAKEWALLET_BUILD_NUMBER=79 +CAKEWALLET_VERSION="1.11.2" +CAKEWALLET_BUILD_NUMBER=80 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then From fc2c9a2bcc6dc192fdaa5fd36fefb49b62091a83 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:24:10 +0100 Subject: [PATCH 09/20] feat: Int overflow issue (#1482) --- cw_evm/lib/evm_chain_formatter.dart | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/cw_evm/lib/evm_chain_formatter.dart b/cw_evm/lib/evm_chain_formatter.dart index cb9b7346c..797728885 100644 --- a/cw_evm/lib/evm_chain_formatter.dart +++ b/cw_evm/lib/evm_chain_formatter.dart @@ -1,15 +1,13 @@ -import 'package:intl/intl.dart'; - -const evmChainAmountLength = 12; -const evmChainAmountDivider = 1000000000000; -final evmChainAmountFormat = NumberFormat() - ..maximumFractionDigits = evmChainAmountLength - ..minimumFractionDigits = 1; +import 'dart:math'; class EVMChainFormatter { + static int _divider = 0; + static int parseEVMChainAmount(String amount) { try { - return (double.parse(amount) * evmChainAmountDivider).round(); + final decimalLength = _getDividerForInput(amount); + _divider = decimalLength; + return (double.parse(amount) * pow(10, decimalLength)).round(); } catch (_) { return 0; } @@ -17,9 +15,19 @@ class EVMChainFormatter { static double parseEVMChainAmountToDouble(int amount) { try { - return amount / evmChainAmountDivider; + return amount / pow(10, _divider); } catch (_) { return 0; } } + + static int _getDividerForInput(String amount) { + final result = amount.split('.'); + if (result.length > 1) { + final decimalLength = result[1].length; + return decimalLength; + } else { + return 0; + } + } } From 591342ec6a0fd26e3163e8536fd14789edeb2aa4 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Tue, 18 Jun 2024 07:08:03 +0200 Subject: [PATCH 10/20] electrum updates (#1449) * hotfixes * copy over the rest of the fixes * use hardened derivation path everywhere * correct balance path for electrum * revert index nullability and correct balance path for all cases * only save wallet info if we changed it --- cw_bitcoin/lib/bitcoin_wallet.dart | 3 +- cw_bitcoin/lib/electrum_derivations.dart | 3 ++ cw_bitcoin/lib/electrum_wallet.dart | 3 +- cw_bitcoin/lib/electrum_wallet_snapshot.dart | 3 +- cw_bitcoin/lib/utils.dart | 36 ++++++++++-------- lib/bitcoin/cw_bitcoin.dart | 40 +++++++++----------- lib/entities/default_settings_migration.dart | 15 ++++++++ lib/main.dart | 2 +- lib/view_model/wallet_creation_vm.dart | 6 +-- tool/configure.dart | 1 + 10 files changed, 66 insertions(+), 46 deletions(-) diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 3954631e8..d061480ed 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -6,6 +6,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:convert/convert.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/bitcoin_wallet_addresses.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -150,7 +151,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { ); // set the default if not present: - walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/0"; + walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? electrum_path; walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum; Uint8List? seedBytes = null; diff --git a/cw_bitcoin/lib/electrum_derivations.dart b/cw_bitcoin/lib/electrum_derivations.dart index 7e19f1cb4..749e5c7af 100644 --- a/cw_bitcoin/lib/electrum_derivations.dart +++ b/cw_bitcoin/lib/electrum_derivations.dart @@ -108,3 +108,6 @@ Map> electrum_derivations = { ), ], }; + + +String electrum_path = electrum_derivations[DerivationType.electrum]!.first.derivationPath!; \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index d3736e076..db42e2356 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -17,6 +17,7 @@ import 'package:cw_bitcoin/bitcoin_unspent.dart'; import 'package:cw_bitcoin/bitcoin_wallet_keys.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_bitcoin/electrum_transaction_history.dart'; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -133,7 +134,7 @@ abstract class ElectrumWalletBase return currency == CryptoCurrency.bch ? bitcoinCashHDWallet(seedBytes) : bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) - .derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? "m/0'")); + .derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? electrum_path)); } return bitcoin.HDWallet.fromBase58(xpub!); diff --git a/cw_bitcoin/lib/electrum_wallet_snapshot.dart b/cw_bitcoin/lib/electrum_wallet_snapshot.dart index 3e3f39131..15ad1cf63 100644 --- a/cw_bitcoin/lib/electrum_wallet_snapshot.dart +++ b/cw_bitcoin/lib/electrum_wallet_snapshot.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum_balance.dart'; +import 'package:cw_bitcoin/electrum_derivations.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/utils/file.dart'; @@ -71,7 +72,7 @@ class ElectrumWalletSnapshot { final derivationType = DerivationType .values[(data['derivationTypeIndex'] as int?) ?? DerivationType.electrum.index]; - final derivationPath = data['derivationPath'] as String? ?? "m/0'/0"; + final derivationPath = data['derivationPath'] as String? ?? electrum_path; try { regularAddressIndexByType = { diff --git a/cw_bitcoin/lib/utils.dart b/cw_bitcoin/lib/utils.dart index b3707e764..e3ebc00db 100644 --- a/cw_bitcoin/lib/utils.dart +++ b/cw_bitcoin/lib/utils.dart @@ -5,58 +5,64 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:hex/hex.dart'; -bitcoin.PaymentData generatePaymentData({required bitcoin.HDWallet hd, int? index}) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; +bitcoin.PaymentData generatePaymentData({ + required bitcoin.HDWallet hd, + required int index, +}) { + final pubKey = hd.derive(index).pubKey!; return PaymentData(pubkey: Uint8List.fromList(HEX.decode(pubKey))); } -ECPrivate generateECPrivate( - {required bitcoin.HDWallet hd, required BasedUtxoNetwork network, int? index}) { - final wif = index != null ? hd.derive(index).wif! : hd.wif!; +ECPrivate generateECPrivate({ + required bitcoin.HDWallet hd, + required BasedUtxoNetwork network, + required int index, +}) { + final wif = hd.derive(index).wif!; return ECPrivate.fromWif(wif, netVersion: network.wifNetVer); } String generateP2WPKHAddress({ required bitcoin.HDWallet hd, required BasedUtxoNetwork network, - int? index, + required int index, }) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + final pubKey = hd.derive(index).pubKey!; return ECPublic.fromHex(pubKey).toP2wpkhAddress().toAddress(network); } String generateP2SHAddress({ required bitcoin.HDWallet hd, required BasedUtxoNetwork network, - int? index, + required int index, }) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + final pubKey = hd.derive(index).pubKey!; return ECPublic.fromHex(pubKey).toP2wpkhInP2sh().toAddress(network); } String generateP2WSHAddress({ required bitcoin.HDWallet hd, required BasedUtxoNetwork network, - int? index, + required int index, }) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + final pubKey = hd.derive(index).pubKey!; return ECPublic.fromHex(pubKey).toP2wshAddress().toAddress(network); } String generateP2PKHAddress({ required bitcoin.HDWallet hd, required BasedUtxoNetwork network, - int? index, + required int index, }) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + final pubKey = hd.derive(index).pubKey!; return ECPublic.fromHex(pubKey).toP2pkhAddress().toAddress(network); } String generateP2TRAddress({ required bitcoin.HDWallet hd, required BasedUtxoNetwork network, - int? index, + required int index, }) { - final pubKey = index != null ? hd.derive(index).pubKey! : hd.pubKey!; + final pubKey = hd.derive(index).pubKey!; return ECPublic.fromHex(pubKey).toTaprootAddress().toAddress(network); } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 15d2ffadf..c62030504 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -295,14 +295,7 @@ class CWBitcoin extends Bitcoin { List types = await compareDerivationMethods(mnemonic: mnemonic, node: node); if (types.length == 1 && types.first == DerivationType.electrum) { - return [ - DerivationInfo( - derivationType: DerivationType.electrum, - derivationPath: "m/0'", - description: "Electrum", - scriptType: "p2wpkh", - ) - ]; + return [getElectrumDerivations()[DerivationType.electrum]!.first]; } final electrumClient = ElectrumClient(); @@ -339,38 +332,34 @@ class CWBitcoin extends Bitcoin { scriptType: dInfo.scriptType, ); - String derivationPath = dInfoCopy.derivationPath!; - int derivationDepth = _countOccurrences(derivationPath, "/"); - - // the correct derivation depth is dependant on the derivation type: - // the derivation paths defined in electrum_derivations are at the ROOT level, i.e.: - // electrum's format doesn't specify subaddresses, just subaccounts: + String balancePath = dInfoCopy.derivationPath!; + int derivationDepth = _countOccurrences(balancePath, "/"); // for BIP44 - if (derivationDepth == 3) { - // we add "/0/0" so that we generate account 0, index 0 and correctly get balance - derivationPath += "/0/0"; + if (derivationDepth == 3 || derivationDepth == 1) { + // we add "/0" so that we generate account 0 + balancePath += "/0"; } - // var hd = bip32.BIP32.fromSeed(seedBytes).derivePath(derivationPath); final hd = btc.HDWallet.fromSeed( seedBytes, network: networkType, - ).derivePath(derivationPath); + ).derivePath(balancePath); + // derive address at index 0: String? address; switch (dInfoCopy.scriptType) { case "p2wpkh": - address = generateP2WPKHAddress(hd: hd, network: network); + address = generateP2WPKHAddress(hd: hd, network: network, index: 0); break; case "p2pkh": - address = generateP2PKHAddress(hd: hd, network: network); + address = generateP2PKHAddress(hd: hd, network: network, index: 0); break; case "p2wpkh-p2sh": - address = generateP2SHAddress(hd: hd, network: network); + address = generateP2SHAddress(hd: hd, network: network, index: 0); break; case "p2tr": - address = generateP2TRAddress(hd: hd, network: network); + address = generateP2TRAddress(hd: hd, network: network, index: 0); break; default: continue; @@ -396,6 +385,11 @@ class CWBitcoin extends Bitcoin { return list; } + @override + Map> getElectrumDerivations() { + return electrum_derivations; + } + @override bool hasTaprootInput(PendingTransaction pendingTransaction) { return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index dcde1d3ce..d25c76dc7 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -233,6 +233,8 @@ Future defaultSettingsMigration( case 36: await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); break; + case 37: + await fixBtcDerivationPaths(walletInfoSource); default: break; } @@ -775,6 +777,19 @@ Future changeDefaultMoneroNode( } } +Future fixBtcDerivationPaths(Box walletsInfoSource) async { + for (WalletInfo walletInfo in walletsInfoSource.values) { + if (walletInfo.type == WalletType.bitcoin || + walletInfo.type == WalletType.bitcoinCash || + walletInfo.type == WalletType.litecoin) { + if (walletInfo.derivationInfo?.derivationPath == "m/0'/0") { + walletInfo.derivationInfo!.derivationPath = "m/0'"; + await walletInfo.save(); + } + } + } +} + Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { for (WalletInfo walletInfo in walletsInfoSource.values) { if (walletInfo.type == WalletType.nano || walletInfo.type == WalletType.bitcoin) { diff --git a/lib/main.dart b/lib/main.dart index 5bcd9ffdb..46bd7c608 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 36, + initialMigrationVersion: 37, ); } diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index f825f0c47..841a88e7e 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; @@ -98,10 +99,7 @@ abstract class WalletCreationVMBase with Store { ); case WalletType.bitcoin: case WalletType.litecoin: - return DerivationInfo( - derivationType: DerivationType.electrum, - derivationPath: "m/0'", - ); + return bitcoin!.getElectrumDerivations()[DerivationType.electrum]!.first; default: return null; } diff --git a/tool/configure.dart b/tool/configure.dart index fc9bd5b91..e7ad676be 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -186,6 +186,7 @@ abstract class Bitcoin { {required String mnemonic, required Node node}); Future> getDerivationsFromMnemonic( {required String mnemonic, required Node node, String? passphrase}); + Map> getElectrumDerivations(); Future setAddressType(Object wallet, dynamic option); ReceivePageOption getSelectedAddressType(Object wallet); List getBitcoinReceivePageOptions(); From 5b3161fb2958b6dd0fae0249a35b3979999367f3 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 18 Jun 2024 02:00:07 -0400 Subject: [PATCH 11/20] Add Recovery Height to Wallet seed page for monero (#1470) --- cw_monero/lib/monero_wallet.dart | 2 ++ lib/monero/cw_monero.dart | 6 ++++++ lib/view_model/wallet_keys_view_model.dart | 19 ++++++++++++------- res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 3 ++- res/values/strings_en.arb | 1 + res/values/strings_es.arb | 1 + res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 1 + res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + tool/configure.dart | 1 + 30 files changed, 48 insertions(+), 8 deletions(-) diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index c270bb113..8af6b653c 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -108,6 +108,8 @@ abstract class MoneroWalletBase publicSpendKey: monero_wallet.getPublicSpendKey(), publicViewKey: monero_wallet.getPublicViewKey()); + int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height; + monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; bool _isTransactionUpdating; diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 959ae92ce..b4d85089a 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -244,6 +244,12 @@ class CWMonero extends Monero { }; } + @override + int? getRestoreHeight(Object wallet) { + final moneroWallet = wallet as MoneroWallet; + return moneroWallet.restoreHeight; + } + @override Object createMoneroTransactionCreationCredentials( {required List outputs, required TransactionPriority priority}) => diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 6b5ae5559..060770273 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,16 +1,15 @@ -import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_keys_view_model.g.dart'; @@ -83,6 +82,12 @@ abstract class WalletKeysViewModelBase with Store { .toLegacySeed(legacyLang); items.add(StandartListItem(title: S.current.wallet_seed_legacy, value: legacySeed)); } + + final restoreHeight = monero!.getRestoreHeight(_appStore.wallet!); + if (restoreHeight != null) { + items.add(StandartListItem( + title: S.current.wallet_recovery_height, value: restoreHeight.toString())); + } } if (_appStore.wallet!.type == WalletType.haven) { diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index fa2747230..db6a4cae2 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -834,6 +834,7 @@ "wallet_menu": "قائمة", "wallet_name": "اسم المحفظة", "wallet_name_exists": "توجد بالفعل محفظة بهذا الاسم. الرجاء اختيار اسم مختلف أو إعادة تسمية المحفظة الأخرى أولاً.", + "wallet_recovery_height": "ارتفاع الاسترداد", "wallet_restoration_store_incorrect_seed_length": "طول السييد غير صحيح", "wallet_seed": "سييد المحفظة", "wallet_seed_legacy": "بذرة محفظة قديمة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 430a26134..06132c244 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -834,6 +834,7 @@ "wallet_menu": "Меню", "wallet_name": "Име на портфейл", "wallet_name_exists": "Вече има портфейл с това име. Моля, изберете друго име или преименувайте другия портфейл.", + "wallet_recovery_height": "Височина на възстановяване", "wallet_restoration_store_incorrect_seed_length": "Грешна дължина на seed-а", "wallet_seed": "Seed на портфейла", "wallet_seed_legacy": "Наследено портфейл семе", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 6a92cb2e5..589f89fd7 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menu", "wallet_name": "Název peněženky", "wallet_name_exists": "Peněženka s tímto názvem už existuje. Prosím zvolte si jiný název, nebo nejprve přejmenujte nejprve druhou peněženku.", + "wallet_recovery_height": "Výška zotavení", "wallet_restoration_store_incorrect_seed_length": "Nesprávná délka seedu", "wallet_seed": "Seed peněženky", "wallet_seed_legacy": "Starší semeno peněženky", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 2e4203edd..a4b816f5b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -837,6 +837,7 @@ "wallet_menu": "Wallet-Menü", "wallet_name": "Walletname", "wallet_name_exists": "Wallet mit diesem Namen existiert bereits", + "wallet_recovery_height": "Erstellungshöhe", "wallet_restoration_store_incorrect_seed_length": "Falsche Seed-Länge", "wallet_seed": "Wallet-Seed", "wallet_seed_legacy": "Legacy Wallet Seed", @@ -875,4 +876,4 @@ "you_will_get": "Konvertieren zu", "you_will_send": "Konvertieren von", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1b3da32ab..ac82c9e0f 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menu", "wallet_name": "Wallet name", "wallet_name_exists": "A wallet with that name already exists. Please choose a different name or rename the other wallet first.", + "wallet_recovery_height": "Recovery Height", "wallet_restoration_store_incorrect_seed_length": "Incorrect seed length", "wallet_seed": "Wallet seed", "wallet_seed_legacy": "Legacy wallet seed", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 215cece6e..5fba46830 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -835,6 +835,7 @@ "wallet_menu": "Menú de billetera", "wallet_name": "Nombre de la billetera", "wallet_name_exists": "Wallet con ese nombre ya ha existido", + "wallet_recovery_height": "Altura de recuperación", "wallet_restoration_store_incorrect_seed_length": "Longitud de semilla incorrecta", "wallet_seed": "Semilla de billetera", "wallet_seed_legacy": "Semilla de billetera heredada", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 997dbc38c..2477c09b1 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menu", "wallet_name": "Nom du Portefeuille (Wallet)", "wallet_name_exists": "Un portefeuille (wallet) portant ce nom existe déjà", + "wallet_recovery_height": "Hauteur de récupération", "wallet_restoration_store_incorrect_seed_length": "Longueur de phrase secrète (seed) incorrecte", "wallet_seed": "Phrase secrète (seed) du portefeuille (wallet)", "wallet_seed_legacy": "Graine de portefeuille hérité", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 478bc458d..c96568a76 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -836,6 +836,7 @@ "wallet_menu": "Menu", "wallet_name": "Sunan walat", "wallet_name_exists": "Wallet mai wannan sunan ya riga ya wanzu. Da fatan za a zaɓi wani suna daban ko sake suna ɗayan walat tukuna.", + "wallet_recovery_height": "Mai Tsaro", "wallet_restoration_store_incorrect_seed_length": "kalmar sirrin iri ba daidai ba", "wallet_seed": "kalmar sirri na walat", "wallet_seed_legacy": "Tallarin walat walat", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4f3a77fdb..ef740b4d6 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -836,6 +836,7 @@ "wallet_menu": "बटुआ मेनू", "wallet_name": "बटुए का नाम", "wallet_name_exists": "उस नाम वाला वॉलेट पहले से मौजूद है", + "wallet_recovery_height": "वसूली ऊंचाई", "wallet_restoration_store_incorrect_seed_length": "गलत बीज की लंबाई", "wallet_seed": "बटुआ का बीज", "wallet_seed_legacy": "विरासत बटुए बीज", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index faef9cd78..295a01165 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -834,6 +834,7 @@ "wallet_menu": "Izbornik", "wallet_name": "Ime novčanika", "wallet_name_exists": "Novčanik s tim nazivom već postoji", + "wallet_recovery_height": "Visina oporavka", "wallet_restoration_store_incorrect_seed_length": "Netočna dužina pristupnog izraza", "wallet_seed": "Pristupni izraz novčanika", "wallet_seed_legacy": "Sjeme naslijeđenog novčanika", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e718f24da..8b28e928f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -837,6 +837,7 @@ "wallet_menu": "Menu", "wallet_name": "Nama Dompet", "wallet_name_exists": "Nama dompet sudah ada. Silakan pilih nama yang berbeda atau ganti nama dompet yang lain terlebih dahulu.", + "wallet_recovery_height": "Tinggi pemulihan", "wallet_restoration_store_incorrect_seed_length": "Panjang seed yang salah", "wallet_seed": "Seed dompet", "wallet_seed_legacy": "Biji dompet warisan", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 6a25ad9e0..b1bd7cd6d 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -837,6 +837,7 @@ "wallet_menu": "Menù", "wallet_name": "Nome del Portafoglio", "wallet_name_exists": "Il portafoglio con quel nome è già esistito", + "wallet_recovery_height": "Altezza di recupero", "wallet_restoration_store_incorrect_seed_length": "Lunghezza seme non corretta", "wallet_seed": "Seme Portafoglio", "wallet_seed_legacy": "Seme di portafoglio legacy", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 56786cabc..5803b86d5 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -835,6 +835,7 @@ "wallet_menu": "ウォレットメニュー", "wallet_name": "ウォレット名", "wallet_name_exists": "その名前のウォレットはすでに存在しています", + "wallet_recovery_height": "回復の高さ", "wallet_restoration_store_incorrect_seed_length": "誤ったシード長s", "wallet_seed": "ウォレットシード", "wallet_seed_legacy": "レガシーウォレットシード", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5de972a6e..874ec2c27 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -835,6 +835,7 @@ "wallet_menu": "월렛 메뉴", "wallet_name": "지갑 이름", "wallet_name_exists": "해당 이름의 지갑이 이미 존재합니다.", + "wallet_recovery_height": "복구 높이", "wallet_restoration_store_incorrect_seed_length": "시드 길이가 잘못되었습니다", "wallet_seed": "지갑 시드", "wallet_seed_legacy": "레거시 지갑 시드", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 497e1bb0c..890362fcd 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -834,6 +834,7 @@ "wallet_menu": "မီနူး", "wallet_name": "ပိုက်ဆံအိတ်နာမည", "wallet_name_exists": "ထိုအမည်ဖြင့် ပိုက်ဆံအိတ်တစ်ခု ရှိနှင့်ပြီးဖြစ်သည်။ အခြားအမည်တစ်ခုကို ရွေးပါ သို့မဟုတ် အခြားပိုက်ဆံအိတ်ကို ဦးစွာ အမည်ပြောင်းပါ။", + "wallet_recovery_height": "ပြန်လည်ထူထောင်ရေးအမြင့်", "wallet_restoration_store_incorrect_seed_length": "မျိုးစေ့အရှည် မမှန်ပါ။", "wallet_seed": "ပိုက်ဆံအိတ်စေ့", "wallet_seed_legacy": "အမွေအနှစ်ပိုက်ဆံအိတ်မျိုးစေ့", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 1bc1f8cd7..25b7f6b3a 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -835,6 +835,7 @@ "wallet_menu": "Portemonnee-menu", "wallet_name": "Portemonnee naam", "wallet_name_exists": "Portemonnee met die naam bestaat al", + "wallet_recovery_height": "Herstelhoogte", "wallet_restoration_store_incorrect_seed_length": "Onjuiste zaadlengte", "wallet_seed": "Portemonnee zaad", "wallet_seed_legacy": "Legacy portemonnee zaad", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index ff8826c1e..1567a0841 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menu portfela", "wallet_name": "Nazwa portfela", "wallet_name_exists": "Portfel o tej nazwie już istnieje", + "wallet_recovery_height": "Wysokość odzysku", "wallet_restoration_store_incorrect_seed_length": "Nieprawidłowa długość frazy seed", "wallet_seed": "Seed portfela", "wallet_seed_legacy": "Dziedziczne ziarno portfela", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 67ce2980a..272c0862e 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -837,6 +837,7 @@ "wallet_menu": "Menu", "wallet_name": "Nome da carteira", "wallet_name_exists": "A carteira com esse nome já existe", + "wallet_recovery_height": "Altura de recuperação", "wallet_restoration_store_incorrect_seed_length": "Comprimento de semente incorreto", "wallet_seed": "Semente de carteira", "wallet_seed_legacy": "Semente de carteira herdada", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 673f1472d..a8943db97 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -835,6 +835,7 @@ "wallet_menu": "Меню кошелька", "wallet_name": "Имя кошелька", "wallet_name_exists": "Кошелек с таким именем уже существует", + "wallet_recovery_height": "Высота восстановления", "wallet_restoration_store_incorrect_seed_length": "Неверная длина мнемонической фразы", "wallet_seed": "Мнемоническая фраза кошелька", "wallet_seed_legacy": "Наследие семя кошелька", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 1779b9da1..c4d282761 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -834,6 +834,7 @@ "wallet_menu": "เมนู", "wallet_name": "ชื่อกระเป๋า", "wallet_name_exists": "กระเป๋าที่มีชื่อนี้มีอยู่แล้ว โปรดเลือกชื่ออื่นหรือเปลี่ยนชื่อกระเป๋าอื่นก่อน", + "wallet_recovery_height": "ความสูงของการกู้คืน", "wallet_restoration_store_incorrect_seed_length": "ความยาวของซีดไม่ถูกต้อง", "wallet_seed": "ซีดของกระเป๋า", "wallet_seed_legacy": "เมล็ดกระเป๋าเงินมรดก", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index e358d6daf..775ab4c3d 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menu", "wallet_name": "Pangalan ng Wallet", "wallet_name_exists": "Ang isang pitaka na may pangalang iyon ay mayroon na. Mangyaring pumili ng ibang pangalan o palitan muna ang iba pang pitaka.", + "wallet_recovery_height": "Taas ng pagbawi", "wallet_restoration_store_incorrect_seed_length": "Maling haba ng binhi", "wallet_seed": "SEED ng Wallet", "wallet_seed_legacy": "Legacy wallet seed", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 76bd79d8a..239a1aa2e 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -834,6 +834,7 @@ "wallet_menu": "Menü", "wallet_name": "Cüzdan ismi", "wallet_name_exists": "Bu isimde bir cüzdan zaten mevcut. Lütfen farklı bir isim seç veya önce diğer cüzdanı yeniden adlandır.", + "wallet_recovery_height": "Kurtarma Yüksekliği", "wallet_restoration_store_incorrect_seed_length": "Yanlış tohum uzunluğu", "wallet_seed": "Cüzdan tohumu", "wallet_seed_legacy": "Eski cüzdan tohumu", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index e1d470e1e..3a45752d6 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -835,6 +835,7 @@ "wallet_menu": "Меню гаманця", "wallet_name": "Ім'я гаманця", "wallet_name_exists": "Гаманець з такою назвою вже існує", + "wallet_recovery_height": "Висота відновлення", "wallet_restoration_store_incorrect_seed_length": "Невірна довжина мнемонічної фрази", "wallet_seed": "Мнемонічна фраза гаманця", "wallet_seed_legacy": "Спадець насіння гаманця", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 02c5216ef..2ab1f927b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -836,6 +836,7 @@ "wallet_menu": "مینو", "wallet_name": "بٹوے کا نام", "wallet_name_exists": "اس نام کا پرس پہلے سے موجود ہے۔ براہ کرم ایک مختلف نام منتخب کریں یا پہلے دوسرے بٹوے کا نام تبدیل کریں۔", + "wallet_recovery_height": "بحالی کی اونچائی", "wallet_restoration_store_incorrect_seed_length": "غلط بیج کی لمبائی", "wallet_seed": "بٹوے کا بیج", "wallet_seed_legacy": "میراثی پرس کا بیج", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 221b189e1..f9751f9f1 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -835,6 +835,7 @@ "wallet_menu": "Mẹ́nù", "wallet_name": "Orúkọ àpamọ́wọ́", "wallet_name_exists": "Ẹ ti ní àpamọ́wọ́ pẹ̀lú orúkọ̀ yẹn. Ẹ jọ̀wọ́ yàn orúkọ̀ tó yàtọ̀ tàbí pààrọ̀ orúkọ ti àpamọ́wọ́ tẹ́lẹ̀.", + "wallet_recovery_height": "Iga Imularada", "wallet_restoration_store_incorrect_seed_length": "Gígùn hóró tí a máa ń lò kọ́ ni èyí", "wallet_seed": "Hóró àpamọ́wọ́", "wallet_seed_legacy": "Irugbin akole", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index d98801cb2..f3b8ee176 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -834,6 +834,7 @@ "wallet_menu": "钱包菜单", "wallet_name": "钱包名称", "wallet_name_exists": "同名的钱包已经存在", + "wallet_recovery_height": "恢复高度", "wallet_restoration_store_incorrect_seed_length": "种子长度错误", "wallet_seed": "钱包种子", "wallet_seed_legacy": "旧的钱包种子", diff --git a/tool/configure.dart b/tool/configure.dart index e7ad676be..b1018a5a0 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -363,6 +363,7 @@ abstract class Monero { WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String password}); Map getKeys(Object wallet); + int? getRestoreHeight(Object wallet); Object createMoneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); Object createMoneroTransactionCreationCredentialsRaw({required List outputs, required TransactionPriority priority}); String formatterMoneroAmountToString({required int amount}); From ab293548d23f848894c069e607ca2bf095cfaa06 Mon Sep 17 00:00:00 2001 From: tuxsudo Date: Tue, 18 Jun 2024 16:16:39 +0000 Subject: [PATCH 12/20] Remove emotes from issue templates --- .github/ISSUE_TEMPLATE/{bug-report-🪲-.md => bug-report.md} | 0 ...nhancement-request-✨.md => feature-or-enhancement-request.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{bug-report-🪲-.md => bug-report.md} (100%) rename .github/ISSUE_TEMPLATE/{feature-or-enhancement-request-✨.md => feature-or-enhancement-request.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/bug-report-🪲-.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug-report-🪲-.md rename to .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md rename to .github/ISSUE_TEMPLATE/feature-or-enhancement-request.md From 1690f6af1edc26c025cf07e5f21eb5af1725de99 Mon Sep 17 00:00:00 2001 From: Serhii Date: Thu, 20 Jun 2024 23:13:12 +0000 Subject: [PATCH 13/20] Erc20 token transactions are displaying incorrectly (#1493) * evm signature name * hide depositWithExpiry and transfer transactions * Update contact_list_view_model.dart * remove erc20 token history when disabled --- cw_core/lib/transaction_info.dart | 1 + cw_ethereum/lib/ethereum_client.dart | 5 ++ .../lib/ethereum_transaction_info.dart | 2 + cw_ethereum/lib/ethereum_wallet.dart | 1 + cw_evm/lib/evm_chain_transaction_info.dart | 3 ++ cw_evm/lib/evm_chain_transaction_model.dart | 6 +++ cw_evm/lib/evm_chain_wallet.dart | 46 +++++++++++++++++-- lib/ethereum/cw_ethereum.dart | 4 ++ lib/polygon/cw_polygon.dart | 4 ++ .../dashboard/pages/transactions_page.dart | 8 +++- .../contact_list/contact_list_view_model.dart | 2 +- .../dashboard/home_settings_view_model.dart | 2 + tool/configure.dart | 2 + 13 files changed, 79 insertions(+), 7 deletions(-) diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 992582ff8..7b02bf1ff 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -16,6 +16,7 @@ abstract class TransactionInfo extends Object with Keyable { void changeFiatAmount(String amount); String? to; String? from; + String? evmSignatureName; List? inputAddresses; List? outputAddresses; diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index f2b25bcdd..9d50fdd5b 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -29,6 +29,11 @@ class EthereumClient extends EVMChainClient { final jsonResponse = json.decode(response.body) as Map; + if (jsonResponse['result'] is String) { + log(jsonResponse['result']); + return []; + } + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { return (jsonResponse['result'] as List) .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart index d5d3fea8d..7a62c0b6a 100644 --- a/cw_ethereum/lib/ethereum_transaction_info.dart +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -14,6 +14,7 @@ class EthereumTransactionInfo extends EVMChainTransactionInfo { required super.confirmations, required super.to, required super.from, + super.evmSignatureName, super.exponent, }); @@ -31,6 +32,7 @@ class EthereumTransactionInfo extends EVMChainTransactionInfo { tokenSymbol: data['tokenSymbol'] as String, to: data['to'], from: data['from'], + evmSignatureName: data['evmSignatureName'], ); } diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 4604db662..2c58cd31d 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -94,6 +94,7 @@ class EthereumWallet extends EVMChainWallet { tokenSymbol: transactionModel.tokenSymbol ?? "ETH", to: transactionModel.to, from: transactionModel.from, + evmSignatureName: transactionModel.evmSignatureName, ); return model; } diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart index 329061db2..fb6cf6b52 100644 --- a/cw_evm/lib/evm_chain_transaction_info.dart +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -20,6 +20,7 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { required this.confirmations, required this.to, required this.from, + this.evmSignatureName, }) : amount = ethAmount.toInt(), fee = ethFee.toInt(); @@ -38,6 +39,7 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { String? _fiatAmount; final String? to; final String? from; + final String? evmSignatureName; //! Getter to be overridden in child classes String get feeCurrency; @@ -73,5 +75,6 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { 'tokenSymbol': tokenSymbol, 'to': to, 'from': from, + 'evmSignatureName': evmSignatureName, }; } diff --git a/cw_evm/lib/evm_chain_transaction_model.dart b/cw_evm/lib/evm_chain_transaction_model.dart index dfdeab8f5..ca4fb0864 100644 --- a/cw_evm/lib/evm_chain_transaction_model.dart +++ b/cw_evm/lib/evm_chain_transaction_model.dart @@ -12,6 +12,8 @@ class EVMChainTransactionModel { final String? tokenSymbol; final int? tokenDecimal; final bool isError; + final String input; + String? evmSignatureName; EVMChainTransactionModel({ required this.date, @@ -27,6 +29,8 @@ class EVMChainTransactionModel { required this.tokenSymbol, required this.tokenDecimal, required this.isError, + required this.input, + this.evmSignatureName, }); factory EVMChainTransactionModel.fromJson(Map json, String defaultSymbol) => @@ -44,5 +48,7 @@ class EVMChainTransactionModel { tokenSymbol: json["tokenSymbol"] ?? defaultSymbol, tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), isError: json["isError"] == "1", + input: json["input"] ?? "", + evmSignatureName: json["evmSignatureName"], ); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 56b58d400..2adb54746 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -39,6 +39,20 @@ import 'evm_erc20_balance.dart'; part 'evm_chain_wallet.g.dart'; +const Map methodSignatureToType = { + '0x095ea7b3': 'approval', + '0xa9059cbb': 'transfer', + '0x23b872dd': 'transferFrom', + '0x574da717': 'transferOut', + '0x2e1a7d4d': 'withdraw', + '0x7ff36ab5': 'swapExactETHForTokens', + '0x40c10f19': 'mint', + '0x44bc937b': 'depositWithExpiry', + '0xd0e30db0': 'deposit', + '0xe8e33700': 'addLiquidity', + '0xd505accf': 'permit', +}; + abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; abstract class EVMChainWalletBase @@ -235,7 +249,8 @@ abstract class EVMChainWalletBase String? hexOpReturnMemo; if (opReturnMemo != null) { - hexOpReturnMemo = '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}'; + hexOpReturnMemo = + '0x${opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join()}'; } final CryptoCurrency transactionCurrency = @@ -337,11 +352,21 @@ abstract class EVMChainWalletBase @override Future> fetchTransactions() async { + final List transactions = []; + final List>> erc20TokensTransactions = []; + final address = _evmChainPrivateKey.address.hex; - final transactions = await _client.fetchTransactions(address); + final externalTransactions = await _client.fetchTransactions(address); final internalTransactions = await _client.fetchInternalTransactions(address); - final List>> erc20TokensTransactions = []; + for (var transaction in externalTransactions) { + final evmSignatureName = analyzeTransaction(transaction.input); + + if (evmSignatureName != 'depositWithExpiry' && evmSignatureName != 'transfer') { + transaction.evmSignatureName = evmSignatureName; + transactions.add(transaction); + } + } for (var token in balance.keys) { if (token is Erc20Token) { @@ -369,6 +394,17 @@ abstract class EVMChainWalletBase return result; } + String? analyzeTransaction(String? transactionInput) { + if (transactionInput == '0x' || transactionInput == null || transactionInput.isEmpty) { + return 'simpleTransfer'; + } + + final methodSignature = + transactionInput.length >= 10 ? transactionInput.substring(0, 10) : null; + + return methodSignatureToType[methodSignature]; + } + @override Object get keys => throw UnimplementedError("keys"); @@ -482,11 +518,11 @@ abstract class EVMChainWalletBase await token.delete(); balance.remove(token); - await _removeTokenTransactionsInHistory(token); + await removeTokenTransactionsInHistory(token); _updateBalance(); } - Future _removeTokenTransactionsInHistory(Erc20Token token) async { + Future removeTokenTransactionsInHistory(Erc20Token token) async { transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); await transactionHistory.save(); } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index e72108e79..7b593d58d 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -137,6 +137,10 @@ class CWEthereum extends Ethereum { Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => await (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); + @override + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => + await (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); + @override Future getErc20Token(WalletBase wallet, String contractAddress) async { final ethereumWallet = wallet as EthereumWallet; diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 16aba284d..2dcb1b4a6 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -135,6 +135,10 @@ class CWPolygon extends Polygon { Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) async => await (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token); + @override + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) async => + await (wallet as PolygonWallet).removeTokenTransactionsInHistory(token as Erc20Token); + @override Future getErc20Token(WalletBase wallet, String contractAddress) async { final polygonWallet = wallet as PolygonWallet; diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index e5c94415f..efdd764f1 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.d import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -87,6 +88,10 @@ class TransactionsPage extends StatelessWidget { } final transaction = item.transaction; + final transactionType = dashboardViewModel.type == WalletType.ethereum && + transaction.evmSignatureName == 'approval' + ? ' (${transaction.evmSignatureName})' + : ''; return Observer( builder: (_) => TransactionRow( @@ -100,7 +105,8 @@ class TransactionsPage extends StatelessWidget { ? '' : item.formattedFiatAmount, isPending: transaction.isPending, - title: item.formattedTitle + item.formattedStatus, + title: item.formattedTitle + + item.formattedStatus + ' $transactionType', ), ); } diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index 8dbd97bb9..d63f78224 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -39,7 +39,7 @@ abstract class ContactListViewModelBase with Store { )); } }); - } else if (info.addresses?.isNotEmpty == true) { + } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { info.addresses!.forEach((address, label) { if (label.isEmpty) { return; diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 9e3be746e..5778f1e19 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -144,10 +144,12 @@ abstract class HomeSettingsViewModelBase with Store { if (_balanceViewModel.wallet.type == WalletType.ethereum) { ethereum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); + if (!value) ethereum!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); } if (_balanceViewModel.wallet.type == WalletType.polygon) { polygon!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); + if (!value) polygon!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); } if (_balanceViewModel.wallet.type == WalletType.solana) { diff --git a/tool/configure.dart b/tool/configure.dart index b1018a5a0..133f12e52 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -667,6 +667,7 @@ abstract class Ethereum { List getERC20Currencies(WalletBase wallet); Future addErc20Token(WalletBase wallet, CryptoCurrency token); Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token); Future getErc20Token(WalletBase wallet, String contractAddress); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); @@ -770,6 +771,7 @@ abstract class Polygon { List getERC20Currencies(WalletBase wallet); Future addErc20Token(WalletBase wallet, CryptoCurrency token); Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token); Future getErc20Token(WalletBase wallet, String contractAddress); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); From 4a0096985a520b162cd3e8abe3aa834a1a81dd24 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 21 Jun 2024 02:17:19 +0300 Subject: [PATCH 14/20] fix donation link saved regardless of the current wallet (#1491) --- cw_solana/lib/solana_client.dart | 2 +- lib/entities/preferences_key.dart | 1 + .../screens/dashboard/pages/address_page.dart | 11 +-- .../screens/receive/anonpay_invoice_page.dart | 71 +++++++++++-------- .../anon_invoice_page_view_model.dart | 11 ++- 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index 6ed8cab29..38f2864df 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -456,7 +456,7 @@ class SolanaWalletClient { funder: ownerKeypair, ); } catch (e) { - throw Exception('Insufficient lamports balance to complete this transaction'); + throw Exception('Insufficient SOL balance to complete this transaction'); } // Input by the user diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index aebf9ccd5..fdcd54c9c 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -77,6 +77,7 @@ class PreferencesKey { static const moneroSeedType = 'monero_seed_type'; static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; + static const donationLinkWalletName = 'donation_link_wallet_name'; static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index c322843ef..3ac97740d 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -3,7 +3,6 @@ import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/monero_accounts/monero_account_list_page.dart'; import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/receive_page_option.dart'; @@ -14,7 +13,6 @@ import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/share_util.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_type.dart'; @@ -171,8 +169,7 @@ class AddressPage extends BasePage { textSize: 14, height: 50, ); - } - else { + } else { return const SizedBox(); } }), @@ -204,8 +201,12 @@ class AddressPage extends BasePage { final sharedPreferences = getIt.get(); final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink); final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink); + final donationWalletName = + sharedPreferences.getString(PreferencesKey.donationLinkWalletName); - if (clearnetUrl != null && onionUrl != null) { + if (clearnetUrl != null && + onionUrl != null && + addressListViewModel.wallet.name == donationWalletName) { Navigator.pushNamed( context, Routes.anonPayReceivePage, diff --git a/lib/src/screens/receive/anonpay_invoice_page.dart b/lib/src/screens/receive/anonpay_invoice_page.dart index f33cdcc5b..2c66b95db 100644 --- a/lib/src/screens/receive/anonpay_invoice_page.dart +++ b/lib/src/screens/receive/anonpay_invoice_page.dart @@ -60,8 +60,7 @@ class AnonPayInvoicePage extends BasePage { @override Widget middle(BuildContext context) => PresentReceiveOptionPicker( - receiveOptionViewModel: receiveOptionViewModel, - color: titleColor(context)); + receiveOptionViewModel: receiveOptionViewModel, color: titleColor(context)); @override Widget trailing(BuildContext context) => TrailButton( @@ -87,30 +86,36 @@ class AnonPayInvoicePage extends BasePage { config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, keyboardBarColor: Theme.of(context).extension()!.keyboardBarColor, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: _amountFocusNode, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - ]), - child: Container( - color: Theme.of(context).colorScheme.background, - child: ScrollableWithBottomSection( - contentPadding: EdgeInsets.only(bottom: 24), - content: Container( - decoration: responsiveLayoutUtil.shouldRenderMobileUI ? BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - gradient: LinearGradient( - colors: [ - Theme.of(context).extension()!.firstGradientTopPanelColor, - Theme.of(context).extension()!.secondGradientTopPanelColor, - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: _amountFocusNode, + toolbarButtons: [(_) => KeyboardDoneButton()], ), - ) : null, + ]), + child: Container( + color: Theme.of(context).colorScheme.background, + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.only(bottom: 24), + content: Container( + decoration: responsiveLayoutUtil.shouldRenderMobileUI + ? BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient( + colors: [ + Theme.of(context) + .extension()! + .firstGradientTopPanelColor, + Theme.of(context) + .extension()! + .secondGradientTopPanelColor, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ) + : null, child: Observer(builder: (_) { return Padding( padding: EdgeInsets.fromLTRB(24, 120, 24, 0), @@ -143,9 +148,11 @@ class AnonPayInvoicePage extends BasePage { : S.of(context).anonpay_description("a donation link", "donate"), textAlign: TextAlign.center, style: TextStyle( - color: Theme.of(context).extension()!.receiveAmountColor, - fontWeight: FontWeight.w500, - fontSize: 12), + color: Theme.of(context) + .extension()! + .receiveAmountColor, + fontWeight: FontWeight.w500, + fontSize: 12), ), ), ), @@ -172,7 +179,7 @@ class AnonPayInvoicePage extends BasePage { anonInvoicePageViewModel.generateDonationLink(); } }, - color: Theme.of(context).primaryColor, + color: Theme.of(context).primaryColor, textColor: Colors.white, isLoading: anonInvoicePageViewModel.state is IsExecutingState, ), @@ -199,8 +206,12 @@ class AnonPayInvoicePage extends BasePage { final sharedPreferences = getIt.get(); final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink); final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink); + final donationWalletName = + sharedPreferences.getString(PreferencesKey.donationLinkWalletName); - if (clearnetUrl != null && onionUrl != null) { + if (clearnetUrl != null && + onionUrl != null && + anonInvoicePageViewModel.currentWalletName == donationWalletName) { Navigator.pushReplacementNamed(context, Routes.anonPayReceivePage, arguments: AnonpayDonationLinkInfo( clearnetUrl: clearnetUrl, diff --git a/lib/view_model/anon_invoice_page_view_model.dart b/lib/view_model/anon_invoice_page_view_model.dart index 187eea375..39992dca7 100644 --- a/lib/view_model/anon_invoice_page_view_model.dart +++ b/lib/view_model/anon_invoice_page_view_model.dart @@ -150,6 +150,7 @@ abstract class AnonInvoicePageViewModelBase with Store { await sharedPreferences.setString(PreferencesKey.clearnetDonationLink, result.clearnetUrl); await sharedPreferences.setString(PreferencesKey.onionDonationLink, result.onionUrl); + await sharedPreferences.setString(PreferencesKey.donationLinkWalletName, _wallet.name); state = ExecutedSuccessfullyState(payload: result); } @@ -163,10 +164,13 @@ abstract class AnonInvoicePageViewModelBase with Store { maximum = limit.max != null ? limit.max! / 4 : null; } + @computed + String get currentWalletName => _wallet.name; + @action void reset() { selectedCurrency = walletTypeToCryptoCurrency(_wallet.type); - cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type); + cryptoCurrency = walletTypeToCryptoCurrency(_wallet.type); receipientEmail = ''; receipientName = ''; description = ''; @@ -177,7 +181,10 @@ abstract class AnonInvoicePageViewModelBase with Store { Future _getPreviousDonationLink() async { if (pageOption == ReceivePageOption.anonPayDonationLink) { final donationLink = sharedPreferences.getString(PreferencesKey.clearnetDonationLink); - if (donationLink != null) { + final donationLinkWalletName = + sharedPreferences.getString(PreferencesKey.donationLinkWalletName); + + if (donationLink != null && currentWalletName == donationLinkWalletName) { final url = Uri.parse(donationLink); url.queryParameters.forEach((key, value) { if (key == 'name') receipientName = value; From aacd7ce6b33a932d13d918d6714bee81a5d3abd5 Mon Sep 17 00:00:00 2001 From: Mathias Herberts Date: Fri, 21 Jun 2024 18:59:22 +0200 Subject: [PATCH 15/20] FR translation fixes (#1505) * Fixed FR translations * Changed prestataires to fournisseurs --- res/values/strings_fr.arb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 2477c09b1..7289b7511 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -266,7 +266,7 @@ "errorSigningTransaction": "Une erreur s'est produite lors de la signature de la transaction", "estimated": "Estimé", "estimated_new_fee": "De nouveaux frais estimés", - "etherscan_history": "Historique d'Etherscan", + "etherscan_history": "Historique Etherscan", "event": "Événement", "events": "Événements", "exchange": "Échanger", @@ -404,7 +404,7 @@ "node_test": "Tester", "nodes": "Nœuds", "nodes_list_reset_to_default_message": "Êtes vous certain de vouloir revenir aux réglages par défaut ?", - "none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange", + "none_of_selected_providers_can_exchange": "Aucun des fournisseurs sélectionnés ne peut effectuer cet échange", "noNFTYet": "Pas encore de NFT", "normal": "Normal", "note_optional": "Note (optionnelle)", @@ -794,7 +794,7 @@ "unconfirmed": "Solde non confirmé", "understand": "J'ai compris", "unmatched_currencies": "La devise de votre portefeuille (wallet) actuel ne correspond pas à celle du QR code scanné", - "unspent_change": "Changement", + "unspent_change": "Monnaie", "unspent_coins_details_title": "Détails des pièces (coins) non dépensées", "unspent_coins_title": "Pièces (coins) non dépensées", "unsupported_asset": "Nous ne prenons pas en charge cette action pour cet élément. Veuillez créer ou passer à un portefeuille d'un type d'actif pris en charge.", @@ -873,4 +873,4 @@ "you_will_get": "Convertir vers", "you_will_send": "Convertir depuis", "yy": "AA" -} \ No newline at end of file +} From 7dd15914d03bd4ac1a5e4c6459070fea6e6eb3e5 Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Mon, 24 Jun 2024 18:38:32 +0200 Subject: [PATCH 16/20] Normalize text to fix french (#1504) * Normalize text to fix french * Normalize text to fix french * Fix French? * Fix French? * Fix French? * Polyseed v0.0.5 --- cw_bitcoin/lib/bitcoin_mnemonic.dart | 119 +----------------- cw_bitcoin/pubspec.lock | 2 +- cw_bitcoin/pubspec.yaml | 1 - cw_core/lib/utils/text_normalizer.dart | 117 +++++++++++++++++ cw_core/pubspec.lock | 8 ++ cw_core/pubspec.yaml | 1 + cw_monero/pubspec.yaml | 2 +- lib/core/seed_validator.dart | 15 ++- lib/src/widgets/seed_widget.dart | 12 +- .../validable_annotated_editable_text.dart | 9 +- pubspec_base.yaml | 4 +- 11 files changed, 153 insertions(+), 137 deletions(-) create mode 100644 cw_core/lib/utils/text_normalizer.dart diff --git a/cw_bitcoin/lib/bitcoin_mnemonic.dart b/cw_bitcoin/lib/bitcoin_mnemonic.dart index 4a01d6ddc..905aece28 100644 --- a/cw_bitcoin/lib/bitcoin_mnemonic.dart +++ b/cw_bitcoin/lib/bitcoin_mnemonic.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; -import 'package:unorm_dart/unorm_dart.dart' as unorm; import 'package:cryptography/cryptography.dart' as cryptography; import 'package:cw_core/sec_random_native.dart'; +import 'package:cw_core/utils/text_normalizer.dart'; const segwit = '100'; final wordlist = englishWordlist; @@ -137,121 +137,6 @@ bool validateMnemonic(String mnemonic, {String prefix = segwit}) { } } -final COMBININGCODEPOINTS = combiningcodepoints(); - -List combiningcodepoints() { - final source = '300:34e|350:36f|483:487|591:5bd|5bf|5c1|5c2|5c4|5c5|5c7|610:61a|64b:65f|670|' + - '6d6:6dc|6df:6e4|6e7|6e8|6ea:6ed|711|730:74a|7eb:7f3|816:819|81b:823|825:827|' + - '829:82d|859:85b|8d4:8e1|8e3:8ff|93c|94d|951:954|9bc|9cd|a3c|a4d|abc|acd|b3c|' + - 'b4d|bcd|c4d|c55|c56|cbc|ccd|d4d|dca|e38:e3a|e48:e4b|eb8|eb9|ec8:ecb|f18|f19|' + - 'f35|f37|f39|f71|f72|f74|f7a:f7d|f80|f82:f84|f86|f87|fc6|1037|1039|103a|108d|' + - '135d:135f|1714|1734|17d2|17dd|18a9|1939:193b|1a17|1a18|1a60|1a75:1a7c|1a7f|' + - '1ab0:1abd|1b34|1b44|1b6b:1b73|1baa|1bab|1be6|1bf2|1bf3|1c37|1cd0:1cd2|' + - '1cd4:1ce0|1ce2:1ce8|1ced|1cf4|1cf8|1cf9|1dc0:1df5|1dfb:1dff|20d0:20dc|20e1|' + - '20e5:20f0|2cef:2cf1|2d7f|2de0:2dff|302a:302f|3099|309a|a66f|a674:a67d|a69e|' + - 'a69f|a6f0|a6f1|a806|a8c4|a8e0:a8f1|a92b:a92d|a953|a9b3|a9c0|aab0|aab2:aab4|' + - 'aab7|aab8|aabe|aabf|aac1|aaf6|abed|fb1e|fe20:fe2f|101fd|102e0|10376:1037a|' + - '10a0d|10a0f|10a38:10a3a|10a3f|10ae5|10ae6|11046|1107f|110b9|110ba|11100:11102|' + - '11133|11134|11173|111c0|111ca|11235|11236|112e9|112ea|1133c|1134d|11366:1136c|' + - '11370:11374|11442|11446|114c2|114c3|115bf|115c0|1163f|116b6|116b7|1172b|11c3f|' + - '16af0:16af4|16b30:16b36|1bc9e|1d165:1d169|1d16d:1d172|1d17b:1d182|1d185:1d18b|' + - '1d1aa:1d1ad|1d242:1d244|1e000:1e006|1e008:1e018|1e01b:1e021|1e023|1e024|' + - '1e026:1e02a|1e8d0:1e8d6|1e944:1e94a'; - - return source.split('|').map((e) { - if (e.contains(':')) { - return e.split(':').map((hex) => int.parse(hex, radix: 16)); - } - - return int.parse(e, radix: 16); - }).fold([], (List acc, element) { - if (element is List) { - for (var i = element[0] as int; i <= (element[1] as int); i++) {} - } else if (element is int) { - acc.add(element); - } - - return acc; - }).toList(); -} - -String removeCombiningCharacters(String source) { - return source - .split('') - .where((char) => !COMBININGCODEPOINTS.contains(char.codeUnits.first)) - .join(''); -} - -bool isCJK(String char) { - final n = char.codeUnitAt(0); - - for (var x in CJKINTERVALS) { - final imin = x[0] as num; - final imax = x[1] as num; - - if (n >= imin && n <= imax) return true; - } - - return false; -} - -String removeCJKSpaces(String source) { - final splitted = source.split(''); - final filtered = []; - - for (var i = 0; i < splitted.length; i++) { - final char = splitted[i]; - final isSpace = char.trim() == ''; - final prevIsCJK = i != 0 && isCJK(splitted[i - 1]); - final nextIsCJK = i != splitted.length - 1 && isCJK(splitted[i + 1]); - - if (!(isSpace && prevIsCJK && nextIsCJK)) { - filtered.add(char); - } - } - - return filtered.join(''); -} - -String normalizeText(String source) { - final res = - removeCombiningCharacters(unorm.nfkd(source).toLowerCase()).trim().split('/\s+/').join(' '); - - return removeCJKSpaces(res); -} - -const CJKINTERVALS = [ - [0x4e00, 0x9fff, 'CJK Unified Ideographs'], - [0x3400, 0x4dbf, 'CJK Unified Ideographs Extension A'], - [0x20000, 0x2a6df, 'CJK Unified Ideographs Extension B'], - [0x2a700, 0x2b73f, 'CJK Unified Ideographs Extension C'], - [0x2b740, 0x2b81f, 'CJK Unified Ideographs Extension D'], - [0xf900, 0xfaff, 'CJK Compatibility Ideographs'], - [0x2f800, 0x2fa1d, 'CJK Compatibility Ideographs Supplement'], - [0x3190, 0x319f, 'Kanbun'], - [0x2e80, 0x2eff, 'CJK Radicals Supplement'], - [0x2f00, 0x2fdf, 'CJK Radicals'], - [0x31c0, 0x31ef, 'CJK Strokes'], - [0x2ff0, 0x2fff, 'Ideographic Description Characters'], - [0xe0100, 0xe01ef, 'Variation Selectors Supplement'], - [0x3100, 0x312f, 'Bopomofo'], - [0x31a0, 0x31bf, 'Bopomofo Extended'], - [0xff00, 0xffef, 'Halfwidth and Fullwidth Forms'], - [0x3040, 0x309f, 'Hiragana'], - [0x30a0, 0x30ff, 'Katakana'], - [0x31f0, 0x31ff, 'Katakana Phonetic Extensions'], - [0x1b000, 0x1b0ff, 'Kana Supplement'], - [0xac00, 0xd7af, 'Hangul Syllables'], - [0x1100, 0x11ff, 'Hangul Jamo'], - [0xa960, 0xa97f, 'Hangul Jamo Extended A'], - [0xd7b0, 0xd7ff, 'Hangul Jamo Extended B'], - [0x3130, 0x318f, 'Hangul Compatibility Jamo'], - [0xa4d0, 0xa4ff, 'Lisu'], - [0x16f00, 0x16f9f, 'Miao'], - [0xa000, 0xa48f, 'Yi Syllables'], - [0xa490, 0xa4cf, 'Yi Radicals'], -]; - final englishWordlist = [ 'abandon', 'ability', @@ -2301,4 +2186,4 @@ final englishWordlist = [ 'zero', 'zone', 'zoo' -]; \ No newline at end of file +]; diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 997ed9452..ffc224e93 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -863,7 +863,7 @@ packages: source: hosted version: "1.3.2" unorm_dart: - dependency: "direct main" + dependency: transitive description: name: unorm_dart sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 40f3c6e29..66c5729e8 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -28,7 +28,6 @@ dependencies: url: https://github.com/cake-tech/bitbox-flutter.git ref: Add-Support-For-OP-Return-data rxdart: ^0.27.5 - unorm_dart: ^0.2.0 cryptography: ^2.0.5 bitcoin_base: git: diff --git a/cw_core/lib/utils/text_normalizer.dart b/cw_core/lib/utils/text_normalizer.dart new file mode 100644 index 000000000..5aeb5fd21 --- /dev/null +++ b/cw_core/lib/utils/text_normalizer.dart @@ -0,0 +1,117 @@ +import 'package:unorm_dart/unorm_dart.dart' as unorm; + +const CJKINTERVALS = [ + [0x4e00, 0x9fff, 'CJK Unified Ideographs'], + [0x3400, 0x4dbf, 'CJK Unified Ideographs Extension A'], + [0x20000, 0x2a6df, 'CJK Unified Ideographs Extension B'], + [0x2a700, 0x2b73f, 'CJK Unified Ideographs Extension C'], + [0x2b740, 0x2b81f, 'CJK Unified Ideographs Extension D'], + [0xf900, 0xfaff, 'CJK Compatibility Ideographs'], + [0x2f800, 0x2fa1d, 'CJK Compatibility Ideographs Supplement'], + [0x3190, 0x319f, 'Kanbun'], + [0x2e80, 0x2eff, 'CJK Radicals Supplement'], + [0x2f00, 0x2fdf, 'CJK Radicals'], + [0x31c0, 0x31ef, 'CJK Strokes'], + [0x2ff0, 0x2fff, 'Ideographic Description Characters'], + [0xe0100, 0xe01ef, 'Variation Selectors Supplement'], + [0x3100, 0x312f, 'Bopomofo'], + [0x31a0, 0x31bf, 'Bopomofo Extended'], + [0xff00, 0xffef, 'Halfwidth and Fullwidth Forms'], + [0x3040, 0x309f, 'Hiragana'], + [0x30a0, 0x30ff, 'Katakana'], + [0x31f0, 0x31ff, 'Katakana Phonetic Extensions'], + [0x1b000, 0x1b0ff, 'Kana Supplement'], + [0xac00, 0xd7af, 'Hangul Syllables'], + [0x1100, 0x11ff, 'Hangul Jamo'], + [0xa960, 0xa97f, 'Hangul Jamo Extended A'], + [0xd7b0, 0xd7ff, 'Hangul Jamo Extended B'], + [0x3130, 0x318f, 'Hangul Compatibility Jamo'], + [0xa4d0, 0xa4ff, 'Lisu'], + [0x16f00, 0x16f9f, 'Miao'], + [0xa000, 0xa48f, 'Yi Syllables'], + [0xa490, 0xa4cf, 'Yi Radicals'], +]; + +final COMBININGCODEPOINTS = combiningcodepoints(); + +List combiningcodepoints() { + final source = '300:34e|350:36f|483:487|591:5bd|5bf|5c1|5c2|5c4|5c5|5c7|610:61a|64b:65f|670|' + + '6d6:6dc|6df:6e4|6e7|6e8|6ea:6ed|711|730:74a|7eb:7f3|816:819|81b:823|825:827|' + + '829:82d|859:85b|8d4:8e1|8e3:8ff|93c|94d|951:954|9bc|9cd|a3c|a4d|abc|acd|b3c|' + + 'b4d|bcd|c4d|c55|c56|cbc|ccd|d4d|dca|e38:e3a|e48:e4b|eb8|eb9|ec8:ecb|f18|f19|' + + 'f35|f37|f39|f71|f72|f74|f7a:f7d|f80|f82:f84|f86|f87|fc6|1037|1039|103a|108d|' + + '135d:135f|1714|1734|17d2|17dd|18a9|1939:193b|1a17|1a18|1a60|1a75:1a7c|1a7f|' + + '1ab0:1abd|1b34|1b44|1b6b:1b73|1baa|1bab|1be6|1bf2|1bf3|1c37|1cd0:1cd2|' + + '1cd4:1ce0|1ce2:1ce8|1ced|1cf4|1cf8|1cf9|1dc0:1df5|1dfb:1dff|20d0:20dc|20e1|' + + '20e5:20f0|2cef:2cf1|2d7f|2de0:2dff|302a:302f|3099|309a|a66f|a674:a67d|a69e|' + + 'a69f|a6f0|a6f1|a806|a8c4|a8e0:a8f1|a92b:a92d|a953|a9b3|a9c0|aab0|aab2:aab4|' + + 'aab7|aab8|aabe|aabf|aac1|aaf6|abed|fb1e|fe20:fe2f|101fd|102e0|10376:1037a|' + + '10a0d|10a0f|10a38:10a3a|10a3f|10ae5|10ae6|11046|1107f|110b9|110ba|11100:11102|' + + '11133|11134|11173|111c0|111ca|11235|11236|112e9|112ea|1133c|1134d|11366:1136c|' + + '11370:11374|11442|11446|114c2|114c3|115bf|115c0|1163f|116b6|116b7|1172b|11c3f|' + + '16af0:16af4|16b30:16b36|1bc9e|1d165:1d169|1d16d:1d172|1d17b:1d182|1d185:1d18b|' + + '1d1aa:1d1ad|1d242:1d244|1e000:1e006|1e008:1e018|1e01b:1e021|1e023|1e024|' + + '1e026:1e02a|1e8d0:1e8d6|1e944:1e94a'; + + return source.split('|').map((e) { + if (e.contains(':')) { + return e.split(':').map((hex) => int.parse(hex, radix: 16)); + } + + return int.parse(e, radix: 16); + }).fold([], (List acc, element) { + if (element is List) { + for (var i = element[0] as int; i <= (element[1] as int); i++) {} + } else if (element is int) { + acc.add(element); + } + + return acc; + }).toList(); +} + +String _removeCombiningCharacters(String source) { + return source + .split('') + .where((char) => !COMBININGCODEPOINTS.contains(char.codeUnits.first)) + .join(''); +} + +String _removeCJKSpaces(String source) { + final splitted = source.split(''); + final filtered = []; + + for (var i = 0; i < splitted.length; i++) { + final char = splitted[i]; + final isSpace = char.trim() == ''; + final prevIsCJK = i != 0 && _isCJK(splitted[i - 1]); + final nextIsCJK = i != splitted.length - 1 && _isCJK(splitted[i + 1]); + + if (!(isSpace && prevIsCJK && nextIsCJK)) { + filtered.add(char); + } + } + + return filtered.join(''); +} + +bool _isCJK(String char) { + final n = char.codeUnitAt(0); + + for (var x in CJKINTERVALS) { + final imin = x[0] as num; + final imax = x[1] as num; + + if (n >= imin && n <= imax) return true; + } + + return false; +} + +/// This method normalize text which transforms Unicode text into an equivalent decomposed form, allowing for easier sorting and searching of text. +String normalizeText(String source) { + final res = + _removeCombiningCharacters(unorm.nfkd(source).toLowerCase()).trim().split('/\s+/').join(' '); + + return _removeCJKSpaces(res); +} diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index abfdbfc58..88fddae09 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -656,6 +656,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + unorm_dart: + dependency: "direct main" + description: + name: unorm_dart + sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" + url: "https://pub.dev" + source: hosted + version: "0.2.0" vector_math: dependency: transitive description: diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 51d671dc7..0513b122c 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: intl: ^0.18.0 encrypt: ^5.0.1 socks5_proxy: ^1.0.4 + unorm_dart: ^0.3.0 # tor: # git: # url: https://github.com/cake-tech/tor.git diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index c49a541ab..56f5d2fa6 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: flutter_mobx: ^2.0.6+1 intl: ^0.18.0 encrypt: ^5.0.1 - polyseed: ^0.0.2 + polyseed: ^0.0.5 cw_core: path: ../cw_core diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 3e3445757..9fb839ea2 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,15 +1,15 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/utils/language_list.dart'; +import 'package:cw_core/wallet_type.dart'; class SeedValidator extends Validator { SeedValidator({required this.type, required this.language}) @@ -41,13 +41,16 @@ class SeedValidator extends Validator { return polygon!.getPolygonWordList(language); case WalletType.solana: return solana!.getSolanaWordList(language); - case WalletType.tron: + case WalletType.tron: return tron!.getTronWordList(language); default: return []; } } + static bool needsNormalization(String language) => + ["POLYSEED_French", "POLYSEED_Spanish"].contains(language); + static List getBitcoinWordList(String language) { assert(language.toLowerCase() == LanguageList.english.toLowerCase()); return bitcoin!.getWordList(); diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index bf9a85b32..d71208bb2 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -1,11 +1,11 @@ +import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cake_wallet/core/seed_validator.dart'; -import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; class SeedWidget extends StatefulWidget { SeedWidget({ @@ -23,7 +23,6 @@ class SeedWidget extends StatefulWidget { } class SeedWidgetState extends State { - SeedWidgetState(String language, this.type) : controller = TextEditingController(), focusNode = FocusNode(), @@ -46,6 +45,7 @@ class SeedWidgetState extends State { final FocusNode focusNode; final WalletType type; List words; + bool normalizeSeed = false; bool _showPlaceholder; String get text => controller.text; @@ -60,6 +60,7 @@ class SeedWidgetState extends State { void changeSeedLanguage(String language) { setState(() { words = SeedValidator.getWordList(type: type, language: language); + normalizeSeed = SeedValidator.needsNormalization(language); }); } @@ -97,6 +98,7 @@ class SeedWidgetState extends State { focusNode: focusNode, controller: controller, words: words, + normalizeSeed: normalizeSeed, textStyle: TextStyle( color: Theme.of(context).extension()!.titleColor, backgroundColor: Colors.transparent, diff --git a/lib/src/widgets/validable_annotated_editable_text.dart b/lib/src/widgets/validable_annotated_editable_text.dart index 134eb16a8..a7777961d 100644 --- a/lib/src/widgets/validable_annotated_editable_text.dart +++ b/lib/src/widgets/validable_annotated_editable_text.dart @@ -1,6 +1,6 @@ +import 'package:cw_core/utils/text_normalizer.dart'; import 'package:flutter/material.dart'; - extension Compare on Comparable { bool operator <=(T other) => compareTo(other) <= 0; bool operator >=(T other) => compareTo(other) >= 0; @@ -39,6 +39,7 @@ class ValidatableAnnotatedEditableText extends EditableText { required this.validStyle, required this.invalidStyle, required this.words, + this.normalizeSeed = false, TextStyle textStyle = const TextStyle( color: Colors.black, backgroundColor: Colors.transparent, @@ -74,6 +75,7 @@ class ValidatableAnnotatedEditableText extends EditableText { showSelectionHandles: true, showCursor: true); + final bool normalizeSeed; final List words; final TextStyle validStyle; final TextStyle invalidStyle; @@ -137,7 +139,8 @@ class ValidatableAnnotatedEditableTextState extends EditableTextState { return result; } - bool validate(String source) => widget.words.indexOf(source) >= 0; + bool validate(String source) => + widget.words.indexOf(widget.normalizeSeed ? normalizeText(source) : source) >= 0; List range(String pattern, String source) { final result = []; @@ -173,4 +176,4 @@ class ValidatableAnnotatedEditableTextState extends EditableTextState { return TextSpan(style: widget.style, text: text); } -} \ No newline at end of file +} diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 47407f833..e00527d9f 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -60,8 +60,6 @@ dependencies: git: url: https://github.com/cake-tech/flutter_file_picker.git ref: master - unorm_dart: ^0.2.0 - # check unorm_dart for usage and for replace permission_handler: ^10.0.0 device_display_brightness: git: @@ -100,7 +98,7 @@ dependencies: # ref: main socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 - polyseed: ^0.0.4 + polyseed: ^0.0.5 nostr_tools: ^1.0.9 solana: ^0.30.1 bitcoin_base: From 8f91d4b8ff583dac01800142070fb1a346d5b79a Mon Sep 17 00:00:00 2001 From: Konstantin Ullrich Date: Tue, 25 Jun 2024 18:52:31 +0200 Subject: [PATCH 17/20] oa1 ticker needs to be lowercase --- lib/entities/parse_address_from_domain.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 409724c6e..c95ce9847 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -201,7 +201,7 @@ class AddressResolver { final txtRecord = await OpenaliasRecord.lookupOpenAliasRecord(formattedName); if (txtRecord != null) { final record = await OpenaliasRecord.fetchAddressAndName( - formattedName: formattedName, ticker: ticker, txtRecord: txtRecord); + formattedName: formattedName, ticker: ticker.toLowerCase(), txtRecord: txtRecord); return ParsedAddress.fetchOpenAliasAddress(record: record, name: text); } } From a319e101564ec9023b4807ccafa3e61bb602549f Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 28 Jun 2024 22:36:12 +0100 Subject: [PATCH 18/20] CW-653-Migrate-Tron-And-Solana-To-NowNodes (#1492) * chore: Setup * feat: Add NowNodes for Tron Wallet and switch it to be the default node for Tron * feat: Add NowNodes for Solana Wallet and switch it to be the default node for Solana * fix: Add nownodes entry to secrets * fix: Remove pubspec.lock in shared external * fix conflicts with main * change secrets names * feat: Remove Solana NowNodes config * feat: Remove Solana NowNodes config * feat: Revert commented out code --------- Co-authored-by: OmarHatem --- .github/workflows/pr_test_build.yml | 1 + assets/tron_node_list.yml | 4 +++ cw_tron/lib/tron_http_provider.dart | 2 ++ devtools_options.yaml | 1 + ios/Runner.xcodeproj/project.pbxproj | 8 ++--- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/entities/default_settings_migration.dart | 32 ++++++++++++++++++- lib/main.dart | 2 +- tool/utils/secret_key.dart | 7 +++- 9 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 devtools_options.yaml diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 841ea570d..5c8ea82a7 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -158,6 +158,7 @@ jobs: echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart + echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart - name: Rename app run: | diff --git a/assets/tron_node_list.yml b/assets/tron_node_list.yml index b12a82dbe..f9fd91179 100644 --- a/assets/tron_node_list.yml +++ b/assets/tron_node_list.yml @@ -4,5 +4,9 @@ useSSL: true - uri: api.trongrid.io + is_default: false + useSSL: true +- + uri: trx.nownodes.io is_default: true useSSL: true \ No newline at end of file diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart index 58d313378..8a3301f87 100644 --- a/cw_tron/lib/tron_http_provider.dart +++ b/cw_tron/lib/tron_http_provider.dart @@ -20,6 +20,7 @@ class TronHTTPProvider implements TronServiceProvider { final response = await client.get(Uri.parse(params.url(url)), headers: { 'Content-Type': 'application/json', if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, }).timeout(timeout ?? defaultRequestTimeout); final data = json.decode(response.body) as Map; return data; @@ -32,6 +33,7 @@ class TronHTTPProvider implements TronServiceProvider { headers: { 'Content-Type': 'application/json', if (url.contains("trongrid")) 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + if (url.contains("nownodes")) 'api-key': secrets.tronNowNodesApiKey, }, body: params.toRequestBody()) .timeout(timeout ?? defaultRequestTimeout); diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..7e7e7f67d --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8ed46a028..2196bc289 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -164,7 +164,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -355,7 +355,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -449,7 +449,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -499,7 +499,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a33..5e31d3d34 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ defaultSettingsMigration( @@ -234,7 +234,11 @@ Future defaultSettingsMigration( await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: + await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); + break; + case 38: await fixBtcDerivationPaths(walletInfoSource); + break; default: break; } @@ -1142,3 +1146,29 @@ Future changeTronCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); } + +Future replaceTronDefaultNode({ + required SharedPreferences sharedPreferences, + required Box nodes, +}) async { + // Get the currently active node + final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); + final currentTronNode = + nodes.values.firstWhereOrNull((Node node) => node.key == currentTronNodeId); + + //Confirm if this node is part of the default nodes from CakeWallet + final tronDefaultNodeList = [ + 'tron-rpc.publicnode.com:443', + 'api.trongrid.io', + ]; + bool needsToBeReplaced = + currentTronNode == null ? true : tronDefaultNodeList.contains(currentTronNode.uriRaw); + + // If it's a custom node, return. We don't want to switch users from their custom nodes + if (!needsToBeReplaced) { + return; + } + + // If it's not, we switch user to the new default node: NowNodes + await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 46bd7c608..fbe77bd31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -202,7 +202,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 37, + initialMigrationVersion: 38, ); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 9559e83b3..7261478a6 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -39,6 +39,10 @@ class SecretKey { SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), SecretKey('quantexExchangeMarkup', () => ''), + SecretKey('testCakePayApiKey', () => ''), + SecretKey('cakePayApiKey', () => ''), + SecretKey('CSRFToken', () => ''), + SecretKey('authorization', () => ''), ]; static final evmChainsSecrets = [ @@ -54,9 +58,10 @@ class SecretKey { static final nanoSecrets = [ SecretKey('nano2ApiKey', () => ''), ]; - + static final tronSecrets = [ SecretKey('tronGridApiKey', () => ''), + SecretKey('tronNowNodesApiKey', () => ''), ]; final String name; From 36eacd869808d86a3a504186c9dbb1034882c7f9 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 4 Jul 2024 22:43:17 +0300 Subject: [PATCH 19/20] Wownero (#1485) * fix: scanning issues * fix: sync, storing silent unspents * chore: deps * fix: label issues, clear spent utxo * chore: deps * fix: build * fix: missing types * feat: new electrs API & changes, fixes for last block scanning * feat: Scan Silent Payments homepage toggle * chore: build configure * feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes * fix: invalid Object in sendData * feat: improve addresses page & address book displays * feat: silent payments labeled addresses disclaimer * fix: missing i18n * chore: print * feat: single block scan, rescan by date working for btc mainnet * feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert * feat: delete silent addresses * fix: red dot in non ssl nodes * fix: inconsistent connection states, fix tx history * fix: tx & balance displays, cpfp sending * feat: new rust lib * chore: node path * fix: check node based on network * fix: missing txcount from addresses * style: padding in feature page cards * fix: restore not getting all wallet addresses by type * fix: auto switch node broken * fix: silent payment txs not being restored * feat: change scanning to subscription model, sync improvements * fix: scan re-subscription * fix: default nodes * fix: improve scanning by date, fix single block scan * refactor: common function for input tx selection * various fixes for build issues * initial monero.dart implementation * ... * multiple wallets new lib minor fixes * other fixes from monero.dart and monero_c * fix: nodes & build * update build scripts fix polyseed * remove unnecessary code * Add windows app, build scripts and build guide for it. * Minor fix in generated monero configs * fix: send all with multiple outs * add missing monero_c command * add android build script * Merge and fix main * undo android ndk removal * Fix modified exception_handler.dart * Temporarily remove haven * fix build issues * fix pr script * Fixes for build monero.dart (monero_c) for windows. * monero build script * wip: ios build script * refactor: unchanged file * Added build guides for iOS and macOS. Replaced nproc call on macOS. Added macOS configuration for configure_cake_wallet.sh script. * Update monero.dart and monero_c versions. * Add missed windows build scripts * Update the application configuration for windows build script. * Update cw_monero pubspec lock file for monero.dart * Update pr_test_build.yml * chore: upgrade * chore: merge changes * refactor: unchanged files [skip ci] * Fix conflicts with main * fix for multiple wallets * Add tron to windows application configuration. * Add macOS option for description message in configure_cake_wallet.sh * Include missed monero dll for windows. * fix conflicts with main * Disable haven configuration for iOS as default. Add ability to configure cakewallet for iOS with for configuration script. Remove cw_shared configuration for cw_monero. * fix: scan fixes, add date, allow sending while scanning * add missing nano secrets file [skip ci] * ios library * don't pull prebuilds android * Add auto generation of manifest file for android project even for iOS, macOS, Windows. * feat: sync fixes, sp settings * feat: fix resyncing * store crash fix * make init async so it won't lag disable print starts * fix monero_c build issues * libstdc++ * Fix MacOS saving wallet file issue Fix Secure Storage issue (somehow) * update pubspec.lock * fix build script * Use dylib as iOS framework. Use custom path for loading of iOS framework for monero.dart. Add script for generate iOS framework for monero wallet. * fix: date from height logic, status disconnected & chain tip get * fix: params * feat: electrum migration if using cake electrum * fix nodes update versions * re-enable tron * update sp_scanner to work on iOS [skip ci] * bump monero_c hash * bump monero_c commit * bump moneroc version * bump monero_c commit * Add ability to build monero wallet lib as universal lib. Update macOS build guide. Change default arch for macOS project to . * fix: wrong socket for old electrum nodes * Fix unchecked wallet type call * get App Dir correctly in default_settings_migration.dart * handle previous issue with fetching linux documents directory [skip ci] * backup fix * fix NTFS issues * Close the wallet when the wallet gets changed * fix: double balance * feat: node domain * fix: menu name * bump monero_c commit * fix: update tip on set scanning * fix: connection switching back and forth * feat: check if node is electrs, and supports sp * chore: fix build * minor enhancements * fixes and enhancements * solve conflicts with main * Only stop wallet on rename and delete * fix: status toggle * minor enhancement * Monero.com fixes * bump monero_c commit * update sp_scanner to include windows and linux * Update macOS build guide. Change brew dependencies for build unbound locally. * fix conflicts and update macos build guide * remove build cache when on gh actions * update secure storage * free up even more storage * free up more storage * Add initial wownero * fix conflicts * fix workflow issue * build wownero * ios and windows changes * macos * complete wownero flow (app side) * add keychain group entitlement and update script for RunnerBase on macos * update secure_storage version to 8.1.0 in configure.dart * add wownero framework * update ios builds * proper path for wownero and monero * finalizing wownero * finalizing wownero * free up even more storage * revert commenting of build gradle configs * revert commenting of secrets [skip ci] * free more storage * minor fixes * link android wownero libraries * bump monero_c commit * wownero fixes * rename target * build_single.sh using clean env * bump monero_c commit * minor fix * Add wownero polyseed * fix conflicts with main * fix: wallet seed display fix: wownero not refreshing * fix: wallet seed display fix: wownero not refreshing * bump monero_c commit * minor fixes * fix: incorrectly displaying XMR instead of WOW * fix: incorrect restore height in wownero * bump monero_c commit * Add Inno Setup Script for windows exe installer * drop libc++_shared.so * fixes from comments * Fix CMake for windows * Merge latest monero dart changes [skip ci] * bump monero_c commit * add wownero to build scripts for macos [skip ci] * add 14 word seed support to wownero * UI fixes for wownero seed restore * minor fixes * reformat code to pass lints * wownero: fixes haven: removal popup * minor iOS fix [skip ci] * fix: wownero confirmation count (it is spendable after 3 confirms) fix: transaction history not displaying in WOW and XMR when tx has 0 confirms, This is more of a workaround, because I have no idea why would the cpp code not return pending transaction. * Update preferences_key.dart [skip ci] * minor fixes --------- Co-authored-by: Rafael Saes Co-authored-by: Czarek Nakamoto Co-authored-by: M Co-authored-by: Konstantin Ullrich Co-authored-by: Matthew Fosse --- .github/workflows/cache_dependencies.yml | 1 + .github/workflows/pr_test_build.yml | 24 +- .gitignore | 8 + .metadata | 16 +- android/app/src/main/AndroidManifestBase.xml | 6 +- .../app/src/main/jniLibs/arm64-v8a}/.gitkeep | 0 .../arm64-v8a/libmonero_libwallet2_api_c.so | 1 + .../arm64-v8a/libwownero_libwallet2_api_c.so | 1 + .../app/src/main/jniLibs/armeabi-v7a/.gitkeep | 0 .../armeabi-v7a/libmonero_libwallet2_api_c.so | 1 + .../libwownero_libwallet2_api_c.so | 1 + android/app/src/main/jniLibs/x86/.gitkeep | 0 android/app/src/main/jniLibs/x86_64/.gitkeep | 0 .../x86_64/libmonero_libwallet2_api_c.so | 1 + .../x86_64/libwownero_libwallet2_api_c.so | 1 + assets/bitcoin_electrum_server_list.yml | 8 +- assets/images/wownero_icon.png | Bin 0 -> 50010 bytes assets/images/wownero_menu.png | Bin 0 -> 50010 bytes assets/text/Monerocom_Release_Notes.txt | 4 +- assets/text/Release_Notes.txt | 5 +- assets/wownero_node_list.yml | 12 + build-guide-win.md | 38 + cakewallet.bat | 51 + configure_cake_wallet.sh | 15 +- cw_core/lib/amount_converter.dart | 103 +- cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/currency_for_wallet_type.dart | 4 +- cw_core/lib/get_height_by_date.dart | 78 + cw_core/lib/node.dart | 6 +- cw_core/lib/pathForWallet.dart | 7 +- cw_core/lib/root_dir.dart | 35 + cw_core/lib/sec_random_native.dart | 8 + cw_core/lib/wallet_type.dart | 23 +- cw_core/lib/wownero_amount_format.dart | 18 + cw_core/lib/wownero_balance.dart | 38 + cw_core/pubspec.lock | 4 +- cw_haven/lib/api/account_list.dart | 6 +- cw_monero/android/.classpath | 6 - cw_monero/android/.gitignore | 8 - cw_monero/android/.project | 23 - .../org.eclipse.buildship.core.prefs | 13 - cw_monero/android/CMakeLists.txt | 232 --- cw_monero/android/build.gradle | 49 - cw_monero/android/gradle.properties | 4 - .../gradle/wrapper/gradle-wrapper.properties | 5 - cw_monero/android/jni/monero_jni.cpp | 74 - cw_monero/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 4 - .../com/cakewallet/monero/CwMoneroPlugin.kt | 74 - cw_monero/example/.gitignore | 44 - cw_monero/example/README.md | 16 - cw_monero/example/analysis_options.yaml | 29 - cw_monero/example/lib/main.dart | 63 - cw_monero/example/macos/.gitignore | 7 - .../macos/Flutter/Flutter-Debug.xcconfig | 2 - .../macos/Flutter/Flutter-Release.xcconfig | 2 - .../Flutter/GeneratedPluginRegistrant.swift | 14 - cw_monero/example/macos/Podfile | 40 - cw_monero/example/macos/Podfile.lock | 22 - .../macos/Runner.xcodeproj/project.pbxproj | 632 ------ .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 - .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../example/macos/Runner/AppDelegate.swift | 9 - .../AppIcon.appiconset/Contents.json | 68 - .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 0 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 0 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 0 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 0 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 0 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 0 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 0 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ---- .../macos/Runner/Configs/AppInfo.xcconfig | 14 - .../macos/Runner/Configs/Debug.xcconfig | 2 - .../macos/Runner/Configs/Release.xcconfig | 2 - .../macos/Runner/Configs/Warnings.xcconfig | 13 - cw_monero/example/macos/Runner/Info.plist | 32 - .../macos/Runner/MainFlutterWindow.swift | 15 - .../example/macos/Runner/Release.entitlements | 8 - cw_monero/example/pubspec.yaml | 84 - cw_monero/example/test/widget_test.dart | 27 - cw_monero/ios/.gitignore | 37 - cw_monero/ios/Classes/CwMoneroPlugin.h | 4 - cw_monero/ios/Classes/CwMoneroPlugin.m | 8 - cw_monero/ios/Classes/CwWalletListener.h | 23 - .../ios/Classes/SwiftCwMoneroPlugin.swift | 14 - cw_monero/ios/Classes/monero_api.cpp | 1044 ---------- cw_monero/ios/Classes/monero_api.h | 39 - cw_monero/ios/cw_monero.podspec | 62 - cw_monero/lib/api/account_list.dart | 70 +- cw_monero/lib/api/coins_info.dart | 40 +- cw_monero/lib/api/convert_utf8_to_string.dart | 8 - cw_monero/lib/api/monero_api.dart | 6 - cw_monero/lib/api/signatures.dart | 160 -- .../lib/api/structs/pending_transaction.dart | 22 - cw_monero/lib/api/subaddress_list.dart | 87 +- cw_monero/lib/api/transaction_history.dart | 401 ++-- cw_monero/lib/api/types.dart | 160 -- cw_monero/lib/api/wallet.dart | 368 ++-- cw_monero/lib/api/wallet_manager.dart | 312 ++- cw_monero/lib/monero_account_list.dart | 11 +- cw_monero/lib/monero_subaddress_list.dart | 49 +- cw_monero/lib/monero_wallet.dart | 228 ++- cw_monero/lib/monero_wallet_service.dart | 19 + cw_monero/macos/Classes/CwMoneroPlugin.swift | 19 - cw_monero/macos/Classes/CwWalletListener.h | 23 - cw_monero/macos/Classes/monero_api.cpp | 1032 ---------- cw_monero/macos/Classes/monero_api.h | 39 - cw_monero/macos/cw_monero_base.podspec | 56 - cw_monero/pubspec.lock | 25 +- cw_monero/pubspec.yaml | 16 +- .../test/cw_monero_method_channel_test.dart | 24 - cw_monero/test/cw_monero_test.dart | 29 - cw_wownero/.gitignore | 5 + cw_wownero/.metadata | 30 + cw_wownero/CHANGELOG.md | 3 + cw_wownero/LICENSE | 21 + cw_wownero/README.md | 5 + cw_wownero/analysis_options.yaml | 4 + cw_wownero/lib/api/account_list.dart | 72 + cw_wownero/lib/api/coins_info.dart | 17 + .../connection_to_node_exception.dart | 5 + .../creation_transaction_exception.dart | 8 + .../exceptions/setup_wallet_exception.dart | 5 + .../exceptions/wallet_creation_exception.dart | 8 + .../exceptions/wallet_opening_exception.dart | 8 + .../wallet_restore_from_keys_exception.dart | 5 + .../wallet_restore_from_seed_exception.dart | 5 + cw_wownero/lib/api/structs/account_row.dart | 12 + .../lib/api/structs/coins_info_row.dart | 73 + .../lib/api/structs/pending_transaction.dart | 17 + .../lib/api/structs/subaddress_row.dart | 15 + .../lib/api/structs/transaction_info_row.dart | 41 + cw_wownero/lib/api/structs/ut8_box.dart | 8 + cw_wownero/lib/api/subaddress_list.dart | 91 + cw_wownero/lib/api/transaction_history.dart | 324 ++++ cw_wownero/lib/api/wallet.dart | 295 +++ cw_wownero/lib/api/wallet_manager.dart | 359 ++++ cw_wownero/lib/api/wownero_output.dart | 6 + cw_wownero/lib/cw_wownero.dart | 8 + cw_wownero/lib/cw_wownero_method_channel.dart | 17 + .../lib/cw_wownero_platform_interface.dart | 29 + ...ownero_transaction_creation_exception.dart | 8 + ...wnero_transaction_no_inputs_exception.dart | 8 + .../lib/mnemonics/chinese_simplified.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/dutch.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/english.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/french.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/german.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/italian.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/japanese.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/portuguese.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/russian.dart | 1630 ++++++++++++++++ cw_wownero/lib/mnemonics/spanish.dart | 1630 ++++++++++++++++ cw_wownero/lib/mywownero.dart | 1689 +++++++++++++++++ .../lib/pending_wownero_transaction.dart | 53 + cw_wownero/lib/wownero_account_list.dart | 81 + cw_wownero/lib/wownero_subaddress_list.dart | 162 ++ ...nero_transaction_creation_credentials.dart | 9 + .../lib/wownero_transaction_history.dart | 28 + cw_wownero/lib/wownero_transaction_info.dart | 80 + cw_wownero/lib/wownero_unspent.dart | 20 + cw_wownero/lib/wownero_wallet.dart | 747 ++++++++ cw_wownero/lib/wownero_wallet_addresses.dart | 119 ++ cw_wownero/lib/wownero_wallet_service.dart | 354 ++++ .../example => cw_wownero}/pubspec.lock | 378 +++- cw_wownero/pubspec.yaml | 82 + env.json | 6 + how_to_add_new_wallet_type.md | 6 +- howto-build-android.md | 10 +- howto-build-ios.md | 101 + howto-build-macos.md | 112 ++ ios/MoneroWallet.framework/Info.plist | Bin 0 -> 793 bytes ios/Podfile.lock | 89 +- ios/Runner.xcodeproj/project.pbxproj | 98 +- ios/Runner/InfoBase.plist | 22 +- ios/WowneroWallet.framework/Info.plist | Bin 0 -> 811 bytes ios/monero_libwallet2_api_c.dylib | 1 + ios/wownero_libwallet2_api_c.dylib | 1 + lib/core/address_validator.dart | 3 + lib/core/backup_service.dart | 15 +- lib/core/seed_validator.dart | 5 +- lib/core/wallet_creation_service.dart | 1 + lib/di.dart | 6 +- lib/entities/background_tasks.dart | 16 +- lib/entities/default_settings_migration.dart | 60 +- lib/entities/language_service.dart | 15 +- lib/entities/node_list.dart | 17 + lib/entities/preferences_key.dart | 2 + lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/provider_types.dart | 2 + lib/entities/seed_type.dart | 5 + lib/main.dart | 57 +- lib/monero/cw_monero.dart | 5 + lib/reactions/on_current_wallet_change.dart | 1 + lib/src/screens/dashboard/dashboard_page.dart | 23 +- .../desktop_wallet_selection_dropdown.dart | 1 + .../dashboard/widgets/menu_widget.dart | 6 +- .../screens/new_wallet/new_wallet_page.dart | 2 +- lib/src/screens/rescan/rescan_page.dart | 1 + .../wallet_restore_from_keys_form.dart | 1 + .../wallet_restore_from_seed_form.dart | 52 +- .../screens/restore/wallet_restore_page.dart | 20 +- .../screens/wallet_list/wallet_list_page.dart | 6 +- lib/src/widgets/blockchain_height_widget.dart | 12 +- .../widgets/haven_wallet_removal_popup.dart | 91 + lib/store/settings_store.dart | 35 +- lib/utils/distribution_info.dart | 2 +- lib/utils/exception_handler.dart | 8 +- lib/utils/package_info.dart | 54 + .../advanced_privacy_settings_view_model.dart | 3 +- lib/view_model/backup_view_model.dart | 6 +- .../dashboard/balance_view_model.dart | 2 + .../dashboard/dashboard_view_model.dart | 6 + .../dashboard/transaction_list_item.dart | 19 +- .../exchange/exchange_view_model.dart | 12 +- .../node_create_or_edit_view_model.dart | 1 + .../node_list/node_list_view_model.dart | 3 + .../restore/restore_from_qr_vm.dart | 19 +- .../restore/wallet_restore_from_qr_code.dart | 10 +- lib/view_model/send/output.dart | 11 + lib/view_model/send/send_view_model.dart | 6 + .../settings/other_settings_view_model.dart | 44 +- .../settings/privacy_settings_view_model.dart | 1 + .../transaction_details_view_model.dart | 52 +- .../unspent_coins_list_view_model.dart | 7 + .../wallet_address_list_view_model.dart | 35 + lib/view_model/wallet_creation_vm.dart | 9 +- lib/view_model/wallet_keys_view_model.dart | 148 +- lib/view_model/wallet_new_vm.dart | 13 +- lib/view_model/wallet_restore_view_model.dart | 40 +- lib/wallet_type_utils.dart | 6 +- lib/wownero/cw_wownero.dart | 347 ++++ macos/Flutter/GeneratedPluginRegistrant.swift | 4 - macos/Podfile.lock | 21 - macos/Runner.xcodeproj/project.pbxproj | 89 +- .../Runner/RunnerBase.entitlements | 6 +- macos/monero_libwallet2_api_c.dylib | 1 + macos/wownero_libwallet2_api_c.dylib | 1 + model_generator.sh | 1 + pubspec_base.yaml | 1 + res/values/strings_en.arb | 1 + run-android.sh | 6 +- scripts/android/app_config.sh | 2 +- scripts/android/build_all.sh | 2 +- scripts/android/build_monero.sh | 68 - scripts/android/build_monero_all.sh | 62 +- scripts/android/build_openssl.sh | 3 +- scripts/android/build_unbound.sh | 4 +- scripts/android/copy_monero_deps.sh | 1 - scripts/android/init_boost.sh | 4 +- scripts/android/inject_app_details.sh | 1 + scripts/android/install_ndk.sh | 6 +- scripts/android/manifest.sh | 9 +- scripts/android/pubspec_gen.sh | 5 +- scripts/docker/.gitignore | 1 + scripts/docker/Dockerfile | 42 +- scripts/docker/build_all.sh | 18 +- scripts/docker/build_boost.sh | 2 +- scripts/docker/build_haven.sh | 72 +- scripts/docker/build_haven_all.sh | 10 +- scripts/docker/build_iconv.sh | 1 + scripts/docker/build_monero.sh | 1 + scripts/docker/build_openssl.sh | 1 + scripts/docker/build_sodium.sh | 1 + scripts/docker/build_unbound.sh | 2 +- scripts/docker/build_zmq.sh | 1 + scripts/docker/config.sh | 1 + scripts/docker/copy_haven_deps.sh | 1 + scripts/docker/copy_monero_deps.sh | 1 + scripts/docker/docker-compose.yml | 2 + scripts/docker/entrypoint.sh | 7 + scripts/docker/finish_boost.sh | 1 + scripts/docker/init_boost.sh | 6 +- scripts/docker/install_ndk.sh | 1 + scripts/gen_android_manifest.sh | 10 + scripts/ios/app_config.sh | 12 +- scripts/ios/app_env.sh | 8 +- scripts/ios/build_monero_all.sh | 31 +- scripts/ios/build_openssl.sh | 4 +- scripts/ios/gen_framework.sh | 31 + scripts/macos/app_config.sh | 10 +- scripts/macos/build_monero_all.sh | 69 +- scripts/prepare_moneroc.sh | 31 + scripts/windows/build_all.sh | 37 + scripts/windows/build_exe_installer.iss | 44 + scripts/windows/cakewallet.sh | 7 + tool/configure.dart | 268 ++- tool/import_secrets_config.dart | 1 + windows/.gitignore | 17 + windows/CMakeLists.txt | 123 ++ windows/flutter/CMakeLists.txt | 109 ++ .../flutter/generated_plugin_registrant.cc | 26 + windows/flutter/generated_plugin_registrant.h | 15 + windows/flutter/generated_plugins.cmake | 29 + windows/runner/CMakeLists.txt | 40 + windows/runner/Runner.rc | 121 ++ windows/runner/flutter_window.cpp | 71 + windows/runner/flutter_window.h | 33 + windows/runner/main.cpp | 43 + windows/runner/resource.h | 16 + windows/runner/resources/app_icon.ico | Bin 0 -> 17470 bytes windows/runner/runner.exe.manifest | 20 + windows/runner/utils.cpp | 65 + windows/runner/utils.h | 19 + windows/runner/win32_window.cpp | 288 +++ windows/runner/win32_window.h | 102 + 309 files changed, 26261 insertions(+), 6351 deletions(-) rename {cw_monero/ios/Assets => android/app/src/main/jniLibs/arm64-v8a}/.gitkeep (100%) create mode 120000 android/app/src/main/jniLibs/arm64-v8a/libmonero_libwallet2_api_c.so create mode 120000 android/app/src/main/jniLibs/arm64-v8a/libwownero_libwallet2_api_c.so create mode 100644 android/app/src/main/jniLibs/armeabi-v7a/.gitkeep create mode 120000 android/app/src/main/jniLibs/armeabi-v7a/libmonero_libwallet2_api_c.so create mode 120000 android/app/src/main/jniLibs/armeabi-v7a/libwownero_libwallet2_api_c.so create mode 100644 android/app/src/main/jniLibs/x86/.gitkeep create mode 100644 android/app/src/main/jniLibs/x86_64/.gitkeep create mode 120000 android/app/src/main/jniLibs/x86_64/libmonero_libwallet2_api_c.so create mode 120000 android/app/src/main/jniLibs/x86_64/libwownero_libwallet2_api_c.so create mode 100644 assets/images/wownero_icon.png create mode 100644 assets/images/wownero_menu.png create mode 100644 assets/wownero_node_list.yml create mode 100644 build-guide-win.md create mode 100644 cakewallet.bat create mode 100644 cw_core/lib/root_dir.dart create mode 100644 cw_core/lib/wownero_amount_format.dart create mode 100644 cw_core/lib/wownero_balance.dart delete mode 100644 cw_monero/android/.classpath delete mode 100644 cw_monero/android/.gitignore delete mode 100644 cw_monero/android/.project delete mode 100644 cw_monero/android/.settings/org.eclipse.buildship.core.prefs delete mode 100644 cw_monero/android/CMakeLists.txt delete mode 100644 cw_monero/android/build.gradle delete mode 100644 cw_monero/android/gradle.properties delete mode 100644 cw_monero/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 cw_monero/android/jni/monero_jni.cpp delete mode 100644 cw_monero/android/settings.gradle delete mode 100644 cw_monero/android/src/main/AndroidManifest.xml delete mode 100644 cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt delete mode 100644 cw_monero/example/.gitignore delete mode 100644 cw_monero/example/README.md delete mode 100644 cw_monero/example/analysis_options.yaml delete mode 100644 cw_monero/example/lib/main.dart delete mode 100644 cw_monero/example/macos/.gitignore delete mode 100644 cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig delete mode 100644 cw_monero/example/macos/Flutter/Flutter-Release.xcconfig delete mode 100644 cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 cw_monero/example/macos/Podfile delete mode 100644 cw_monero/example/macos/Podfile.lock delete mode 100644 cw_monero/example/macos/Runner.xcodeproj/project.pbxproj delete mode 100644 cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100644 cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata delete mode 100644 cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 cw_monero/example/macos/Runner/AppDelegate.swift delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png delete mode 100644 cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png delete mode 100644 cw_monero/example/macos/Runner/Base.lproj/MainMenu.xib delete mode 100644 cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig delete mode 100644 cw_monero/example/macos/Runner/Configs/Debug.xcconfig delete mode 100644 cw_monero/example/macos/Runner/Configs/Release.xcconfig delete mode 100644 cw_monero/example/macos/Runner/Configs/Warnings.xcconfig delete mode 100644 cw_monero/example/macos/Runner/Info.plist delete mode 100644 cw_monero/example/macos/Runner/MainFlutterWindow.swift delete mode 100644 cw_monero/example/macos/Runner/Release.entitlements delete mode 100644 cw_monero/example/pubspec.yaml delete mode 100644 cw_monero/example/test/widget_test.dart delete mode 100644 cw_monero/ios/.gitignore delete mode 100644 cw_monero/ios/Classes/CwMoneroPlugin.h delete mode 100644 cw_monero/ios/Classes/CwMoneroPlugin.m delete mode 100644 cw_monero/ios/Classes/CwWalletListener.h delete mode 100644 cw_monero/ios/Classes/SwiftCwMoneroPlugin.swift delete mode 100644 cw_monero/ios/Classes/monero_api.cpp delete mode 100644 cw_monero/ios/Classes/monero_api.h delete mode 100644 cw_monero/ios/cw_monero.podspec delete mode 100644 cw_monero/lib/api/convert_utf8_to_string.dart delete mode 100644 cw_monero/lib/api/monero_api.dart delete mode 100644 cw_monero/lib/api/signatures.dart delete mode 100644 cw_monero/lib/api/types.dart delete mode 100644 cw_monero/macos/Classes/CwMoneroPlugin.swift delete mode 100644 cw_monero/macos/Classes/CwWalletListener.h delete mode 100644 cw_monero/macos/Classes/monero_api.cpp delete mode 100644 cw_monero/macos/Classes/monero_api.h delete mode 100644 cw_monero/macos/cw_monero_base.podspec delete mode 100644 cw_monero/test/cw_monero_method_channel_test.dart delete mode 100644 cw_monero/test/cw_monero_test.dart create mode 100644 cw_wownero/.gitignore create mode 100644 cw_wownero/.metadata create mode 100644 cw_wownero/CHANGELOG.md create mode 100644 cw_wownero/LICENSE create mode 100644 cw_wownero/README.md create mode 100644 cw_wownero/analysis_options.yaml create mode 100644 cw_wownero/lib/api/account_list.dart create mode 100644 cw_wownero/lib/api/coins_info.dart create mode 100644 cw_wownero/lib/api/exceptions/connection_to_node_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/creation_transaction_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/setup_wallet_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/wallet_creation_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/wallet_opening_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart create mode 100644 cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart create mode 100644 cw_wownero/lib/api/structs/account_row.dart create mode 100644 cw_wownero/lib/api/structs/coins_info_row.dart create mode 100644 cw_wownero/lib/api/structs/pending_transaction.dart create mode 100644 cw_wownero/lib/api/structs/subaddress_row.dart create mode 100644 cw_wownero/lib/api/structs/transaction_info_row.dart create mode 100644 cw_wownero/lib/api/structs/ut8_box.dart create mode 100644 cw_wownero/lib/api/subaddress_list.dart create mode 100644 cw_wownero/lib/api/transaction_history.dart create mode 100644 cw_wownero/lib/api/wallet.dart create mode 100644 cw_wownero/lib/api/wallet_manager.dart create mode 100644 cw_wownero/lib/api/wownero_output.dart create mode 100644 cw_wownero/lib/cw_wownero.dart create mode 100644 cw_wownero/lib/cw_wownero_method_channel.dart create mode 100644 cw_wownero/lib/cw_wownero_platform_interface.dart create mode 100644 cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart create mode 100644 cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart create mode 100644 cw_wownero/lib/mnemonics/chinese_simplified.dart create mode 100644 cw_wownero/lib/mnemonics/dutch.dart create mode 100644 cw_wownero/lib/mnemonics/english.dart create mode 100644 cw_wownero/lib/mnemonics/french.dart create mode 100644 cw_wownero/lib/mnemonics/german.dart create mode 100644 cw_wownero/lib/mnemonics/italian.dart create mode 100644 cw_wownero/lib/mnemonics/japanese.dart create mode 100644 cw_wownero/lib/mnemonics/portuguese.dart create mode 100644 cw_wownero/lib/mnemonics/russian.dart create mode 100644 cw_wownero/lib/mnemonics/spanish.dart create mode 100644 cw_wownero/lib/mywownero.dart create mode 100644 cw_wownero/lib/pending_wownero_transaction.dart create mode 100644 cw_wownero/lib/wownero_account_list.dart create mode 100644 cw_wownero/lib/wownero_subaddress_list.dart create mode 100644 cw_wownero/lib/wownero_transaction_creation_credentials.dart create mode 100644 cw_wownero/lib/wownero_transaction_history.dart create mode 100644 cw_wownero/lib/wownero_transaction_info.dart create mode 100644 cw_wownero/lib/wownero_unspent.dart create mode 100644 cw_wownero/lib/wownero_wallet.dart create mode 100644 cw_wownero/lib/wownero_wallet_addresses.dart create mode 100644 cw_wownero/lib/wownero_wallet_service.dart rename {cw_monero/example => cw_wownero}/pubspec.lock (57%) create mode 100644 cw_wownero/pubspec.yaml create mode 100644 env.json create mode 100644 howto-build-ios.md create mode 100644 howto-build-macos.md create mode 100644 ios/MoneroWallet.framework/Info.plist create mode 100644 ios/WowneroWallet.framework/Info.plist create mode 120000 ios/monero_libwallet2_api_c.dylib create mode 120000 ios/wownero_libwallet2_api_c.dylib create mode 100644 lib/src/widgets/haven_wallet_removal_popup.dart create mode 100644 lib/utils/package_info.dart create mode 100644 lib/wownero/cw_wownero.dart rename cw_monero/example/macos/Runner/DebugProfile.entitlements => macos/Runner/RunnerBase.entitlements (66%) create mode 120000 macos/monero_libwallet2_api_c.dylib create mode 120000 macos/wownero_libwallet2_api_c.dylib delete mode 100755 scripts/android/build_monero.sh mode change 100644 => 100755 scripts/docker/Dockerfile mode change 100644 => 100755 scripts/docker/build_all.sh mode change 100644 => 100755 scripts/docker/build_boost.sh mode change 100644 => 100755 scripts/docker/build_haven.sh mode change 100644 => 100755 scripts/docker/build_haven_all.sh mode change 100644 => 100755 scripts/docker/build_iconv.sh mode change 100644 => 100755 scripts/docker/build_monero.sh mode change 100644 => 100755 scripts/docker/build_openssl.sh mode change 100644 => 100755 scripts/docker/build_sodium.sh mode change 100644 => 100755 scripts/docker/build_unbound.sh mode change 100644 => 100755 scripts/docker/build_zmq.sh mode change 100644 => 100755 scripts/docker/config.sh mode change 100644 => 100755 scripts/docker/copy_haven_deps.sh mode change 100644 => 100755 scripts/docker/copy_monero_deps.sh mode change 100644 => 100755 scripts/docker/docker-compose.yml mode change 100644 => 100755 scripts/docker/entrypoint.sh mode change 100644 => 100755 scripts/docker/finish_boost.sh mode change 100644 => 100755 scripts/docker/init_boost.sh mode change 100644 => 100755 scripts/docker/install_ndk.sh create mode 100755 scripts/gen_android_manifest.sh create mode 100755 scripts/ios/gen_framework.sh create mode 100755 scripts/prepare_moneroc.sh create mode 100755 scripts/windows/build_all.sh create mode 100644 scripts/windows/build_exe_installer.iss create mode 100755 scripts/windows/cakewallet.sh create mode 100644 windows/.gitignore create mode 100644 windows/CMakeLists.txt create mode 100644 windows/flutter/CMakeLists.txt create mode 100644 windows/flutter/generated_plugin_registrant.cc create mode 100644 windows/flutter/generated_plugin_registrant.h create mode 100644 windows/flutter/generated_plugins.cmake create mode 100644 windows/runner/CMakeLists.txt create mode 100644 windows/runner/Runner.rc create mode 100644 windows/runner/flutter_window.cpp create mode 100644 windows/runner/flutter_window.h create mode 100644 windows/runner/main.cpp create mode 100644 windows/runner/resource.h create mode 100644 windows/runner/resources/app_icon.ico create mode 100644 windows/runner/runner.exe.manifest create mode 100644 windows/runner/utils.cpp create mode 100644 windows/runner/utils.h create mode 100644 windows/runner/win32_window.cpp create mode 100644 windows/runner/win32_window.h diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index bf0d0f7bc..4654ac033 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -46,6 +46,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External + /opt/android/cake_wallet/scripts/monero_c/release key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 5c8ea82a7..99a45287f 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -27,18 +27,25 @@ jobs: if: github.event_name != 'pull_request' run: echo "BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV - - name: Free Up GitHub Actions Ubuntu Runner Disk Space - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - name: Free Disk Space (Ubuntu) + uses: insightsengineering/disk-space-reclaimer@v1 + with: + tools-cache: true + android: false + dotnet: true + haskell: true + large-packages: true + swap-storage: true + docker-images: true - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: java-version: "11.x" - + - name: Configure placeholder git details + run: | + git config --global user.email "CI@cakewallet.com" + git config --global user.name "Cake Github Actions" - name: Flutter action uses: subosito/flutter-action@v1 with: @@ -72,7 +79,8 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} + /opt/android/cake_wallet/scripts/monero_c/release + key: ${{ hashFiles('**/prepare_moneroc.sh' ,'**/build_monero_all.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals diff --git a/.gitignore b/.gitignore index d0952ca98..77441e66f 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ lib/nano/nano.dart lib/polygon/polygon.dart lib/solana/solana.dart lib/tron/tron.dart +lib/wownero/wownero.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -156,6 +157,7 @@ assets/images/app_logo.png macos/Runner/Info.plist macos/Runner/DebugProfile.entitlements macos/Runner/Release.entitlements +macos/Runner/Runner.entitlements lib/core/secure_storage.dart macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png @@ -166,3 +168,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig + +# Monero.dart (Monero_C) +scripts/monero_c +# iOS generated framework bin +ios/MoneroWallet.framework/MoneroWallet +ios/WowneroWallet.framework/WowneroWallet diff --git a/.metadata b/.metadata index cdddb9350..7d00ca21a 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - channel: stable + revision: "367f9ea16bfae1ca451b9cc27c1366870b187ae2" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - - platform: macos - create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 - base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: windows + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 # User provided section diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 57462099c..b03c8a925 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -38,7 +38,8 @@ android:fullBackupContent="false" android:versionCode="__versionCode__" android:versionName="__versionName__" - android:requestLegacyExternalStorage="true"> + android:requestLegacyExternalStorage="true" + android:extractNativeLibs="true"> + + + diff --git a/cw_monero/ios/Assets/.gitkeep b/android/app/src/main/jniLibs/arm64-v8a/.gitkeep similarity index 100% rename from cw_monero/ios/Assets/.gitkeep rename to android/app/src/main/jniLibs/arm64-v8a/.gitkeep diff --git a/android/app/src/main/jniLibs/arm64-v8a/libmonero_libwallet2_api_c.so b/android/app/src/main/jniLibs/arm64-v8a/libmonero_libwallet2_api_c.so new file mode 120000 index 000000000..6cdcd70a2 --- /dev/null +++ b/android/app/src/main/jniLibs/arm64-v8a/libmonero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/monero/aarch64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/arm64-v8a/libwownero_libwallet2_api_c.so b/android/app/src/main/jniLibs/arm64-v8a/libwownero_libwallet2_api_c.so new file mode 120000 index 000000000..8f6150ee3 --- /dev/null +++ b/android/app/src/main/jniLibs/arm64-v8a/libwownero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/wownero/aarch64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/armeabi-v7a/.gitkeep b/android/app/src/main/jniLibs/armeabi-v7a/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libmonero_libwallet2_api_c.so b/android/app/src/main/jniLibs/armeabi-v7a/libmonero_libwallet2_api_c.so new file mode 120000 index 000000000..c0d56dd3b --- /dev/null +++ b/android/app/src/main/jniLibs/armeabi-v7a/libmonero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/monero/armv7a-linux-androideabi_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/armeabi-v7a/libwownero_libwallet2_api_c.so b/android/app/src/main/jniLibs/armeabi-v7a/libwownero_libwallet2_api_c.so new file mode 120000 index 000000000..5a71e87b1 --- /dev/null +++ b/android/app/src/main/jniLibs/armeabi-v7a/libwownero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/wownero/armv7a-linux-androideabi_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/x86/.gitkeep b/android/app/src/main/jniLibs/x86/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/android/app/src/main/jniLibs/x86_64/.gitkeep b/android/app/src/main/jniLibs/x86_64/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/android/app/src/main/jniLibs/x86_64/libmonero_libwallet2_api_c.so b/android/app/src/main/jniLibs/x86_64/libmonero_libwallet2_api_c.so new file mode 120000 index 000000000..654be50b9 --- /dev/null +++ b/android/app/src/main/jniLibs/x86_64/libmonero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/monero/x86_64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/android/app/src/main/jniLibs/x86_64/libwownero_libwallet2_api_c.so b/android/app/src/main/jniLibs/x86_64/libwownero_libwallet2_api_c.so new file mode 120000 index 000000000..bb3da908f --- /dev/null +++ b/android/app/src/main/jniLibs/x86_64/libwownero_libwallet2_api_c.so @@ -0,0 +1 @@ +../../../../../../scripts/monero_c/release/wownero/x86_64-linux-android_libwallet2_api_c.so \ No newline at end of file diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 2b6649271..8b734a7bb 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -1,2 +1,8 @@ - - uri: electrum.cakewallet.com:50002 \ No newline at end of file + uri: electrum.cakewallet.com:50002 + useSSL: true +- + uri: btc-electrum.cakewallet.com:50002 + isDefault: true +- + uri: electrs.cakewallet.com:50001 diff --git a/assets/images/wownero_icon.png b/assets/images/wownero_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a3da77b9e08a57f71bf63ca5863667232c96c0c6 GIT binary patch literal 50010 zcmV)eK&HQmP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 z

PIo0SD7)(|ZvK1r#hCC+}~C32=H=$X2qE?wR#!fIFpxhp1>Y2L6K{XVW(yl{Bw z>G%$5jZ>&@;V(Xede+-f;fq2PSxQ& z8#|yiP=?e^uW}sUV=#g>bPnYZL|K|G{`<^3Vz{UZ^n~GINQ6^m{Qs&qxSm0&P z*?;{xb~Z*SFU1rogfFR^d%0Wfo$r@eM^tQqvE>?Pf912-|9&B)pBVwk1_LDzK^B2A zkY78@(eHgP&E1|kwSbGwEr%A2bc7Hg52i!P*uzHxDgWgl4_yeQf^*QguuPOHVyCW3 z`5R-NUR+pQwph?E!xjpck#wtH%~42J637}i5ak__Qz)Z|(+L0OOfUYI%j$+^EWzT@ z9mLhByZYahR)7fY^3JWPpp3KBONJfad?)_M{JUy3bGwf$~qmv5$X} z;t5Z+T}C-b(iS3TYIr)Aep@ zXm{YRzD6GG9)po1lcc3UFBM8UZzeqT)=SvNI~2h&YL#QT*k)$5&g8e>hyI=&L6euW zzLEY?g(g-){iB@1>l3q_`Ik>JHeIFGt`WN!Ej3Z3SD4o;5i~ctN3v%0t^8~GjdY~& z))Au_a3yv$hB@N?=3E)ledDoW{T#tGvO9qdxBcT1YD?qBG}|J$eQ#6E`#UI~moK*E(Hk?7wT z8yNOV2trPfC?OL`Em1U%om|G~mxPzz3DZj{y^Q2>VNXDX#ZEE%{1hzpz1V&!y+SyNay<&~E(KPWD6=W^x9^-ak1a|s zTpSLCLZZ+HNkN{LAU!_sE<@PB=`~PhI{1$VgG>jrT#| zeX!?;9-+0@(TF;DEpXO#-&Z$Gxmlyqud>&&h3H!F0j$^}rJjy3Tqw`MdndYnqI|sc zPRb4waxbXE#pliuS(!OqL%;hClz>Cyaw0LyD@(7QULHsye9KW#1!R&WXywp$iApW< zc=zIWd8S0vhd5V22zt&mb`?|oX|Ohe5E)^FqubyM@sDc z8;`TR-?30whN2*=sEE5swv#JObO|b^qa9{<1p5iJ53B(y%751)jF&f|bFz)Hy}iQ5 zK%_o}<|KIcT&0ROqe#s78+HS6%%%4LmNao-!c$G={pwF;p^Yl!QL=I>9z zzd0Ys>DvlzA;J=|c72Q$`Dz2eW3cB(--kIIvm7@`6}@63YmK?LP=W>PLWXA7u`cNp zVD#Rd!D`Irv$|YET>t90ndb3~EG7)!|&7@RM2;rEYo{$D+pk<qeGP7GGoU(a5i(M z7Jvs}#}7Ts+>QpzN3w53@w)ORNw{M+w? z4;KTjKi;5;K0u+QAt2!{0{>!)<=;Hb*kVj6u@r@Z#Gw0Tdk(BwTLG>O&AnA5cjjrW z@YWLRh{95g`hubHvnSyU&{jQW&^3mjkUc#sJz*7n0FL~`2Z`@5(d0_UiE#Tm;U>P$4w^;Q4J)p(2eV_D_)XXfk_RB3BGLkuAo=yPT=>1$ zDV`CO+Y&k*q_sWVx9?@!IWp|W+t3PF(ZxnnI;KY?)djZlC+2^mL5fh9#5 z&(zZ=!-}^~!jc2f8QSaNaXNz?tV+VZC37C#P$iH#Dw7GzV}J7lp=5R`d~OwV{05xn zxsslP&M%#0=6Bv;s8gh-IPW{C^`@cBS7 zw#>R~kjOYPx=f@RI0E{o>U; zu+Wm&Df9&oUnZC^eWqN0(q%U?7y-8fdo}II;6zR!d4oN<8I9S!fA(Fd2Wwr29}!re zAl3p(^_y=U;2cc<;>%3^;c*5Ri|BU7o}5(!zU1_EV(M>4@h((H5>ip7Ky4;Myx8FU zKmST#nOb6H%0Qo0;0RH&&~b#xUIO;P*Z;LYvku4JTPayILqJCx#vjU@f8}1f+=~9_ zk6swy87NLe!A3}ZX;f&#dz%(``I4~1Q?aQbT`-ICXp@wl%$sOXR#R+@(>SG?q?Mz2+So3YSD`3qbzD6=qN2RI9jpMMSchf9nsRH;xw3!N#p7$GwfxIS|( z_wMFZb63>zy9M|B>x;~)6=cq0>%nEvT1zU3dnXSup%)C3@Y0u zucr(`#<=j9!t?EuvRQ2JK$=1rf6gPV&*DI}w^ z0Jofk`G5N|?%8DqXJbm8C}7`L5po0|&8oh+TrxiehHqZ zmsEj+%u(!E#V*j#KbR|n8@!eB-ebI>6*buRuKj`dqvTnsxmJo$I)KE^LgVBdF8|RnvY|v608hn_iS~6XYP86FW&h@<_D&qGPeE3 zCT?+AiBbX2%!h|Dp<)d5zjBEq29fyzVisXIsE~gwj-#64jUB?9Y8MfXmttcFsvwD@m_ilklbyi4+=J z^fZQ>@c5oSU6)xtD_Aoi^{NZ~wO=@oXa-hfCFNBm%QsWRyMQ$pVVq12U z$Z{+H5k$uTGr;t6>fU>;b^lmvpL1p~z@SkCNPK(_2WQTiUDjUfTVH+O z>FV?&kG*a$Pu%<%a|4Tj=T^*gg3K9Noscw&ZN^6?IWacLiPotMDSqivf>Dvt9Qezp zVZy)9ef}O;tn9I!fREs53j5lVql z2(7Qqj>m7ID5Wr2ib)fq1O}H2l#aqTKloSh4WOZsSrO${63|?+q7{v(gTA%mq2Nos zkcM03;7ULX>0O?aaNtYNqB=IG^17(gODR7((L9x%9Gm3W=s5L4BLKIActT`+ae)z% zR7!{<(o8Zqw82^QgU-3xeCN8*ZoN3u6s80`xB&GY9|SHZ4_Wp2G_Vb>|KYdNEGNFa z_6pt=b4Uv^e5~UnochqS6rPKyrd5PcKJRTU7~@};Di?(5clAktsShuWNTIT<*>t$f zq5tqDuLPE*C&x@L`E)O-+D4F(9T(n4(-S1CZ~8(%@a@uQ#YoOAon! zM(xM>)`7@bMDYvN+KB5|i_?-+S&RT>VhU*)DQu3@=3l81C|5uyHR9ryW76suxk%)^ z$Vh6<0&`EE=;g!vz(USAXbvxRXNl6T0hU$kiiQl64q}Hl`=5t{|Ib&E2Qq4l1ypAx z_`@~181cj^1*;fZE;2Gz<;X96#iI+=L9OVizVakhmU^*G?bc1O18mWkPF4f8J;rU)JcqU6HiHs($3l4wo(Oyb0=L)AUNGtkS z#RTxK8pS(?X~%64neXxLyMptDWxo6pI;%R;-_OI2lfU+L1}6&)Cj%6+P^YaV#D!}< z|0PO4F-T-IwPu0pRLs%edva zQS8tkJVexxkY31=TX8f8oX(=`bXldKX`&$S5OR z&qjXzrhnJIS?&C^#r#`YCJ;SWe`~F$+;~g846c&MPQKv%Be=4EOGE-UuO?@!#gT42Bz58BXF6n?_6`M zTLLe7$UH(SjO&o<1UqaP`QBZ=Z=|sHv6aYCd7N*6dNRWuXi>H`R2-i*6TbH5XY=Y= zC*Tu@vP1R1f8ggh!W46l%+R16ERB_E*X|$ZuO>~#Il)PKGdzU=077L+L_t(e@Y1RM zIOqPSKs8LOSNhs*Gh9c6))Y;|SbG4OKxV&y&cjE29H6rf(WAn*dfp@`-!e#}(#9$a z&CUuts$O^gQFjNh|P;b47SP?GI>UB_DauSTpbcBF8ancGLl-m$i}4s z4*u?A@L<#934~IOZ@pvK10umm5@~1;8@AlD6RxLQ_rDf2p3QMhoq!f(j~-*RGem47 zT(-V{vwF@2M5H#>{b^Q7IKc_VIpG&S`8D*``nqHFrrYjtBD2p6DW642C&NPMwHdB+ z2mz7MlsW|_pE?201Dy!kDVa++r|njP>U-{DazmT8%=&x;UoEG>&BV@3!q*jdapOqbczc)_uKPsH|Vf zd#(@LB#c+4nI2y1G*#;ZHVVAhyYxg`Np2Mq}(d;;_p1<7ms?+9ybf;o z8+Wo)Nl0T8td}mv*|(-z&PZr$f8Aef@zN(BWq6@Naaj?q8>G1=FPA8*DK{d<7Hb^% z@6WLKsiOgoPM)!^xV-1;vWlng5-vqLi%}VMvqWvYTO7Zq)FoB|2{P^-t<*3NnK1)4m$`xsB`I?BGq7axw4 zd)VSJ`;4@GprLE$CxydmmIr)Rz$0(1 zkBEAC9x;CBp9cP5hJ(NHIAhZ_bSJ`HAf5U(x%}dUBuq7ypXVB0@@ zBW&)YKRcT^ymi@x`k%kZ;8K-B77Bly=mLqHOY#jk@OV0BvQIy~|4DnIGR?%$0&UJ+ zEhX3c&>7!#G%~!w$te0`VA-4BI`|%EKZKLE`;uft7HUa}<%5gxV1N%_73H+yzwX1; z!Z!-N4F>PoOs6Q$+x@s?Nq_}#e>lnHA3npj#xUhnqO6ZWyf&J3u6$8}sko9zM(4^j zA3VjvZ|(DnJH)<(?7c3h2&J(W1OjTBt>3=OgMB)X>z&Ks*TBO|Ebd=qAgdA?A6M`e zHs?fi_Uq8&W(fat*$Vr)fx)UPL~%f9KDVs%xkXE8$R-n&8D2;ZO?9I72L#qCdmalQ zm?>UM`7S1HH3Mmdqkq276CL&`D4!C_yQFQ87$LOej(yLKB%4y|#U@5&-D@P!RSRcV zB~~7z{qrKM$RcALaNTf0Mz5GNopWHu9BiQNAWCr0WJc>(PZ;1qSBJ z)b~y?^{daq0*D4{7Yv;jXz0pDt<`R4nq?$pG%G31%^A^mZ1%Vj@v3Il{&VF;KPhks z4uAd`DxD%a6Jc(hXAewZ|MjsjNi)tqu+JPmIXN&-OQjxWf1N|Ds}H4CEY~{h+davN zH!Qejt^ILa_D!;uJ2!V=KgPNa%p`zHi4#NZAn?ED0z*vx5c}@h20SDkG+`0g13P}= zEi7-Er4==>E+KDcM&ua<8Mp;jRYTJYw!X{7dSFS z0wRa0BY?O10=lSFB`FzBZ(e4{&)y589xl!RBKH!74Hy{oIO`>Tp8drFLx*FEi6H9( z(G>2CD?G~pr2ZOXadffP`NW~ym)ui3jx$$W#-`_yMqKwB!qO_VnH-vDa%h3$%M&dG zzellnp8fhO9ihx7sLV0gC~)elFTpqQN+3DgdI&Covc63CZWw#uI$DDXwvw~bhI5FO zWR>R=rw~E}B8gCG*al8R`!|o$dUTf2`T&(qu?xm{O|DGoXHZgBQ%;HuE>&qfHp}A2 z4#5%pvo*ch=)LB1Edjae9JcG1J0VGxB&E$lxy{bMej9A3+wIqNHAIi!zyh7L<0obd zH~_O>-p^p8L}WZ*&V`6@0XslRXOrZ!C+5bRQv*v(3@*`(6V9iN;GAEv^fX;2A(fKK z@UTTkAHm5bbn$|VU4#(b_C~ernEfC7JcM}W;hmhzFUxbIDWKwp~QZ4OeQ6Bl?Jspjv~Kj%%c!Vhyz+tU^K+^ z&A})tcnvWLGr#);b}{J|#pyNM#qBv>1O{4`q?NxsJvr@iw>(ppOkD;GAU9==ghtfn zGP^~ye3zsC@U!?n#y8CbkH&T ztIxtL&=BX>MC5`y*>wrR?*lDcGfptOB8GqB4qxsY5vZSful!FWfkMIw-@ovLq4o4U zWmCi$gNh=gkQdQB=CUkGes;1ldve1)n_WjI7ZjXcUjaBl<`l*W<|<8|-f@s;w;!Nc zssEwNIxpq}bvEC&pC(E=j{)aWOC%%Wj$m*$=J2N<^xcAWx;|Ej2d=u;;6fFE+hE7{ z-cEZkA(Jh%6X>jm-gVaDat>>Q9he2nq6HjMKK(roM?UryHnawbGmTOTn`PJw%u#x4H#xL4*!=ACHyq)a>yNNd zZX%p7Bsw3r^}M$?=^)by^P>$WM;7_Yi(j7;mGVa&7vHe2tfb~D3^pn(e(s2`;_7<_ z*7P-tt`n?~Z-zcI0Y_Snb* z$1Bsc2y|Du+o9(^5gjB z+z}b|Qj7SWQTVO_Z~AM`6Jfd}xEic4=n)u1ZWb&9kHYkWCn>Z`sHk*F_hy9L7hS*e zS8F$1Z(EVX6e5i9X?LSCjJ!-)06!T1r`DRfN;ok%&y&;NFzqP&_g2WJ6}|8FWPRhn zbP`Ig$jEYuLm%7gkq4T9@R8ImHMAh$WaBBzB;FDjgX{m%U9`757AvhDfQZ8{{ysWw zE4-;!5+z^Pe-KW7bT7rjf{|nZ<%(Y0hm_Cx#5b-fh^s}sLKiK|P`gC+sAB5Z_QH$6 z0#viuL$*VghJ8hfTUgq0o29`9@tuQ={`k$X1wRK@rG>lVB&juMlwbTu;NZvhQd=mX z+6t4N52jh}f~=g6!qIN&&cA+k;Tbbgo29NgB-VJBqYut}y*h{UHWqs2IhKf+8fA8} zna|$+iPEn2#y`qT<2!`2egQ#O`$*>zNY?6O&r_l~_~&f3j{a-I4E-NBd;GLw01k0| zmO!|UoQYfJMLS=$9Qy5t*mQD$YO4rjq@5Nz^3aiv*gFO;p)+cz{nNK_{PT}cJR})v z`qEw*dKU5rzIp<6uetvjU+&VfYx1&+PZRshR-;%a-IlTOd*2D4Jw$f24kGew*DgUg zN!k%ag#t#_S*R^DbA7_=e&)Mi4?ZmoT?#p$@AvZ|Z*KK{yI%fMV=X1z;ceA*zA>*(Z zTC^!r;%jWDBELW<5D0`Y%rDGQOEuzkgC09Q zzaQbwEdLdu+)@IAf{_i^GxO{b;!LB_p%BuAngEYQth7K2N#psGFr6{bst{$KLBb&s zdCcLA>vg?rUU3cAn*OgQ@f?f-Jz--uH>*tiS0CJUJW2}owgr!+=oEl5n z^|#*+I|w;FOOkbZ2XlaxevVoTBtD?M2s{qsf3}ao`3Pn5dKK1%y|==4yKZwOA%wG0 zJoul|f7`fd{rQQ>xP`Ia1j>hj4+O+6c;`O2HID&-FEVEsAs! znMJr%dkA+Op+z9i6p~ayquilBn)3P|djQ@M_#vgGl_ivPv2XX5E9a&4(B(|Sp-(iiOEp3RY8a66w4XW04>w&Nxp@5?qkHEUX7ox(0qz|8?iDYaYPWDNbS$ zSRrs)VWmV084UOgp#{xS%GAIj*MHB=$ahy@1c<Y7 zBVTAyXeqStELajp9;-f=3*dzC&WHnq!C_ibsUMh(rk;Iz&vBmKwx7>E^+#kPS^s{| zBA289GLdrocfEm@tW$F(P9HmQVpDba24%z>5mNY;W_$KE(#P!T1PEaOM+8J2SXi8+ zS}Y@8SM^t_d;~&ZJP@OI=)CVGEeMB;1tJneH`mbflDXrPh_)q?3gKiI#6t*^qjs*i zq_Zo4RiB^r7uV$K5D~~TZMRg%$A1hWk}Rke^8?H5`u>~I?-}(jW(-dG1@8;nLToF{ zLmM_a0G6To*$L*qc#^?|h}desxd}%emKF%yIsA2jK*3$`IlE`5Q~A1yKdndcc=h$&}^Xay0N|6WgBoNWOMgmpgdW!2PF zdHyH%-!eJ(;`VVCD@~fN#UpzkzNC40$qsO442e#d8C+y;q|U6XA2E_22ip`m{{K1r z>@$KO!QU5FQpw5;%$GR)naAOI-`6cu-Q^a{htAjiX`iVO7VDarGB5`6?`&uAz8x$M zc0%8PcOq=a^(x%TTz(F?aE+|CSvdUJXBn*zQpzH<6J08AxoS>{F0g05=j48WST`9688`3N%Bn4IIxStl7la1u5IMR}$ParWlD6*y51oat91+Pixp=VwvDZJx+^EX|YWyx-!B8aAl4hyH z{Kz7^fA|gXH>!T;&z*GZQ`=6uUdxMs)EZwSV*^pl)9})-JVi89LZ>l`C{V!ndJ2^D zuCWRkGamrUfo<~5Ok#xwS$0_UIibZ@hc^tnSj8w2lUnC14}0OOwO{MVWe9;wjP!%r zjxr_(7C2fsog`)R&xzIHZdRbX=+E|(?(W}h!E~A;MFx@r)#K3k^g)jz)D)146~=qF zx<4;G!h!}{12#h8C$}*2&g)nnOQ^>!GVRE6JcF?K`zOvdd6mej=PDdX>vvcuG(ss7 znNqK|nHgT@y6?LUzGoOV;R}6qZxx9yraoOJD=7se&NmlLz}zPfF*p%3lvYuxLac(! z>ov#a#|p1L0GpmgY8@s!OxgaM@xi%dVrYSOlm+E*c{Zd=jse{NznDGsXBEY_eqvz&4AwOztI;~fbUJ5c!0HD7y7llms+9<7b89J2asD^iw<4O*sSZyvql%h}1~Ua8eC4+PXljoK}u zKHKxlnGC~&5E5+)%udW8rk06czsZ*i7Zgi2BN2vz^lgMrc!dx;6T;<6Z%$laW@yK5 z4!?AOVkag}1=9F{ri=V;ubc@fOcAOB)_#feakQ?Z5WeXcvs zRw|y?3pkDF?quPjjd5w3e7SJ@rk{MI_H?#?^EeCTC5+61K_UX~rDX3z4_?w3U7`YT zJaGU0L7RnYJ3K>5D^Ac^k%2=)ZnU%MX)ynrkgnQ`PAIQ9&XvOqPtXb%9)cjUF(QHb z{36w+LhKoYim&xISPH`bog+8x!vk=koU$lAroaen+_Rhd#0-svWrPE*G+F2wklhrf z>@N>m<2hba09UN!dyJ;WEs1bH4k0X$Jah@&_*{k{pp^-T1WDcr=sTEFDKVmiu%pbKw$EeBKZVfEZhNj( zcpLs)kli62(nr569h7WLZK1;Pzc|X$?~KE7C|MYgF)IR&F4Xo}1un1<8Gl+g0K6H- z{>2@*2Wrez>$D?_Lt}|CLSuwJANs^=as`W#d7r+-NJ+Z@jWI{@+qNDM9kkiJbl6P`FH zfb2F&i$B#krJgsP4WuHbEe%F^M(jjEDlJkZD3_ck=H1!lGQr>eMMrW>BHSOXWSs`2z~5u=HKiH4ESs z7AO0P%X62p*Y*nzCo?)xN~73hsyfGY?|nPvzqP@a_Evn!Z7O^jx+Xl{zR-dH;X>d* z!aU6W_F=?&aDzJ-m;<$@|jpU;ea zmvmA|8R6Wyn;G&{M-E7W^?sa@mPA|XDr0G|!M1npr1-oG6!}!&X)yoo zFekozg6d+KY8nxlyoX@D{{AyXAaK@#6h>(ICD-bF-^9Rj=b7!NIk<6#fAgFFineHW z@3$wHCslo!*)3PD1VhT5-*E>3WtDLqK3|S&?=&{u?u3uGpYw=)8dx}qAP@5S z-xtwDMzJ(CipNG zyNwnS)de)^&kW=phSvgtubc=UKocQPC&6Tij~7WnN2WBQCW|9=Hh%kc4E}?+!3}g_ zyohq>Z!aNp+yX> zYRk=|W2bmw`#6)s(|qNTzhdo!_g4X3yu7URwhp-W{`<+KWwG3(6B#N?rB1a|dKMwx z<%AnS$a9X{3I)*Df#-vQj!rY7s0xUtq<(sd=wydz*C-S~Mbh)ZVM8!bAZ5?#FnFaJ z5)58(3_$U%nAdD7pekJ0nu+ z;?3xM7ZT#I%iQ}Wqxs{{?K;7}t&_}Fn_u=(|d{?jn0J!hI`$@H-UTl*oLoqAT zX|yM+h4Nl)<&OwN@nRJK!l8tM%n>_HSrl2EoMHCq;|$%n6Urd8@K%5egWa{FY`3`= z=w=E<2`GZxRblw{U8ECB%r7sJdKwm#lPIgYDdpa?yruxY^&&z=oL2&wGBiYorMN|7 zFhSi`X6w(r8E!3lf4_*2=R^|YdrDU%qq~nY5Ah`U+Ww<3{=vV3Z+56G6)92iR-tj| zON4|ILM$c)_kBn0#M2X77x?D32_}aYNVIo7_ddK{p8pD001w=IukXVbhE!)PR@=C$ zqy5g~#@4e(VjD-?!b@^Op(1UyGA@PoC&Ha^0$qe4j9(O}qho9Tq;k4}CnP&lt6H& z1wbOYP+lkh%aat2Im&Gf9fMTLZywIaf4D#e;H<@F8A?YFg^Oo#XYIrw{4Q~ z>I}BnW402&!w)^O-VrZp3}1EyaL)tx`P7B;X^f6Bm{_n-YM=%RY%wD*l^T(jQr_!a zue~8(RO>4MufwHtI3ch~B2|R$2%;3`o5Sb4qc28!#0_e^FiEyfC z77Abxif1WUk|yYi3}8ph}w>s+ z3KB#g*8udD9MeN6hMsw+o$n0bh85w7=BPUBNM3r8s*FyQv0Uu1P-&1}FWL1s-^Ad1cfwA{${AV7pp@(DmJ?@^ zf%c!$0;%$g{{$@kxBX20*yGQodh~Oh(LtG*UwHq|vwM0Ycb&X` za8qaV$B^y^O_~a&B9IDWyh$XT@0BJL9GOr>CAy@I0XJRxkWTk^3OYKWQA%lT7Hs>O zyJ0sz?H7QHS7urIS~Q&u=>ggM$k2-nPeSvHlZ^k>0S2Zdm3qX0tU)V-D8i%(N_l1D zLJxXxuFDmWXgQU$%-xc{uEq<^Eni7uMP4V-qs+2Gc60BM^*#If_;klQucng{sT8z2 zSZ!D?wwWzAXm3&6@&j*%cU8TQA6z`Aoj+&S(|6%mx;xewdRw0f&vF?!0H;6t6dZ0b zn3jkV305KP+PoI)iHEAp{Nh&{YmAdx{m;qL)cc>Bb8Sh^v zc|~nkgW-;Mya}r;$DTWwh<5hhM{65yQdZw8Jde2FtzG{bU6}^#)@+B7lHHVCj{acL zMp8&ixE37Udw>nq9WW}oEaWRkAwOQ;9oQEi&K<`H-zz={(M>fr+_sa7j#x^TSgJ3g zixD`3lq$GC(pxM2@r{)!gzj4>LO*!_M7W9-fau?HgL{?lpuVEQg{CXpS3w_Gd8Nb1 zl#Xc8uB6OY>twrQc6{f}jQxX~;m**&Q#Rg8ll{Q1V&%E>fwbWn8a(8D3(pDgml-ep z#-mimHG_>3MHeAb8?3;KTZIBZ`?N6v+my*?UThrru`e%wy*+b##?@P1-T$@LyDVHXS!7aqVK_WU4SlKNgrs#877&J@?=Pa z6-4!nrTxd?$P)1lo1hG?&VqR+EqU$02#7mtRPNnIQ45Yw9`n&+BXESE zQGTs1Z(Vk*C@Eg81j0G5=H0(L{=KK;ht2uQPU!-mYo1AQy|K@fAkl`7Y|$8KF}ZP( zT|fK=YJcx0cw5;+b>+(VETx#`Mvo3UJ0Eq_kOLK5w^0?|Wj~p=9n&j`@K$X`J!!B{uV1s=VUStNZV}pHzC&P80;4 zxlXouc+=-?lHE|0h1;BU!4*E60$3?$D1hKhTOm z=BtmDyYv~7TLgI}VFlDJLyX?OhjMLz<#d_ZWRXOt-dzdmAU~LIO&2(MCc4Y32W-Tx z{I~FhSzR|PEMDORPC8#RQb<`UwV1CgQ@N$ehVQtMjX(7U#M`PqvOf^UJkE8-xv(1g z5F~RKu8m*(U#)ZWHy$T_ewmG(A)=;65&I~;HHawkfx$}<0WOWrXVNX|eNXBaTQ6)q z!KslYI@-`lJAD1oORMF-RCz@efCTQl=k4GOR#+09u~=;})0xXUN9I4bIU0R~!@W+c zNLZ8gqOYqIfarG-&d}KY6XgWLsW1l=3QannG5+w2j4o9X+p9ihRO%m zX2uT5#QjS^ib4T87r}($WGLD(ey4SkIE#slrrL^_{Nw$kFCO6fAAA$sRrXb5G7vK; z&g}z=Uf?PfAd8@i_<5fJf33i+ciayLfhT7ff8-EuE}?7{qAiGJ1!aBK0%HO_c^H+e zfAx3oLD?`?N|V|S9cdw!BsM{mG<7mc+XmS9_SdoT^+n&KKY(BKQAXZo-X9;$-TAYM z1E~ewn%@QB(H6)5=xK_F1>5RFNEZc+e~%{-S>FKeLcHQa#W^4Aan{&Qoc_{$y!gRK z7oWAAEdxvq*U6*@C?(9pS6W{27{J@_yEm|Vd5xV|OQIb5x-zZEbi_pVr70w+xzdL{JxFwR_(JiWgs#T1*+?A2~ocqN?+4u*Z z-!;*mzCB~E)}k%bo&^-RD!;b(HEscPsS-lCuTUtE;h5BmNE91x7_tkE<_F6Q@?e~Z zk0=!tq1PTP&ebC6@?ziovmV8FUJN3fBytgUK4nx^IrV1;={$dgt?zmr#dmLo3W$R6 z!6xPCM;*S^Wf*BuF2l%-cAQ`a zG@Ea~k+HXK^QGF?;j#FHgR;fqd)=}5POQ4;hdfNvJ5N65GXGBI@$~5cbx8kmiW7gj zkJ5x=!$OUUt9I$(JmOS;CtzPHvOB-}QLTMC)5=~Oct62Z6zNV|w|{!6*7?l*P-c&e z%`iE%h*g3#O^Ic+a?-rQa)n&Rsw)Z;zDUf!|GPiWjSHK(;h6z(v$+0!PRkDggTnX} zp>uhfCByH$E6^MJEPxCrY&YM&mZ|N8r6m(TX-k55SCx^!`37%}<}lF`{Or=cl{jon zJ=_B5_ND_l>QQ%E<#Qe33&8%2=|^9pJ-JM4YMJ6vM0Ke~H7f!Zjqr=x_RI|< zd;ydRyPrZJobB?{n~OAM!^ogqFl&_r9g}4yJSN>GyTD~weEuEjFyY_rLdr~G88N1* zZ62VyV}QyXJK-juzGHx%9iM|kNxNzZ<_}8uFToXJuozNel*id`;!Am-g4y4H26L=~ zYFf%BCeE~H09t*g;!K4mT)_Lsg7mq1=i&e^aJvDI z1K#&HUe=oM{cz+yD~bCch=CEoKMis~bz&ATyK=*`VKIZ{Ll&Z;Q7G#z1KsfKBsS^ZtX#RXbUbRTE9;3L$ea6arQmKF@sv zh<9w{x@@DTN|<@4=ATr^^l6%wZe zskJy|kg=jcoit8q>x_=JRK|wT!v#tkhtV4}-px5 zh@!9CPWIEsI}^`NyrtugZk%Sm(#EPQ&%j)3~Z<{ynPeVwjtQF(fjYEzWa~yJA4d8S&nss5DI7e8a*9|yfU}H z0IP4kSp4@Dc`z~Pkvx~5g`XW@4jR9|pP5G|h-ZszN^6ulft5!9QjlgDk=7U4xheZB zyMPw$-7HHazqFVv{_tGfSUR^@3;s>z+lcPv#F;~!Nn{%0bN|1UIU(va`zR(`Gj0M52JtnlroQh1-VU_iMt>7=6Z zmvc0qd7KSz-U;vA=wYTd)AeUlV~CXM?Qm!8YW?pu&Vtn59(FD-mJ#&8%R)0!grE2& zdXPpN@#lZ%7D{dlC4pZQO?s|G0wf8f=aE8H5_ALw)op$vfTXP0#q)bneZwsK-^(zL zqPJsr2ix|oc@ey*RTNqTg<(Nzuj{&#HNi*r_ksJ`B;$JzQCe_pNXp18LRsPa0VDx8 z&=E(GAKMF=enKK#vF}-hAh5IGL=z$YU(;-VXnCmFdEvSf93Gouwz@zj6SR~U(?0Mm zmn&NVkr%k+kq-X`2K}1z0^529owgYdQs(-e0JW=^o2%F z_UeO0_`Fx<-sdalyX3C$)8$+AIv!&*ITjKl zdW~hmIkHa6>Egg?O!21=-u+$q9>5i@07Nf}twb;- z1Fn2f_%KH=3VyNd-g1iXrG>E z>FG(De`?tFT{jcGWk=6XmULTI^WD|E7d9;ITv@H&V^?3_IS5q)@`c666rF`a)c5gu zn7~fBUMVf&%u!k=hwbm8AL!MFPA8A*ELmAA&^`9*^k{}xUX##yc${N@@l6I7id2_k zHl;Pc890X&ima0ot0E4G$*%>6a5v|H=2as$t;_;vVc&hg30ow|E0 z2mN%`D}w+lkDBj?KG804*1J76TxA44cb`}6C3r-hhnHA<^d&l{JIIDWHlS!C3MN|h z%vKfXbgiKC;A`kIPvx#2D743L$TCPRxY!}YVI!Jm@h3L!Pfralk&2jBobj(e z{I6(-l;{c(y^ZHemMh-^aA$wtq{+$AaRe6SEUjXdfvlL;PK&SIR(svs?PBuFQC#=| z(yWXLTUQ7SK`==G3*#r)?xY`$wJgKygf z*Tt}1fl%mRr*v_v9HF#~73$f)?RRNbExuKaiz{m9&N&Is4tCK!+v;7vfAQzRfmPQa z`^L;5P{BVD7L*mfE7F9#eVtJEkTLh@QI?)NO?6gKYDmhR0@_A>{`)EmsUMYBT)@sB zzOS6OvRtSMF<4yUk$KQj(>Alu*#Y@)zS%i`a-r6CN5-a@7@j3n7Kikt9c!KQ3SaoS zT=@#%Tp}!1dR{^dER>r(z4Ihni=%Gj#PZaR+Qy%(HyWQPNcE}6+KoOW?fb)5x&bfK zraAf56W9SsJX&Pa+i!vU2YoAX4Ig!n0O>jGrSpYoXE+A^x$yiI zCfpg{J0Cg6qFM8ker0eLXu3QD=G%C}A9S2&b8g}hcNc-ZZDt>Sf%@bUs;P+EF$0|f zkC>5t2b{_4rnbaL>fdB!)*uvBSu z728GPgj`W_bt?duho1ZrX-66D>N1&?ROiQOicT`yS@`14+UB=!ONM@q#q@`9q9%m& zpuWg8>gF%!^+`k`iGe}}I#aaf8@PGMP79`vzrfO;v?=bavibE}(btc__Of>sY97l% zMWFgqZ*rB7>CT2qYgZQ1T*4TyEvt^3`ySz?`jwO+Anyn`Qm1u%p3~o)pmlVa+FVSv zUSyLUpeRb1W`-0oPHNxDXWJJQz&mdVt{~2O!YOB4u4;a7QZD}c)AgAn149*eyfVwl zE%VHbG_YCL?JiwWYJQc<)vW*^U;^fK|Bz)Fi{)hw?}o`_9_OrEoNpeU{+jyxcMjb8 zNn-b>SW^_%N);7_OYNGP{Bj`iNGw|VG?Iv7u$H2(Vy#8DL8X?sUcj8T%soCys>d-U z0Rxij?!E)h%t0o45L+od?8GC$Q_~!M;w6&VghD4mTS>t|C9P4)3dEWArnm(q0@t6- za7mu=I1^Rqa!4M~^;r{Xv1yAm`|ih&KJm!x1J+Iq%%g>-9c3&Qnz$rGI^W^wP_K%1 z?Wofr>8F3E%;RTC{W!pKykE4b^8!v#}I4rny?0jGe|P3niINM<|KPc4wnw9zw$ za=XAtS|rW}(5VW+fe>{MrOGQVubVEkEBX3fj%S0xWwNQV=s!_ZzcO8$T97x^7~gV; ziP|i}>h3l9fArGJ@(NAuU5H%u3LrP(e04S`n}V=p%91$=CZVw*=E>p-7Hbjq+3gF1 z)1_Z1ZQ;{KG=EE5doS2Bgs!*qE*4q;;d}JkUNqPv8?U_XZVRfgo{&g5A{$X?L!m8^ z9Yr!>Fwf1?#L>``bX~D|IC3;b1=*mqB##xM8;3QdqYm=eE z)T(e@A_)asYj%8GHFEVU0CI-cobv^6VFC$@HG)~Y#C)xeWPpJ!3v4PXH=Q0Cuf_8F zH#2yDC6WIwvb?!J>3;5h`}0NXvAt|TsOXcThZrD*?JdCYduJRNOB@%`oea^jlu!s! z^52V#zNA-xS#ZoBJH>2#5+gH=Hi()=#NY}F8-a@j#F|KJl-5YOR$akLF~*RlDWneC z4v{!S+u%}*XgW`aZ6MAfiVe*W14uOl)?hMUZtO4tX{X>9w*u#~9w7($&v4&;I15hN z?xMB)G_$vb};4D_kUp5DF`>MzF6PC;xh0&V6NRIcaaWevdmfyv*UT8K%b? zbdnBAs?g@Vap*I~ADGRt*$H7h((hF=Ia zTob)g*zV7Z98s6YSOSPj_usvrz707~iP|-ajdCuBzVhF`#+tQaE$e}f^grjWdzDA= zr8mxVv?om9dyEB{vzfB`V1oEa;nv+Bd2aSG^OIlsdn~Yoq)3~LS>|Zc?#-jKjoqst z0|2iQ3&6|zi?L0U37XyaBMfnLY>I5@31h^}M|KWv{DVrn`ktzb{{zM10G&=>4$I4h zuBF~&Kg(5j4|skc75yR|6tXMf5xVgIvy{NCQk3hZU@x|Nv3FGK>jSI=XC1`?kS?`l z^0Uoy^Rv%P9GD%cPucMevm9ZHITqHtwO6fN3A5)FlVzH`wDmA2$0FF)cBhw@r*9v> z@uTH>`7ZPWu+9NpMl65f)`qDdWH!5s)!`NGSUaYCVJ{qg;MWe5m=P? zCTZvQoaQ6*rRDkOwjXEW_C*dv&(n+(misn@*P^_p6+lYDLS>#MrO--{&dgJ4oT43N z^Yyv8U%0Ni@k6ynhS~RHhJ4(KeR3YRdDwCpeVM z5Ls(y7F&($Ul{uEjcofLQb*rolH@0pF1^iUsZ@~`&a4ObbZvpZ+@esS>A_;1H6r5) z=iH}7L4NLtId<~c_GNeK)+HP>w4;RCatkXYk&Qhbn>%-g;ngm$VFj>KoPc(eaB8rP zvYxau(QRrwV<#3SCYp`rZ+GggkKeK34fn^X`j2GUE!McQlv0RwbBFRQ=X~u>yvrdj zh%mM(6bF+y`RC6q9(ahFv>S#-++4N6@eNa)8mMCe-L@g?#u)OJqSx|McnvE68}iLr zsGbWG!eu#w9gF1#&+Qziv@OEo+{XD~vv7FnE1Ma80PF5ls_?y7^Ija+g(8}@6~S5D z03kzq@LC+;V#G-)PviIm$j_6xCzmVj&h*{B2rN?5G*!ZKvE`eZMGh^Nc~Ip5@@Zew z%l|d40K7P1aZ=>GAzhQ*N@ztHt${g@e2b*1GDhoV+HulZT&h17cS?`#=K2p7TcSb?^5P4QkE0^lrV zvS%9ZMPi01Qw&f)+u6?lyy|8qyZ`OH+8NCDPxW z9hiIMWMeWL7#emeX;9p`ftk@dM-NOf&eWOb3qSiu^wm(gce&-70=NX3Wsb*hdy=@L zNpwbH8Wgl}Gc!?o*W#YN#c{q?-XY6YFK?2L??#w^gcCOcLK!U(7AG>{EB12ioZf=C z`WJtWDAWtJ8QC-Mm9yx86Y7L@I1oY|1Nj@qx<7Dfd8$4hHIiKg_uST}II&@pBppXl zph%IXs5ATU)Wu!eH364lO^yy94NW2f=N#G?tSvA$Ji{hE=uVYSHPUG5uoB|K zm5%=KhDLl_loap5mhJ|5mr(NcQWE!g_xhovUJWtoGkt#o4ufzKjC>B4JtJg$uPG!C zH%9e~3nIzdNs4V1sO{O#L~WX5qf;Ckp2QHN<03YxgLR>7_nKU+TvGs-D4sgYVx8&M zq%T%xdGe;c#92%`YGYIeBICOGn9=`r@`byj+g=cA=Wj19refg6Z8ah6d%(O`Io^VE z5mH4CBZXAp9M<+)XIEz&FA(bfWfvht_j&=O^Z`8OgvGhcIhP8ppKwNg9wUFJabR-3 zGN>Hpn0q>VhS{68yW_)cKq736OutDICCpXkkwRdEATb@JQm+n3o>y3|DS%57i4)2% zetd-4NlU%3NPTSCgWRgnH7*mZj@kaekXx8-BeG?e=_<$c#KO$-;>RTRPe!EmO=s}OC!gmA75QY= zzWDI)@aCzv7Th&4d30(j(~Q(Pej0fak3SsK?BiFpogl7UCDG!i2uMsw-Dj-dD>#&?cY zJUQTK)B8eUUi=TX1d;P@8wCBc_zI-Omvitv693LLudzVNKe$R}#^J+N=UUHCZz#LR zmvf0_Yk>RovLLyhnQL?lGY)Gm0d5`(WWP`xsp2VQG`*6?LB+44LHyUOnktDE*A_d( zt~9$4s`lT0oAU0jc7i!V3=9maC9V-ADTyViR>?)FK#IZ0z{pV7z*N`JAjHtz%D~Xd qz*5`5z{004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8xfB;EEK~#9!?EQI|B}rD^3IA+yziZssBXZxf_PuNArHXD^ z&~6$)wNev zW#yh3k+H{J-EEma%>CXQk&zLR8JU%tRdMR^ii{iA-ObI-jvYJaJ?{~0%N9iPw}0{{ z0f?-`OGB!gI02D~kzN84;T@6$0eCdtNEC`V)>zTOIE!#GLPZ##a`en$zV)eZVrY6C zYNv6oCn^>KC25M zW`M=)NWAyqWbctl4HoyZI62FId-(TweeZE_2E4@!hY$kgi=-ySk|HCK-B{v~0BDy2 z4l5nPOSBOPr?FDvfuH)Bp9105e~N9ntmqrvfVYcy;Q)tn5(Oxie-0o4O)-V>dQ3Sf zLR!Hhpdhf)A$@oiPQZ?OjmFZ97-*$J?JG+uk{U_+gtO*R;I8Z_5EMZr`zf=gh*eD! zT%8EuUM`(N`R@iufCkXcj+c$GU(s59R>twO#Dyh${;X>(*8OC?;_byTytPOHPB^^v zXrs_r&@CE;HW#Z2&B!71$MF(DnEU_^i4>s`?rNWGTe`BX0JbR712~*?DCdZ*?oNIO zSRwHOEHNVu&3jH1**`mmF#Aa50L@~`g@qX*abk32WXw8upwuoLoE$0KDJ%AVlKHzJ zIpn<;BA5aB@m9&@6iS>O)xN*o7Df775Ypqk$JdXmebGz13dQH-$;p@Rm>z!9d4FPl zetyOw{f^qOuOyO!UB>HkoEn|s(?<*^hfLOo66rh$jkE?QBu0V_J)X73u`O3G+X`Sy z;*i-ubbuEUVI`Gzk&*fcaT=3E2}T+K_AhAKpFSmOdMC)DSYGakJ-6>2*zHEX4dFf_ z7NbX8rxQsjwe=0@wL}PYE19vvIVZqnTNZPhP@UUc;=Xl9QggoUjZiw>@5Y|O%U_lA z@ozLcL(kA}17q@j-0~d=Vvtyy zf|gjw{0HEjz0r?&MY76Qf%hIE1XdtH#9(77#0L=e1Nhc2RF3X_1!tc_RX_J&;qYe~ zv+|Y0Q+1PCLu`hqcJ^?ldXcB@{0vKa8B|21BD`s#bfz4)w*pr!+X`R{Ql`w|g@dTJ z#1t&4YZEKap6LBNq`qh)kF3HCcceIGtPiWCCr1j68@6L?9mAtd;GEB`iI z;j-rn7L&sXkPr)L?nQF17p1=-(vq)UaK|c?f5l7xpOhP(>YUQeL;H64shJ6+Hy}%3 z6tRnGs0L+opkpuTuf?5^z8qf3*IL}~P$ z_@zhC?ryxSTH~ZrDl=alUPxS~gLA)u72$V@LHO{UTUmq;ip>g*^dJb^AuEG})QRxB z^b+AECN=0tI;BJ%)QfcD=Q}F>m4({;Q)gNiOu0JX3wD^|(l{4}7WkIWeG6yBsmnLu z*4aMY5ZP7${XNh(Sk{_$MPi01QzQmp4}18{!~czesaQluWYOBG{kZh&aps4WiVk_} zr0_!EJ%|X8M+gtX;f2dWT^hVZd6zXoXRD*3YZY8`>9;PfuY$=HfX@`2@CHP1j#}wp zl>%_qp|l8*0&DQjdnvVZLLOI2|MS$OzY{TfIyn(Fmv^Q9&%ORD)T+`kgHAYy}|T@BM?H%zklU-)$w*({=7)1>Sp{aO_+f=Fw9}8JVuq>@oYKuI|5Anf6)F+w5cMyRFFn?z*3CMS7vK{z>2acaBw9>u;Nr zO)8LV3m|Y=Xf$t|>s}{+ULgeD+codu1hdlV*MuqmM-wH#^lI`--BcZwLebBSH+g#h z8=S4q5bFqUJW?sV2si>G!DaVQ-XT1E>>vL0`t9CmhTP(c(rpdh{(_z1qWFT0))exnH?yZ1(b^kF}|)O3D>mIeclmuixBTS27V?f(01`=LW1N6Q8yp~{?C!^h znicLnwU@E^8jX5WB(rTfI9_@JWk2q{yUSZy7D5OiWq11ezCk6r|GOEa2dr7~S*`#w zzeLZg?!spqDzqCW0+{qd{}|H#bz<@Qs2KWNV_!J`>B*t}ZfLmX&)4R8VgDH#@e+8A zglwxN5w6e5dk4!qS^)rf??6a|hoXrYT&hq>ixiw;*Ww_fC(Gin*|AgV!Z+D!^8K>I zyS+dngb;rH*}YMO$_sY`(QKVN%2AglnY&e{ibOa8&Ts&_vvtoT(i>whbq z?VR0Lo$v)y1TSev3A5z}?WhBNaN~EfyrUI>!;z{4sT2jJ7+NTB&&fUPSsI}^yCC-r zOq2#?^#`QXAH$^sN(tfnCw+GU_f491X1fDcg8% zzA%RvG^C{xP1PdunluYdz7s{==my+K8DfBs9{4`)x^RG?J5C$&{PWFB}X#j(lp)Y*)J%m9jN3kS{vea8agfWgJu~-8p4Hf}jXR-M#ROofc zSo9j<`u`n+kC840S46rP;WVHxWBpvNU#`3#SN!b~hh_FRr3Fa>abd8W7Jt4nD}Lky zrHB88Djxc0@x|xtL3S`i89|XY3A4=5rritLu6~BDX5VD2y$qB0FYw^TZoZFgjgfHhohJ4-|`ow)8|P(f=@Vyh}JQS7X`pLFHr^ zP+=QDUo3m|0L``r+2_(}Rt#1jBRn7yK;mRfY8|*XLI||bA$IK?HZ>@%5XwQ^A(ivA z>58yi^sALS4juDw5q=}J6b&-GJHlnYaB;E-`)-PVL zVgie9KZf%Ikf;wTo%L_-3+Un~`d(lDuDaK!`yFQmFEwNLV+*?R$MZ``do(`i=O!C` z`tYYYGyEozb0`#6gm)(}0Z$-8J96OG$A0?bH*oZCUEVepfL!&WRrb4dmQhusyj)@b z>;yZ`)~KC{#lBs4?xorI*Vc93hj3-cZ~&|3e;z98U(HrJ|JPcX$X1B3at2d1Tu1cB zpS{Il(OTh@$5Y4X7M42b4l4GvsAFXVRq7B8wHcVSFqXh@0?{H=I#6grA%VCJ%s||L zs0FGW4xfg}cO~L{=nzu^SILeVa?3v_(ADhS3y;EW}NS8lY`}&3R!QZV=h?Sa)sth5wkEs+`5CPy$frymDKf3 z;tU?$i=OCJK&4Bg6kCer3s*F@{k7h=Uo7t|{*038Kfm|%_|!n#^3};>Fs?{tW>pA{ z#1Yw`ck*0@TU~B_1pparwzu~$n3##BD&<7ewhi{Q#wdO{k^9F+-$&Z`1gc%cN-110 z?Zq|T3568c{!hYQ+RFqRHSw~Ii59TBjxDCN!~%uEHiLUwh})Vlv;+flP?&=10?0W~ z%b-$hxE!I?3W-4iwjY2bKqwtnD|dwoKnUu`?Ey%}h?hrTrjB{_MV8LKiEmV)U7_HrC{sq6D$IgS|?Rz#hGshx#bmrl@5VJW(+b~ zm#EAYx$Wd$b}tUoX*b1WJbG|)a^NRuy6?v85+I}u6GL1NJeY6@>5wXhRAgf}#j!+M zoFlECAsIbIylalyK}&6<3BxJqdC+yxOW~(%gE9e^EPMbgWpr8sffoi#V8~gW=OUe9 zO+f92y3U87fzB?3?VXoFmTez&S&85*L^@=_fp;Ji@blmoA=Y4vFi?YuD*E;zM%opa z)%f`ujdLZE3&Z%SaiXQ&M6I1DMnM!Y-l9aprK!@re%{h+@*unl*G-ThN+|JL?;5=Q zKg>-ve(IHHUpcXV@1#3BIL~MIoaEw|1uwc8n&NHX2)yMLK#<`3FQ%Ho<^Vfqhq$A@ zmtBpKIPG-aAGPGaL+A-iB2bE+qpo-U)l2T&(;2X|plC)KVq)cooM6QIO1jK#N;*lk<&k4MGmTQR2W49K_$KJzLIne92ZBAJ&u&vX+CgvI$Lo>@%g z9CqwD@!n|)hn8Sh9Yz}v&qBNevPp2YJHpEAGS~GQ5Mke!H3wZMqjdD1qi>>1}a(%F6bkBCR9~A@=znh0V}WEuakpGaxQN?K#+8qkL}_W=G-T z7~+kX#+!pgvv)#sFD~^6uL5~ef28Fy2}PR`FEW0DoVe9%&?V49_;!0hl#0KmD#O23 zGyK@T=Q?MHjt={;?0TIGLvvY5_m&3FO(8eG0=NtoMFJ(t?4I3AF^xekvty~m?Tfpj zaZ!E5EV_TIRdk3X71levija}QC#G+rtlnR)GBf*3%$~WIhploiw8N`5MlIo09bcRw z(HE$UwJ05E!Tn9BorCI0C{ICQnG}cj^w@*DPA`EMzW2=i155yZjTB)|aE3{@vY+y6 zY+-_|NL^mdmC)B8d0(!uNPO>Sm*ZD@kXcC7TZjmF1_4rBz!yOckHN$Ug(HV3%ogCe z<8Wr4r3(=v8AqFOR9ZuJ@d)u6@2D4bS#33~Ub($=eeHE5X@>$D5yfDAsgug@Vo|*3 zuITVb&Rv*&dLf$V6q7OzNjpv`Qly;&iTeBL7QqYL+&WCakNw2oMS4&<)HB z?cu&x9$|ND6d6sici&FcdTsirx9Xn0bDbQuB7A#(tt?3}K51t3YJ$ zDH|s!)58|{J}ibz2#n@yg%@QP3Z1ht2?{vjJfhu5qU2wcj_mro$IhI$%8zkzbcrwQ z`ZO0t&fzl0XpUjiL@|}8l9!I;{vt& zJ1}|@MlXQ82&Fc-P9IpP|5|UMTxSN|D^f2T^o5g2n55AX6eW_AP%Xm92y)k6CXPm! z7hhuO_<3|=H-34TC>fz>s|XvfuuQ`J08CH3Ao2wRWWW|zi)s9~{EPK> zA7#&9n?7s5TDg6PpDFA_>o&8MCapq}AqO4)7TF)%+zKFvp=OgYkU2<3>t!A|wVy-N z6D%EjO&;2H=uhKYzbLex^f|)b=dP*E4F0}VLOAG)+Xs3siO4JkBQ4G7BFXTpxXCvd zy?+sPxCukcpl2bP2i*kP0qApUjDwq0GAOfZps+B~hSFK|o=HZUBe>UJVd~36=;=G~ zb4Mw<9Y`gzVj?+W(*)FG4i)wih`R{GO94i!wX^A09MbvZy|~5S8{b*}=-E@pfA;}5 z?uJ_l&+b3NdEE))R63N)A=Nj!57(EQTLE+>lz}RxF{AYw2QE&qe`bvF<#J(km-;bu z}?2-$tzj$x+}n1TZCYuJK4rZbvlJPO!XpS@*HEg&B2{b7h4mO+El_c5%X?3le;SO{Y*50qGeS8bnt2uygm_aO^a#S6eKdJw?=+K&L}QX%*Fl zVfV`_2IfmFfc_WO2c6E8gi=DF?4)<~|FUOf@Z&ViKh^WTzC7U2N^`L|M>9_Fw@9Ax z&8+|gBp!@0l%trTMvZ&kJiy)y!<3U!O%(Nimd<{ir!Dcii{x7|hXw0+YgaMiWUGYt z0wEMi3Y=}=qIsIbZ_t@|g^Bmg!@emf&q8q#)I6wG@C^V65z2+QZg6Z{u2Nj~?$V6E zPb$E8AO)77d>w_o}h3HK0s^N#%m!V)^jU`^G zAe{5LQYd-&2U5!S4KynM(|vF5TNgDeFNm?Rsew1imELzzEI)7 z%mm}7D`Kykd{-3fpR+c3jPBc!S>kKvWOLbzTeone%#vby-eIL8g6Y|yBQD~Hr?8`E z8GNt>M;Bnv^H4bnzP=&^*elqT$MN%fyglN@H$r5Vju58gN?Zgg1+fS$L*XP07GZD? zV(tH-IoZ~-~D8`<84Oh&+!!@fKp5B*76VjTq_#ClFtiI9J)>`K2E%|}1FyuAGR zgT-+#gkWx5Qc}x(7Fun}!f1+c~e0 zB1OLSu5aSV>?FHit%$qz;qQ&S`Zc`R84P{}LIDbqSCJJqT*>PU6yM7U72TM4@AH7s zH+b2>>17)F1j)dw6mL6)dd~&a1Fyo)7ohMu6lNjnU>(RfN9X!g*UZ8|+{|ZcODv+~e$zL9i$04vG^D$n4@*I?=U|gbxDk#^aY3n4eh04cEAA3vKYDh!? zSoZ*o(j)V?C|qA2c*dgS1;Qktgh0uvH2ynw6enidbM-H$MTganKY8Ylu(+FcEx6(1 z@9+NIztbh1Z~_bhYYmaq?3f?sk#k46tGTaSm=WJu@r7To_#$4Sgv7gEPI#A7o<8p` zS@8)n96_c4e1@l&IJ`B;*wT@8+U4W;Jzr($;d!`iI!yY3<$%qX$;#EEzxOT!9@>`c zAglNQ))Xhf#bBx+_rb+Ucy)y4a})Rz4^nR34lRL@F%}hAld`mJWy6-hs-R$UG~;D% zlyN(_jCl!!RPB~*{&=I7{;Ssp&orOe^9pCGGYDBgNr}l=iDkC-{>;z(batg1BR6~t z&_!>F&@l=@*_Ihuu5e&(g27`-jufgt>5Tc&#M(I08X28Fov-2Q7yw>|0KO4&?bOBeDgbQ>?`5#xhU zIzV}+gfAUu>7_Y@7^duZV$E=1GRi5@WnRA^Uodi~;cCU>R>Fa^F_NZ9u@sm8acN0R zT%4Q!-wqVVocErkilMHPETKzmDl7Y~kQ=H1`u(de54YHe;YOL_@7Bc!Z~Ie&aW(!) z692zi=cJNy<>X&OR(SloPCn?zk7vY@q|p-1fmc!cUS;&blW^A(6wX6&4)ijLO=R0_ z%S|o9T2OTuI0^AGjGdu&be#IryIDB7M4@pPI*Ex)g77j_YRWwS-PQjJ6ugoWXG|pH z_{aC_-MdGp$A09@7k+24_6;@P85gA9V1>hp;2*frOUDgW01mKTgnD^EF^Q=y*El#g z$xa{1feZ03dN2N*_fAAngm-S;$-k~-S!xJ_*L9lZX%3}+#`*nzpVO1d4 zP~s-!^xl>$6Or!?6TFy(;w++g2JSmZb-YOYwCCdUOO)$J(2WU{sR9}=bw*bv*5G#7 z=$2h(Wmz(X?=4)61|C!Qe#e=amKAD_naVQtLX!C$Zagjd4R->*=kfP}jBu%EygtlB zXOHq|=bqA}9Q$RT`fry)h5{UyvzgXQ$`&4}CGDh+j0M)WQL2p}KFQMFPcr$9Cy5_< z1IAu}cnYF6Xor_QjQWOTA#aDI@VB94IaWRPWtrt62?MeLm1)$z2G!A78to}WyNXPz zXcgfRSP@)zLgbFXF8y$J2fymw_r{430>VNiWK_Iks4#Hbp2EoQxTTJnEw*VFECP?W zmd}6g^Eb4gZnzIXN=?NU8EucSYkHV_(!-_ldG%{v%5MQDw#L2Yz^9Z;3Lmve#05;{ zEb729Ccmi#qt8L*0#4Vvc?P+7u4iq&ZMk70fT#)4DTwEg_v~hTM}gKSU!e15n{;6( zQD+P-%Low%K9sAr()@T=vpDZsHFq9i&SR^N3edQL-Erd;fb)#h zOGLl3Aoj9LQcHeO>cY3+jTBP$I{zvhZNdY_fo@{#GgWpY^ojcM;I}#<+u!vS!4P(3vxVRE{kP>NmM-o;qka|{S*vd$qUGTD9gT@ZglTs$%>x+b=jDp16vmhp=NJU(?71FuSXq`dbZ6;u7AwogrO#{3pOBnlxMRvIWc zjB#j6_~;_7@>hrsy+P@TX*hTaiYM{$0>UR5OgGygc4ZZmcU#^*;`(6Q;e+nXb5aM$ zaOVJ=*iZY(3EYX>iRLTI2lU-w&^Ms{=(8>@BZ+v zIbI!E#Hrvsl)lHP1n|=z`>E^NCD+{oc;bo2v;GbmGD!_3TV!Ou!jbu%JhXJQyuUI2 zzehg$F{GBl`hc+7v`*|fXS{HvzKtk!F!3zS%F7Jg`v&m`=V8yYPX!ZAa84lNyL4ePPD?{ySkg)AV?in{MmwYcA$*7u2*IbH{PcD0lIyMj-v7R@A(%pW zcAREnX@Cb$?PK3-17e~x^pVKvj~Qbk;eC&r|B?yV_3N%Yx{N*|V-E7j>Hjw+fcZLrHAUx$dI#;wC+G*9yw_#l`S)K z7pdAYqV^!##yuusK%l{^%N`;SzjY7qBtk?&px#p?YH_yx*&$hGu2830PP+`yQV67w zJo)64*R>1Y5+|VaftI6`#tbY~I5aoGz^hW-Q8@5juH}Eh2pdOn;CA!YUd&g=CdKxC zgsHI^(|;`^QV5AN1|fu|DgVJ?d0?)Sx?h}|PmGu6nX4_+ijyqLc>NvE*IfbR9Daex zr^fwf4l{V7AodULcu?8s{}RF#rIS}R@kP$=VhkvUDNmE^`!vH}w*&`H!N3W8Jd0O0 zE9>ohwdhdU3)(jI-^r4*DR*lNX1_ZPx-<)gSK*!()!{aapSXZOAt<}sNUa4gi9~ey zb!07&k9Pr&5U7=l*4dA{3+~zdXGWeb?=ShU96CWOnhy-iYi5*P+d?UCfe+woC?_$K zO9LFdIKiRQ6C5zR?vJhf&j_&-C)e)4OVsC@>J%X*9p-73kK^_}Pwj&z;I>zx{3=#l zL`0!%M%ND^@_x~Mxa`-icL#3Eb(LPUr{^Pt{C!)5Uj*hMZcyAYgqe3FEs!Ec z%W^NWaJlREPvljH2aI>cqAGoJEv)xvdM6@Uc3 z_Pt-jNU_&NKd|ukEkDwxYiMAjj>5ZXnA)fF8#*9Qq(S`5sT#}%|eGyKlvwIPX%yIwg77z zC6W?lcFykO(8X~MO^-3YTrG%L{|Y93Oa-9!ChySC&o8@~ew}!UcKH;3|4U4KU>ZhW zf}v@UOX0~neSMV`s#xLtUvE%s%R56>>Hlu%IR_3&z^0gDiY~kf$};i|_tE&Hmub9` zQtBK*r6ViP?HjLoO7Z2yrWU1?K*>ja%l_9LX6Re+aebj6k#>e6W!f~kbat)2)78nA zhx*_Di68G0ZFuPB-wPMBdv-q$y!0TqFYjdW*o$)iuF=0EWBF4~$UwEQiEJz3`_eAG z4Ajq3Vr9Z|?Il`w{UJNQZXU*8f$CY13pnKh5v-*z_PaWV|MER>Ws%)&d0R?ODc6q+ z@qinF)^1oh!o^RH5lQ#aH$G5j3v$8(D5f#Ho1^GA ztT?pu;1fixf8g;lPty%`<=n=TtjpFLzL_vjT04W=_dGlQ)ErEH8RCl|>)m{Ao0X)x zq+-q*FJl|zyDgVUcii;W+CT<0kBbxJ*aG8U|0s>+mvBZvXAi1fS>Z^Ke)F{pGb=#~ z9sR8xh3aSPZ~9N{jVAodNxY)YVt3KZ6vvhybE~I2VtiIjM|y}HDCfeSTP`ExDF*lY zdx(<%h@>18Mkwnf{KhrwJQ|M%7YDDuFqrBo+DAXh$k)FB6EEZA3sC9tw@W9HP9rZt zer?OPT!G{q6@4^s&U;kRf?j~3SEzi$S7_h%KS+wNAtDoe2HANV)`aLwQSU;PZBkMt z`)jokb6>}vMTsT?6X@=`^Go8Q@~mP+j1JE3ek z^Rviw(pwX77e18Sk{iT1U53!gVUb=f(I~wD`@YKPpY6cWSD|(uQD}f^W?#y~j@RPJ zw&fi!tG1^~flXV%+N_+111~fBp*igC=Sj=2KwJmuv9dQO)|Zvncg}%t)lf^nFjACy z$5N3Ui!~w>WAkc7^yb`FWlK>2iRb-~KaLO%CoE2aDwG&ns&N0QeLOgSAQI=2|4&5x zy?7-9Kf6JQ6d_DF2z8g%TBW^kIPI{)5(!5`AESBT4;cEUvoP^2R8D|g?u+H*6wJQw z=SDAh{cmj>58JX@WS&z*D^xm;C54Fr7SEkSHOC}-s^ zf8cc`4$e+8Haie1VFPky$+D}KEm;5_c>Mj3BfKPXl8L1n_n$h*-P8LxC?+Oox!*xW zJG@XL)OOE&1Ncw@)<+<-lGtlqUwDkQxY7)*@n;x%VhV113ChPnG(h=2Gpi5m1HJ2a z^98u{U}GJ$Z2@e{S`u;qve;KFz_p;Ph)M+(X6MlDA#}T%QH`&6om~1Svkqg|>j>kX z7#kS*FGI`KrAk_$UTo4RbZ|0Mw*K^!pV`uLToWHaD8%R}Cz_p0!|bh(#-i^3e@g2^ z-Dm9AR{iz(`ajDVeZ56^hZJo@VFo|+CWH5$g1b+^;Atoy~E*U z0-}x3UZDIp7K-W{Uq5k7O6AZ(prrukw>(C-biM;xNr$p8GqO-+Z)co+wH*gF^50>7 zSw)3Fa?k}+IJ}VA5gxqT5Ff}CRiyCJ;#Hj_K11>73B>#6VemC5PvcaZOX|d}r3t0U zj_qCkwp<6vLxj-HBQQF_$5SxyD)NbC2JSvbgEvUs0-iLhB9)!u!qS{+<hUT3&I`E8GD3E;uy?3PrL04_4R~j~ChZ7>EJMdttFzIMjeQ}O zcb?qGU^+tExh;?3Eja;;l=<^_{ZAa2-pw87cW_U9^l|F;V<3t`C~RIXZ@q-OZZ?$z zX-I7oQ=Gz1yhib{^Kj1#F!(y?1e=;r=cAv9<#Mv>Z+>%M+m>y)GRekcKE6x>sszMM zP=Z)a(whg+v`Qx`Y)mpL+TaFj*u74=Gg&*7Z~b#`XS=FvO9iUN}YdJ+H$3uR!r6 zPNm_ST7@Ez1i}@u$x#2cY|HhM6?lISItFJgPFi&JEZqMr)pUX7KdzwKLqrgqf1&j55MmnpfTaEo}jc#EeXpsh*BR)G7QB zGMWIyY6#_ek~==E1f%P;E3Z<%<22lN3MOC2#|wbVtF3e?n%5*@-Ii^6i>2QizY_K5 zAew`bm*L*i6px;yT|0)=^=|s{#wbBDXJfbEtw5?`l*WH!;B+j;FAh*7-dg*&arT)<^m&#m(FFB=fnE7oZj3tdqt! zf26p>@1ILg2~V=6c5hk%$m~CW2+asX3{quypjaeo{A;8iu{k<vh~Ud8ga5EgQ?Kakzp{0EdV@6sKY0D~!K) zj>f?2B!y|bYJ$l3?yCaNms1SF;iUlK%EZlI8XqiGCMdImp{uq8Hk>Md??**WTv@8yNrQ|m9en}4=M`%2Yrx<+yj})x zvz%wSa+0qR?%NLSZOct3eQ+lp7x24Y!4xk-cbmM?k|i(A{1dKNOy$S# zsokZ<7X~Sr0$z%YRzFOB?>F8fZ)$;b(vc{K(FUayWi+S1^rGO6^FJ6lp4j*bvTriy zInW7Sw&|!kcHlg<2i}Bb~zcRn&g zqy2+&BQ&D#oBXRD%Bd!e;xUHqUW9uZFmN`MZtm`Xw%7A*xy7Y_5!Ov8pM*o_soge1 zr}`#N)mME#pCg+Bq{}#tzyIDncaDzF*BG6t5+^F4YEj5d6gAyY0p$CC=_SMU3iq7Y z$33U_Q)=jMchcMe0m7Rfmt5Y*y_H-zlnTFl7tv7*#0Ou3N6rSPpGvbVCxS+l$)-Yy z+p;Y;we*pApprl&^4M`W@HBNdg?ABx81F>i{$Fpx5^#8u!^K(qJr5q=!(%58F}_?w z*)U)8njGuNhTo7GZI^OTNek>=8f9N&Y`El#|43@3#d=!C_iM1Q*RjT+lmJ=Bm(MWp z@FMJd72=s-zU1-IZBPETyd5MfE#Dp2(m`<^cE85JgEQ#i>A)_ORi*ac(oLG%gn@31 z)p1+@(|yf}-OFE^7Ue|uwj7%{UvBhAfkXLlXlJO;%t_htKLol)dL`9-vIGb3K{nBq zGt>^wz@hU{ehu_e59Psm2b;100=w_7g&MmThuB-&RRZ zV&P?kcLL#j_vk0+=sgHXAND0U3zb>?9WTK>Z$RlHO4FU_U1;U1YTfHpVBXH>-Hzmz0q@{o_r@O>Y1e*W{PVGV*O|kN zHV24gwBa1saE_QXrC&+OvA_w<$LOM244@!LS6LW#N z(MmeFZW7V@?Ypd$iJ6=oWO8l@yO2uYBV^frIl005dz3zNML5!Uj`qllaNq>!Gk6+o z$NIPB9V^n{L>mfc;pl0agU^%3bASm^flVU$B;6bY(QY=r{atrHEO#xBvSVR*!yJKi z6~GWx9y@&-4;|mny`#51?v;N$ueuYMd^RE`bP#(GEmGMesZ6uu{mZcPS*R{zoZC2K za9g(JZ7f0o(M8z#4C5bI!j`5WN(0-KN?31!%)|5AlGPZL6pQi0$s6+@e&F?eyz9gv zhA6Ll|8*5W5zXjKmGRlZLfn!cbFNqTY|{>UYjCQKADV~pv#|F944eWnhm3A?_ixL# z+*lEk;0vgpfc@uDJElpC=Rvi*WTESxziI(ajZtx1{U5vPV}*(N!L3;U0AGFKc_AZx zkI;qtlvg3nD9t6Lo@1+0;8BMHo*zz*%k(c4<6xcH$5`^q+ao$#8yPWfVTds|);?aLQ$%;87 zEa*8nGL7GP91|~O6Jq1~nJW&H;v7Yx)i)M81@^Fqz3icUnXQ2M@xS-Kha#LZR3BBT z;qbyi-u1$x?4KT|?3YU=ZC*x-eI7*cHuglW^HQ8}B&tJWD%c+2?s8j#{{HaZdxe;?#J4qFY5Dlb6ra1} z3!EA|hZA(shTey|+p9W6p<9w&jbR2ZXc12qzZ=omN0;lb|8;ECy9Pv?%Ai8-84f^* z)+kU$NF9g{-7TyDghR+I%Hbub1R2$FUFVI=kYC$MU`yfyCxo;yq*btSK-B>oN0BzF z54q9clN+4A5gdN~^QCm*5mF7N04VhS9YiG`65?=NDS5XLLO7VzY5W7lv-;=9M9o)S zl}ITp7K_6>fwLZsNHTyzXsnhIK2BIHpJK;thl(^?AHAZxuMmgy4GIGzkQ{=!B*Z?Q z0ilBnbgL@#1}lIS-q7L{3_Xu7PIpbR?K0)pK!ig(3CR#F-VL3JEJh9+Du9i)4I93o z4bI<40jzib?&1Mh9jXk)CFmTYFtQhOI-|r}_Zf3`t*keC^FdhM{+@WB{H11XZn>2) z2TL5nd5n`NG`hDs$!>L?)ODcPAgNF9kEFgsIC)jmOnQm(1?;H>=70Shu55^f#Mvw* zSc*-tmRwG*l^MA4b6oEJJus9Pj^@AuM?NwJg_$fPYkLc@CFxSkWs|5g!m1@rC9=t{8d}Mh?J7>^JQJ-rFZPeEYR1c3e76u5JDkbpHl;K)n@XIR{+^F2!oRe zN%0JrV}!l`s``c7a&_WlU)h~1acdu?<=c=hMtF_)c7ySA^D2NaetPG7QjivqD4T}> zx4MYfAKYEq@$(lhs?_)bi=>bS*uX>KWRY9T4wNGYrXHLW@56Weri*CuY>;-NsfvXdjS&CkC z`%u|9^s_4Zf zgYRBE`g;>Y7x>DZU*z;y9qBZld=6zMc`=RIwLC)MgcXsAzSB9g;hnqr&fPRd*E_PD z=3rq+lna#wa>uLKqIT=1O>R+oZC`!3thRebW+M8+Fn#y#raQa-Shd?OT(VVJ&5xgr zQ|WY9`Co4Gha|jBOq_f>db#5|o-cf_N^tm@C(rdWYOfTTEiN9p;qyrkSTD6Hep}%L zV&~Ebg*22V&v6wRufqpklPW{Ke3tS3lVxZ=BCQacUS_@Pg}idp#!Cb``*-#*&LQ&> zDw_usZ`hI7MFbhb3P9t~coBeG`MVL)g*xwfErjl|O;WyP$;XlK&M|jy6neAb4KrJKP|kJ3Y<9(0ZWTCjubrZo zgV%K&>U`eie^_hV;3Wu&H>N6t_>O{>|0u6MBz(vn&;my>m5eks`{!e-9Sxoz#(U|+ zm0^&47q;1JjP$N=m0e@PtSonQqkDbT^Yk@S2t;4V(#tjK6|RVN|8~=hTyw#^k}a=I5}fxkip2Mg%IZJax1f#HiurO%Cw6F$WNbO&JtqceJ$YVC z9DU$!yf_M=HsgKla{?*{iFZgHhsom%xHQaztYlICK%%e0?}~(ozLzC8SpxiO!+{%L z-;vlBN+|>i??nLqWUahzzK3nuq^RIc>U)o#6d}QRgN!uJ`gP4^vBhM7t>9QT5=VG0 zB_o_Sc<)i!e97JO!mqy_P)g_9)uWXecgOJOPQAG21Ec==Xb zF6b2B144M=Lb|TMJnr8KnW3>V&VOZSSD;fdESV-M7KpUHqJgVBeC^4*N(rpjNn2cD z*S)^;Xd@_@0_9E-nQAODvj$9delJ;NTi$xaqk*q;;-GsL@oOhtYr6H-QEp1`5XLw zt0wk(*H3{ZX#L`GnxDJKfQb-90m3cJ6%}CEmTND;!5=1q@)?n)mZY&IDHWLfk;kY# z0PB+CT+i`yEhGVoz&tGd{7baIG()vnMf!49J!l=)AmMN_1+S1!3T)+JsrLU%Vd@?; zi=u(nQG~G)qpK_}C89Pe??d=3R+#w!#OmklPe5?N(cnF;xXsvuqc8>x0XgH3zs%1y zJKt~WqsyOv6>_=jCjug94W^_dRDHQS)i=96x#807^W0|SQb!T=dvjO*e$z|ezCRnY ziL)%rl*uYE0@T(y{RS$4&73MAC;<)e-Bs#eK1Y?z^xtg%2*~KeA6Z=Nh*VW@u0R}> z(Wz_V;sKVF;lR<|1CTu6r4v?!<=d}? z{fh_}`p)BoknWDbqk9HU=(7vHRHT)FL<)#BGxb^Fh50rwUF<~8=f4RZqXZU9q8+xR zU_8zm9HQu<6_+_S-BVjUe=FC|Ko&v@6e9RLgzKA78#`id3Rw%7blE=4fhae+(Hl!1 z&+b*=2?c1pa4Q#@oAF@dvLD^0J_aVCp$uu{u~;H?B`?U~g^R67zHMfCQF!l=N+Ly8 zu}22Nk8jD-f|UccDF=hc$dp#Wvh&V8!Q_(p1KzSIj5Qwf7Nw->f-W5>gOg?z6LU~# zT}r8}ww!bt>GiGo%x_m2g|Q<$sOuzb|2K4ChIi?N$*d)ZLw*FL5Z)0vgLGbCjAL+c za6lkF=vJWKHVSn)Qq!R=Esa8(%DZk4no6>DuBBU9ZpQe3UV06XE=G20$o2?yMj)*r zU3hVQo4tTb-n3Gmh-y%eZ8 zLMwH|I~PF!@^~~}=yekgoB$Ooj54Gn0^SvzX1!D10jtM43i7Q=SH7(Do0@rBERa^9 zaRkmBfK$7nz7JXjq*s}bXPe6G<`5tJA35iN0v;=p4hlLhA&v^`o-J?)Z@MAta77ap zOYsqF99l)_Bc~6syFN&v74H*5bXERl>8vb{jy6P-l@PAjUNmkl>Gx&WBA6;H?}Ib< za^}z;n*K6=ef;PQL!$VgjITBIysHK1vjy-8(WHJQxBk=lBrvIcydF~+! z^@D-BJsIMnSz=tqaC-s+4x(mOnrij^R{efcDu^rOVzjJ39=95XUT2cZ{C|14GMhXu zOF@%8pBE*Lx<~YWv+I{@$u>L$k zXLtbDhZo3+I=lJ$H?k8lsCsw8UjdK|@XjHOgk@HaN56^lH|y+OZ=za)4QH{_Q7$q0PB*&)dvp z&r=gq;bBiHXfe@$m3(jnKcuuk3Mc>T^E975OSM%1A9ukgm%qo=i6^9j3J=Qnh&^U1 z%e?C!ejT!K3s3$_`p1QEuItf|NgGqPMo5c7&2phuPnForSMnKz6beW8b&( zZg}tYM6UZcYdoD=hsm$s#pu@`>N(Mc!y&tisUxEhi-3IvTE2mm<-Mo3{z+O+^SBVIU6_J&VSe^NtBC`9;|cJM;x>W8uwO#^sf1AJpfF%5>bxb@Dh>{xOg|bavNv=WEbW6 zyNHsJOdV(Z(3rqa? zUu{uI3uqgNH-ZVhs&LLFTOhaoBwRO2h2r*;$TdEqyzDIfwRZ(13`owv{is`+>iS+;9TCjbWb4%0ce zfNDpmE418Iu_$P_-v|7G1`tBr>Ae?1Tv?$GD;-VMV(^~(K?eHvE;=z|2a*}DSvUb` zL;GTbVOs+skz}~;)xe>QGSz#(#X0J+%41MC39*-0pJdAF7GC~=6-KHDyvBHgc7rf; zA3U{}g)iJib@?8mw2HC<;iHUNN_QEYoDbUvghLuA`El&@qbz>rMF!(C+!;gVIjGhl zHknoRHlRKCJ!W3(9I16^6()XUv!WyYg-O=Bgm9PtyIB8Rha`$BL;~igmxBTjVVbC} z1>pJ!b_jaA2&KDrGxPLW3bIDDQE7z!dpZZ5j!;S+PLdQY#>NzL^AF?g6~9U=9WC8~ z9U9as1&}qq>y#3NCwm@BO-(FgyfIrQWY~;!wX^`RJ)F33S$yQy^#yeI@&LAocH=N} z2);N;{mXl)EZ#@d*@YyejpqB??%vWXJgOH`@_?_RY>_gOxleZ(ZgdcjIw-sXY6^6_ zyKQ(|NKPJ#3}ZNhM6c!ExO6+ZlDmJUwYpJRF^TCUXyI}5ZCDCz>E{BxqzQsxweBs` zjuLF=$}BLcBfK@jd;f5}dzaG8&JH70$vJmL`~Uo5WmUsCm&E^9rZi;EsT4RlPuUdk zCaB*oee)_sAP|8ObxRp34&}okyUatr?AfGcX;Qq>$q&H?pa)AKtS0+Ffc<8Z$Q)By?0@#fHz{-?Vil7Z zgA=_l*6L%c(k(%jar{f4mGOdG*L3Y)@w37tq%~+Bg2i`{{>dcE&mN%GdJvWF4Hag( z)aHHfxUVWjKIyG?Y`r^?cz$wCDGe8}h(+O2m!D3N5Jmy;4fd19?YEx}Re;&NZ zOf~O&W}M)rPkCLzIn{?@aJs_X2us%ni$u-X3_#XL0QZC#7=fJ8JZ($AD@cGs`R{)U$ zuPJm&%pYH3*oAO;w<2F=k#rF&Igl;}Q&8Be=-|xfR`h9F)4(9TDSTxY5+JH^umI@O zTC1HOxre$AuI*4xXRFf2+I6Gn+nAWMZ-4;7xZ;+xUAu;6u4KJL+8Q(`Vd@?lpW8v_ zmBW;mj}mppLWmF-9y_yyychUZ3$o8sXGxPKbS!aJpfg4t-|a7nMIlUuqN$>7k@iaw zE``#E-UaGKWPBF%Le|HunEW?RdY0eD6SU70m^rOQKi9(s17)kU^!Z%TX+(RW32l&h zc-r}{pGxKVS7Z^?ZCQr-|BzzP?WAgpk?>EGN1ryghij|XulZl_q^Ph`qrKaa!GKy!nktYg)VcR_HrE-2tG9jI2Wo+=aPrnGp4Fitq7uZwYQIfiU#hu+ADbB zP-W4z^m|VR`LOlg_cZZxGvk_ZH@oG1^GMG9CPI}Wi3U@H)(%*F7c;+KB|3j6h57;F zWDvD#Q<$%4A}ihIg`*`~G;2*pZre%mo}--j{m3|jRIj*3u9dEG`xG9~rmyw=xny})}w;?Tt^;s!;&h*T~ zmVdVZsw%hYqqtjp?+30n6lbJkWXC|y_V3lq>iZzu8yaA#NhE?pP6!23WBtaY2Jcw$ z1n_p^17@O7A#Fp{+6%|uL;F)T;!}r-TYHJpQkeXGIHbn^V{h`v-K|@pQ>?e8ClKy5y^uYl*{3VQh1ugnA zz21U}FILe`dUpju@f$3;q7ohyg2)SKbifxO1uHE|uTp0DUa$bAks`}m>*JaR4sw+~ zaW5RYs=}ntoh_M0lUY8K5ZPYLf@`LMW=sU<5-!k<-1t4?O)PmV$V+GxiR!!H+&wIQ zqC(~3edxwGDjiyZU-vn>&q+`c(`lfKicZv`IoPK3;0S|1d?)Mx4CrqjWnXoa*Z=ir z8Ja6FW)-fHAfpK3Eg~z3l5g9TDI#5r_Tb(u(E7cXD1GR5^z8)@uY;O@OIQ7zHqVJr z2t(%EwOat?DZ0$sVJkqZ)2nYNMb}pDVl*;lAOuu*46=Cj{VafkxewqfFX0fQ)n0X` ztRic_bRAb7lI3Rb7E1xvg&4>dWj!ClEnm4O=T%e+QUUWraOS zoIY`uhMDzopn=91gVBN{U1o7`nb8j&B>wi>dbu0L>^@%$N1}J}#=m(AdBHPO8-R|> zN;#wmVX;zzD`zJr2s4D9FJL>8*6(RbW*81HLT&Dbg;~9|A&l((`t<-dd%f%a9K8vw z9r$`9i(dIG=pXz!Dyd?kc4qfqhrpAxae z_zstKJk<_~cLhi%VCim{dYFs96%n1jhq$#5+b9I$&YZ$6_gqWS;|WlfR@`QBw9U?M zI86Ngx4~{8vLPiP%Q0sd_?jYzzwh15>}YXb&QYh05f+KSXVDWc`mje%Bhx`j?IY-u z_pnlvd`U3F}`+^q$%Fpu(k|vi;XjhYxpqdeI9>Um9U<7r~(Fx zTg|3Fx*>sAj~FDozv$z^FQ_bgiIo_W?*B8FWwtGBEp|{g3MUAK&nmdy^px7yOY(eW z9Oyi*4@iL%o;2|kd>Q5sz{`)Y{Mj)E=Z>OVd%zb_MFGNQWaSR;K=~NF$5|Ds5Z9fvScNMcg*|H5~uhCvnpbwx(pBL0WS)rO7fIB{qtiFPa8#qjsrkrsw9(~%~O z(OX~(0AW5kA#pw?S(KDIbOZAey+TR)+;G-FQEb&h5fYf>GJ3M#gkEh6ULBJcQErQ&6wEzhtptWw7D1yEqC$#na!0f>724)nhFoit6mWu3Mq$m+pm zJAj>l*}LK8JDLB3A>#9QP;Bj@;A(WKxV!=%g^8P?b^&}WX_PJXoe6vX{5|lYQ5Xu_ z$|SG!>}3~G3S=v>?BP!}VgGzT?E9=Hj?P1LM1ssdFnwf z{{B#~1ZM6Egyl#IIV@<1dsbc&FS6PX8S{z;W`XBZw@hKU6{4iRV0%G6v_8u_iPhCTM3V43(l zcjPUR-keC2B=m_O&tqTfNyCpkTqwi}Vx24Lhb!)Wr4-INq}J4GwJSY+MI)WR+ylOY zL`o=>5X%Pa7)Hl5v)Lx17j@B{%; zO4zfD92<}5+g1v47vh?xptev_a0m|n?T4BBm&a(nw18T66jV$kVz9k@G3@Ig zw^)W4TlN90ZvphsUw!+bjJ`{HNRouord!%;u=oBQvGA|-c(3HHby?<_avG9JSiFPQ zrv_<0wVUG7Jt#MT_fa-sT`yrR->+t@HAY(&RFiav;P790A3O>aAoUidRKSyvpX0jQ z4GrY5|15O~ou}RQOCjn(ybCb)zuu4icW0P-@)QHUNXcuI)tQCiK?R3|6AmvRX?sXV zsB|7=@u`>rIfr~uA|_Ac{aHk0`uGY~r9Zz_a)ZZWze09vyn=Pxy~QZXhFgHVK0w|` zB^AaRy!RBMD@-C34)1#&E3q2LeHC)kd-^lblgnf70-S}^Itro!ogtXI13vcvt*0ib zE#HMoM@Ur;sZNnTq~-byS|hc_rxL3j^U%w zWR{1@1&|^!beY|QZy=}s_Joxn5^xCA|FQ?!US#pBO**rRFjAf(C4xz>gE<>{ zq>qS_n0Wp+8lMvk)FXNoX6N5&Ffg!Ase# zv2Y!S$0q7;Tu<3h0j&0drfEptky?@@$<+}F5GMcip1b0Pvee{_==--1K0B#Oh?WI* z!NnsapBbg`>~03@51=~L;P8_MCk#1-nH3IaK{$gq23gQ7#!cFLEC>JE*TO>tP`B@d zP*{$F5&1+4@nugImQ6uL8LhtnnK=eMpmMuFND#U^sRJfOB6bUQ{EfS5{`)CrKXrln zQk!xq3s2=yf{b@Cr`{D-qdF5*7E82VEFkS1g(n_@=q0>55wubqgXxtkmv13qi#2_| zh`#DdZ>_9W1{=_gT?4sdCtwssSZ6V*VR3Ppt8oJgA-2f%_ZEuittwaAZYQ$HTE6Of zjWHhE^u&G~mTt#=>Ne7s4pXe}$EW46XGfxJj1(F#ZI}OFDv(5ol4K#OBX6&;_xm4! zheAX!IPx{hgUqa}uKsa=5C~1LI@3Hb--JD7WEo7`Q7-j1DPaQW9)7@sh(s7QV^IF+ zB!#%d)E~W$oiz-I0(jl6OeDP|a+#k<7(nBb0)_e^IxlOGC*W%=(T)_`Jdd|+R0e#r zBDfQ-gz@&O#03vrr<0=PEdm|9hH}LUAWc)Gl2`4>dn;IgZyMxC8BLNLK_Q@raC*P6Sd8??VwN0hBfKa*1@GV9yUd0Jjmi%NC(z1-q;%bOvaAMIhZ-O7Fi9HCYC623!M^?Moc_Ht0Hw zhcq==5|z5~Bqke8D?tVZ+WK6&Vg(?T?EXyCbVaDtS&LkS4M?pIdCmeV6u4h|llJGQ*(V1%^^ZTvz8`o3-dhY#fHII* za_T-z_S1uuS$0=N!%i6f8~YfZta9pio<}cBSWJjT1TqSWDBl)jicm=jiKMc02hFc& zj5&t>poH;ds3i#3p%6jZ$}A|EC7@({fHF@phw!Xh-6}Y+Y1*rL)cshlm(Fx;#OIka z;Xc($wcAhD8kAh+XX~HyS4{@gldUV)K1#B+R(H|&g041fcuPx8hLV)Q9)jt6ng63f zmcP7@cs4lvR@6t#E71sp$0EE(MwU5;qeS(o6Is}J+As}+SA?oQ(2tnWg%<~Hp zpbkvI%rAb4=}(_wcXN{Qr4b6J6mS09&%rB%;uJOeyWk3jn%DlOpKnGeSiRy!XK z?>)}t}p^Qqe1eb`EBK{u#`R9R^x8I&IKV#DE?}pJ;IM zV^6aGfBqVHH(^q*a$x3FYYd@wof4pg4b@;KGgT7pbX?FFvoooe`Tj z30B!qvq6+li#y=8ucd8XrTDmj!wMmeBlI;`;x_x9d``A^W2IOub{EX<;<;9Pg}4?L zz$p!!0`9$bh}ps~qjv(j2f9?;1mX|326WR0$Tk z^JF|SS@qc9FYp;OSCZukECXj@=2xDl^<0a|bQIS%flkXviY_L$pfSAq%b($P|9C(W zhD_fsG*x(u7XlNKRJ%n+(4GWAJfz^8cd*mInNJ?W%{3_}MPfr10`>@W@a_p4BEZgr z9i%v0p_wXX(gnr}HP}-^6_0^jrbmTy8RChn9w685Qd=51U094m6kM;rFGrY@N4Gj2 zapeWQ`meIspNqEUmDZc2%4s;?fjbMVKm_@WEQBz?wE)T>2UP|plHCWs5v;)1G{K`A z_R?ASF3QYs0d4@=cfiTJX#ep6%<;oi8heqZhEWbHLHP(_a%Y*xX0c1T=gIx9D>vugiI;e7_5eqaQtI`MDHn+=oN1w)Uc0vDChsZ<7Qj`Ba==m=OiT|xclhK3lsVr3AJlOXp9*MHVQC+nyo2RG8H3aJQd+(PnTD)5-&eJ`%iNnqR#Z{b zsW=vg+l+keUaEg-UtsYWpo`QA<3g-l6EnRN6Hzb)XlZyE-uQ*jF?hDXNTW(@;_QCm z4aq+6yS9??bb#U6h&TTAC*k+oAyn8Q1F1_yW~*pS_|S(qzs%wezDP(te!RflAALWK z$qt>Gp=Ddye%tj-Zm)1itdN;cV2E=4HpH1bx$uc9ymAOScR*(>n7cmb$Gb5U?blnp za8MES7iYRuGBAe$mXlBz=IZ5`B=`WXQY7v9<*u#Ybw}j{7i6H5?Dyix0510+Z%}sm zmXU6fO$KST34$SL-43Vkr~RoTM5o?^u1_MY>V8jox^><26kCr(2+xvlGCSC&_{apc zj~oqjT_tEs0;)&2ERVuJzyBiqmzgj^Y)DIj1Z3u{ zvT@k+*AK$69n?QpL%eyILVYjN={^eUkew&#w_*)WCM?KhnnR8qUw<3%ckc=lsR(TX zH(&35y?)NU2NIBNg2(?e%bfZ5Uu5uXiAtl2Hz1Vmg|#jjIN9gcQV=C{GdS*E|Lqqz z%maw;7($MQ9gfRu3wyaDJcxkASYoY$|D^_eu*RY42RZXgpTP-7d0C;-Vz{4|h#Z0| z0&SGm5hAS-yJ4C}(D?K+6&u0rgWx8>_D7Q5%9P#fBpJvH99B9gMZFMi)~CHUa4IUeFbZDfIbSDLZo3-!V?jD#=e%9{>lHtXs60x z0tFwGV4%1HL>i(NE((gpK-uoaUWl0crCPs*H|Q*@~tlKIwFEr2CSi2h!cNDsbcL+tpHci=*LiMKrtvmrq}X=1U2 zx&Jl*1&KUs#RWjm>${o#{W9vpJw)}Rh;)eDK+X$W0)dssdxY~?TC@vG)OWNv^5^fN z@SS^L6iiH@(aOFb;JBHwZ7njhqP!uj`g6b^w0Yy7eu|-sgB0hBL{?w&asriC_WsZr z{5aerP-;nV8f{|Atpa0>3UB=SmuURbdAJCo4N6BO#$IOq&6J8r!h4T^LvYt$`8t{h zVR5j7DbYuQ7{*IpccAx-HPqrk^wfP!eR2R^-Gfesx{hOi%{T>blsKF-2;sx(AB*5j zUxDAwj!bdO4CzS+T+3{gzu?6j2as?T1ww^~PNUfck0>NATcTwC0Qsg=DHw#f7$Jnm zIfIlMf3v^8-21&I^f}wk5UiOf8KLueN70U<+oNC=Mu(TDe6W}hCL7>71PeSY`W8!j z683)cz3|~(FbZBdq!3v!udggrxAsCs+FAg<2Hym6fVWlc(vyq&QK?9mSu zIk^7=y!MYii#nE4X-C9erlP(+l@2R{qtbOGCA*hsxL8}c)n#LynK;afi39Nn-u&NgL!jFfQBaE!>&pJNH1~CBzTGY+u7TZ z21@AeXoAW_1-syIEkjYq;m)_Zh(L!WZ4te+3t=r8uiQ3t0X5hx6uSs~a+zbl_6&PwhA4Haf%sDz1PR_d zoX@crtH?hft0#~jE4Kr9iSJm-Mk8iZW}iGou~bBU@9yB7EJ#qMcirxM%>H4)TZ<^F zKpG!Tg+(DU63v@^<=C7=Qqi^@ zY(+45cyd*ht$YuVN7wo)1b4u<+{&=%l}Q^|l!Ue8sxU>uOYD*jD#U}eDS0fwe5Sm9xad&TEr-$sC0@9EtR?Odv7xH)5qWq0>}uR zQw!l8SVTx8@(QoO#j2ZXi~{a}qkreU)bCoRRcZt?C?-TCG7#zkO}&ixm8 z*mR5g-_5;Yzy7fm3Fdo*l#)iF1$R$E+#_i2uKM(RE#!UJ0$S>-`XIv%w1v`3c^I&L zH4mU2L~Q{xKoY#nd6UWiC3~wE5;(e(x^Cl?z+MRh>P;|(YZ3ioidTlN_vZ| zK&QX7J&=X(MX=G7H<#T@%Y5tNL0Cy8i5XZZ(s^p0`G0X7W(lJw3kizuKQ%^5tXNgN zqCj}oBZ|X6{2}58YRuM_XvGPs2;(8gRERuWyhht{C2(tt#5LP=d`ePwA-_Ydg2?!+ zLKJ;bAaUiP!5u9d9Pd zeE{+*!6)719zv)Pfh#zc_cb{BH=hVZ9;H5R&Cs*G5=<^oY3D|-$T8sa;QzxMr~cDR z)aC{#`)UZNWZyJWW2D{^}9S3o2Q(lVj@~6ra~Gu!4W9?aqU32d~3!xuSQ~s zG*VqdPUxFfT-RG|v|(1)S>9zh@;Cn!+(&SZl|aQ=Z}sI;1Wg*^LBSO$&?!)drGImZ zQ@`~JBlA@%?JBk-@KW@EL915&s}+d}lQCa~oktT>F^YkDMC<7VUj2tp!tu~9(k#M& z2uHSbZKfnv<-Y)n)_KkWYD`zd~C7xM!tiE^YQ7}r4v-;Kh& zZGdC_A_cY}VMqoAAVa^eS=!7I606#9{?#)?Ep^2TKq`$W6g=>TQWELCM@sqGs8I0Y z3MrAK2aF(2W4OSIML&CtUdy4&EL+(wde6l$gk~IMAkjNO1TkKLC#wpaMx8 zSbQWV$JzEre!D%VQW26r&Oz&6o?!ZuZ!p>@QfLV>?vVZ)l81_~P0~|ei&m`2A*@(T zHMM$7>2-MHr~f2mmehj_LiKyJ`_eK!Ap&A#U?!#l?1Yi;-^bv4b}(ICrmY=Op>U($ z-gOu21y(|N=Roj}sBSl{bo}Lx^b|M?1*6eEx+*82Yq!1k{<8p5hP~~xIP0CgQecim zSb;VYjxTnTf*@=Odm+J08AzZAqlYIk!s2g4D&SivPDom*W1(c2x!p1LBaeh)>?Od2 zsF=#52&-0VO^T$GvGW4cF#B^aGW`c97^;_uELh(P*k9ESx%DoXl~x9jcdFc$m4NSt zpZY(OSS7I%v=)?oOm#V;a$IuZ<6nYjffS52BqEFS_O+wGNuui$2>nv|zJm;Z!vX3g zi?KJJHTQZ-?&e9Pp?GL#=!3oxr|mn!2|xn=jYVQxMENTx*m2e%mG(+U&q}3{BB>#E z-?fN~UcMR=%X)>f5$0UIueDtm(2AnL-R#kwHQFl02z&F?hufm$CVM!-OH6da+`uyK z4#knb^8_3v?0d5u`xQ(+U8Gme)8@cvXh=8%7ys$=n6E4|vRtCli32%ktnnENv45ZL zyWX~IBfigu&+9{UOT>Gi*+F+CW1RuSYYE5y&nMwI&>c-|S5El+L50uwOG2^eFfK~K z9;p1K{fz(V16&+y(TYqLWRzF#>919BD@&OCmX=Ckx9V}SuMpR(+Gc?^w9YP}eS{Ew zwH7ZU8IgSVcd0ba@G|2^7HLyQ`+Hu22AISUQCsh)X8 zqwA)hWT1u^*ei&4RA}h58~43_(q~MI9HEeZw$+rfH_x&GDVASw=yk;8j@qg(>U{hQWG?0b3%HQ9$+i2uB>?y!lEg!oEFn zIk$dpVO-`|S8|>Mok4~(tV5=7ly@14XbjGyoTO|@49-+J`;VW2PXo)L>NqB!@fKu= z^Q-(7tqpjt-?g#ra#rhTE&Qvf1=ko^39rW~LM`yV)ht8iTv zK({V{&+^y&8VdFrj3JpPqzHpkmPXWJai~M(kr8(MrT4%N8&-YS8)E$_y#2mVAqk+G zhKASR)X%*_^Qk$8nq^d-EI(9c+o!cza;_CnCA-?1_mO@0^w99|qv&EvOA(bb5$FH! zXW_Fxj0-2ak%=|mc>XwLiHjZSXiDYJ>|p1gKFZuklX}#FQl7J*`e?jvT`|g$RxE>$ z-WKBjz6Yc427#cL@DvgzNv9eJ197}T6iqm!?_=KbL6MrOd~Uk7`gCXGZ6}nmKhz`{#2DI0Ywv_6w+EOH^AH-A;+L z?vhyM@$ws58A*g^$|hz@_tL(USm4J~3MkE|$jbK=o1L8xSh^`;zBrxj{#$FaA)xCmu zXrG{6(!~+PTDpsc^DiHJ=vDuTdq07EeeZG3jh&}mN?v!)rM;6k@dUzQ(hdVrnQ~jf zm*!X0s?U|cif79$gk9jrWaxxjSaL+27lw9}a6X=6@O``3^@H~WdaNod2YS9T`(8GG zj`YG1l;|rk_j6yN@QPt*SyM@Xw#imK-y@B@!K66(vs)Ps2>kj!Nm14lEqR8g%Ut;P zPs3+{X6Cabw*xY?VxLGUyGj5oOn~^#QTBiIF=i&3ER~wrD1BQ*kF9jLAptwWqdI51 z{-70)m(!qI;HwRa?E=b4(zLVg^+k73_4?U+pW3p0{oLpoPV9UGDV)cp2JijrQmK^e z

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 qz*5`5z{ [Install Flutter]Follow installation guide (https://docs.flutter.dev/get-started/install/windows) and install do not miss to dev tools (install https://docs.flutter.dev/get-started/install/windows/desktop#development-tools) which are required for windows desktop development (need to install Git for Windows and Visual Studio 2022). Then install `Desktop development with C++` packages via GUI Visual Studio 2022, or Visual Studio Build Tools 2022 including: `C++ Build Tools core features`, `C++ 2022 Redistributable Update`, `C++ core desktop features`, `MVC v143 - VS 2022 C++ x64/x86 build tools`, `C++ CMake tools for Windwos`, `Testing tools core features - Build Tools`, `C++ AddressSanitizer`. +> [Install WSL] for building monero dependencies need to install Windows WSL (https://learn.microsoft.com/en-us/windows/wsl/install) and required packages for WSL (Ubuntu): +`$ sudo apt update ` +`$ sudo apt build-essential cmake gcc-mingw-w64 g++-mingw-w64 autoconf libtool pkg-config` + +### 2. Pull CakeWallet source code + +You can downlaod CakeWallet source code from our [GitHub repository](github.com/cake-tech/cake_wallet) via git by following next command: +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch MrCyjaneK-cyjan-monerodart` +OR you can download it as [Zip archive](https://github.com/cake-tech/cake_wallet/archive/refs/heads/MrCyjaneK-cyjan-monerodart.zip) + +### 3. Build Monero, Monero_c and their dependencies + +For use monero in the application need to build Monero wrapper - Monero_C which will be used by monero.dart package. For that need to run shell (bash - typically same named utility should be available after WSL is enabled in your system) with previously installed WSL, then change current directory to the application project directory with your used shell and then change current directory to `scripts/windows`: `$ cd scripts/windows`. Run build script: `$ ./build_all.sh`. + +### 4. Configure and build CakeWallet application + +To configure the application open directory where you have downloaded or unarchived CakeWallet sources and run `cakewallet.bat`. +Or if you used WSL and have active shell session you can run `$ ./cakewallet.sh` script in `scripts/windows` which will run `cakewallet.bat` in WSL. +After execution of `cakewallet.bat` you should to get `Cake Wallet.zip` in project root directory which will contains `CakeWallet.exe` file and another needed files for run the application. Now you can extract files from `Cake Wallet.zip` archive and run the application. diff --git a/cakewallet.bat b/cakewallet.bat new file mode 100644 index 000000000..1904c5710 --- /dev/null +++ b/cakewallet.bat @@ -0,0 +1,51 @@ +@echo off +set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron +set cw_root=%cd% +set cw_archive_name=Cake Wallet.zip +set cw_archive_path=%cw_root%\%cw_archive_name% +set secrets_file_path=lib\.secrets.g.dart +set release_dir=build\windows\x64\runner\Release +@REM Path could be different +if [%~1]==[] (set tools_root=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist\MSVC\14.38.33135\x64\Microsoft.VC143.CRT) else (set tools_root=%1) +@REM Generate android manifest file +cd scripts +bash.exe gen_android_manifest.sh +cd /d %cw_root% +echo === Generating pubspec.yaml === +copy /Y pubspec_description.yaml pubspec.yaml > nul +call flutter pub get > nul +call dart run tool\generate_pubspec.dart +call flutter pub get > nul +call dart run tool\configure.dart %cw_win_app_config% + +IF NOT EXIST "%secrets_file_path%" ( + echo === Generating new secrets file === + call dart run tool\generate_new_secrets.dart +) ELSE (echo === Using previously/already generated secrets file: %secrets_file_path% ===) + +echo === Generating mobx models === +for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron .) do ( + cd %%i + call flutter pub get > nul + call dart run build_runner build --delete-conflicting-outputs > nul + cd /d %cw_root% +) + +echo === Generating localization files === +call dart run tool\generate_localization.dart + +echo === Building the application executable file === +call flutter build windows --dart-define-from-file=env.json --release + +echo === Prepare distribution actions. Copy needed files to the application bundle === +copy /Y "%tools_root%\msvcp140.dll" "%release_dir%\" > nul +copy /Y "%tools_root%\vcruntime140.dll" "%release_dir%\" > nul +copy /Y "%tools_root%\vcruntime140_1.dll" "%release_dir%\" > nul + +echo === Generate the application archive === +xcopy /s /e /v /Y "%release_dir%\*.*" "build\Cake Wallet\" > nul +tar acf "%cw_archive_name%" -C build\ "Cake Wallet" + +echo === Open Explorer with the application archive === +echo Cake Wallet created archive at: %cw_archive_path% +%SystemRoot%\explorer.exe /select, %cw_archive_path% diff --git a/configure_cake_wallet.sh b/configure_cake_wallet.sh index 837a002e9..0539221a3 100755 --- a/configure_cake_wallet.sh +++ b/configure_cake_wallet.sh @@ -1,11 +1,14 @@ +#!/bin/bash + IOS="ios" ANDROID="android" +MACOS="macos" -PLATFORMS=($IOS $ANDROID) +PLATFORMS=($IOS $ANDROID $MACOS) PLATFORM=$1 if ! [[ " ${PLATFORMS[*]} " =~ " ${PLATFORM} " ]]; then - echo "specify platform: ./configure_cake_wallet.sh ios|android" + echo "specify platform: ./configure_cake_wallet.sh ios|android|macos" exit 1 fi @@ -14,6 +17,11 @@ if [ "$PLATFORM" == "$IOS" ]; then cd scripts/ios fi +if [ "$PLATFORM" == "$MACOS" ]; then + echo "Configuring for macOS" + cd scripts/macos +fi + if [ "$PLATFORM" == "$ANDROID" ]; then echo "Configuring for Android" cd scripts/android @@ -22,5 +30,6 @@ fi source ./app_env.sh cakewallet ./app_config.sh cd ../.. && flutter pub get -flutter packages pub run tool/generate_localization.dart +#flutter packages pub run tool/generate_localization.dart ./model_generator.sh +#cd macos && pod install \ No newline at end of file diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 249b87bd3..1c5456b07 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -4,10 +4,8 @@ import 'package:cw_core/crypto_currency.dart'; class AmountConverter { static const _moneroAmountLength = 12; static const _moneroAmountDivider = 1000000000000; - static const _litecoinAmountDivider = 100000000; - static const _ethereumAmountDivider = 1000000000000000000; - static const _dashAmountDivider = 100000000; - static const _bitcoinCashAmountDivider = 100000000; + static const _wowneroAmountLength = 11; + static const _wowneroAmountDivider = 100000000000; static const _bitcoinAmountDivider = 100000000; static const _bitcoinAmountLength = 8; static final _bitcoinAmountFormat = NumberFormat() @@ -16,69 +14,16 @@ class AmountConverter { static final _moneroAmountFormat = NumberFormat() ..maximumFractionDigits = _moneroAmountLength ..minimumFractionDigits = 1; - - static double amountIntToDouble(CryptoCurrency cryptoCurrency, int amount) { - switch (cryptoCurrency) { - case CryptoCurrency.xmr: - return _moneroAmountToDouble(amount); - case CryptoCurrency.btc: - return _bitcoinAmountToDouble(amount); - case CryptoCurrency.bch: - return _bitcoinCashAmountToDouble(amount); - case CryptoCurrency.dash: - return _dashAmountToDouble(amount); - case CryptoCurrency.eth: - return _ethereumAmountToDouble(amount); - case CryptoCurrency.ltc: - return _litecoinAmountToDouble(amount); - case CryptoCurrency.xhv: - case CryptoCurrency.xag: - case CryptoCurrency.xau: - case CryptoCurrency.xaud: - case CryptoCurrency.xbtc: - case CryptoCurrency.xcad: - case CryptoCurrency.xchf: - case CryptoCurrency.xcny: - case CryptoCurrency.xeur: - case CryptoCurrency.xgbp: - case CryptoCurrency.xjpy: - case CryptoCurrency.xnok: - case CryptoCurrency.xnzd: - case CryptoCurrency.xusd: - return _moneroAmountToDouble(amount); - default: - return 0.0; - } - } - - static int amountStringToInt(CryptoCurrency cryptoCurrency, String amount) { - switch (cryptoCurrency) { - case CryptoCurrency.xmr: - return _moneroParseAmount(amount); - case CryptoCurrency.xhv: - case CryptoCurrency.xag: - case CryptoCurrency.xau: - case CryptoCurrency.xaud: - case CryptoCurrency.xbtc: - case CryptoCurrency.xcad: - case CryptoCurrency.xchf: - case CryptoCurrency.xcny: - case CryptoCurrency.xeur: - case CryptoCurrency.xgbp: - case CryptoCurrency.xjpy: - case CryptoCurrency.xnok: - case CryptoCurrency.xnzd: - case CryptoCurrency.xusd: - return _moneroParseAmount(amount); - default: - return 0; - } - } + static final _wowneroAmountFormat = NumberFormat() + ..maximumFractionDigits = _wowneroAmountLength + ..minimumFractionDigits = 1; static String amountIntToString(CryptoCurrency cryptoCurrency, int amount) { switch (cryptoCurrency) { case CryptoCurrency.xmr: return _moneroAmountToString(amount); + case CryptoCurrency.wow: + return _wowneroAmountToString(amount); case CryptoCurrency.btc: case CryptoCurrency.bch: case CryptoCurrency.ltc: @@ -106,34 +51,12 @@ class AmountConverter { static double cryptoAmountToDouble({required num amount, required num divider}) => amount / divider; - static String _moneroAmountToString(int amount) => _moneroAmountFormat.format( - cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider)); + static String _moneroAmountToString(int amount) => _moneroAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider)); - static double _moneroAmountToDouble(int amount) => - cryptoAmountToDouble(amount: amount, divider: _moneroAmountDivider); + static String _bitcoinAmountToString(int amount) => _bitcoinAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider)); - static int _moneroParseAmount(String amount) => - _moneroAmountFormat.parse(amount).toInt(); - - static String _bitcoinAmountToString(int amount) => - _bitcoinAmountFormat.format( - cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider)); - - static double _bitcoinAmountToDouble(int amount) => - cryptoAmountToDouble(amount: amount, divider: _bitcoinAmountDivider); - - static int _doubleToBitcoinAmount(double amount) => - (amount * _bitcoinAmountDivider).toInt(); - - static double _bitcoinCashAmountToDouble(int amount) => - cryptoAmountToDouble(amount: amount, divider: _bitcoinCashAmountDivider); - - static double _dashAmountToDouble(int amount) => - cryptoAmountToDouble(amount: amount, divider: _dashAmountDivider); - - static double _ethereumAmountToDouble(num amount) => - cryptoAmountToDouble(amount: amount, divider: _ethereumAmountDivider); - - static double _litecoinAmountToDouble(int amount) => - cryptoAmountToDouble(amount: amount, divider: _litecoinAmountDivider); + static String _wowneroAmountToString(int amount) => _wowneroAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: _wowneroAmountDivider)); } diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 2bd4eaf91..6881393ed 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -105,6 +105,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.usdtSol, CryptoCurrency.usdcTrc20, CryptoCurrency.tbtc, + CryptoCurrency.wow, ]; static const havenCurrencies = [ @@ -221,6 +222,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static const tbtc = CryptoCurrency(title: 'tBTC', fullName: 'Testnet Bitcoin', raw: 93, name: 'tbtc', iconPath: 'assets/images/tbtc.png', decimals: 8); + static const wow = CryptoCurrency(title: 'WOW', fullName: 'Wownero', raw: 94, name: 'wow', iconPath: 'assets/images/wownero_icon.png', decimals: 11); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 8cf438769..630078949 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -28,7 +28,9 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.sol; case WalletType.tron: return CryptoCurrency.trx; - default: + case WalletType.wownero: + return CryptoCurrency.wow; + case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index d62a78468..6f1b4078b 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -297,3 +297,81 @@ DateTime getDateByBitcoinHeight(int height) { return estimatedDate; } + +// TODO: enhance all of this global const lists +const wowDates = { + "2023-12": 583048, + "2023-11": 575048, + "2023-10": 566048, + "2023-09": 558048, + "2023-08": 549048, + "2023-07": 540048, + "2023-06": 532048, + "2023-05": 523048, + "2023-04": 514048, + "2023-03": 505048, + "2023-02": 497048, + "2023-01": 488048, + "2022-12": 479048, + "2022-11": 471048, + "2022-10": 462048, + "2022-09": 453048, + "2022-08": 444048, + "2022-07": 435048, + "2022-06": 427048, + "2022-05": 418048, + "2022-04": 410048, + "2022-03": 401048, + "2022-02": 393048, + "2022-01": 384048, + "2021-12": 375048, + "2021-11": 367048, + "2021-10": 358048, + "2021-09": 349048, + "2021-08": 340048, + "2021-07": 331048, + "2021-06": 322048, + "2021-05": 313048, + "2021-04": 305048, + "2021-03": 295048, + "2021-02": 287048, + "2021-01": 279148, + "2020-10": 252000, + "2020-09": 243000, + "2020-08": 234000, + "2020-07": 225000, + "2020-06": 217500, + "2020-05": 208500, + "2020-04": 199500, + "2020-03": 190500, + "2020-02": 183000, + "2020-01": 174000, + "2019-12": 165000, + "2019-11": 156000, + "2019-10": 147000, + "2019-09": 138000, + "2019-08": 129000, + "2019-07": 120000, + "2019-06": 112500, + "2019-05": 103500, + "2019-04": 94500, + "2019-03": 85500, + "2019-02": 79500, + "2019-01": 73500, + "2018-12": 67500, + "2018-11": 61500, + "2018-10": 52500, + "2018-09": 45000, + "2018-08": 36000, + "2018-07": 27000, + "2018-06": 18000, + "2018-05": 9000, + "2018-04": 1 +}; + +int getWowneroHeightByDate({required DateTime date}) { + String closestKey = + wowDates.keys.firstWhere((key) => formatMapKey(key).isBefore(date), orElse: () => ''); + + return wowDates[closestKey] ?? 0; +} \ No newline at end of file diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 00b2c51f1..3bbbf38de 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -79,6 +79,7 @@ class Node extends HiveObject with Keyable { switch (type) { case WalletType.monero: case WalletType.haven: + case WalletType.wownero: return Uri.http(uriRaw, ''); case WalletType.bitcoin: case WalletType.litecoin: @@ -96,7 +97,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: return Uri.https(uriRaw, path ?? ''); - default: + case WalletType.none: throw Exception('Unexpected type ${type.toString()} for Node uri'); } } @@ -143,6 +144,7 @@ class Node extends HiveObject with Keyable { switch (type) { case WalletType.monero: case WalletType.haven: + case WalletType.wownero: return requestMoneroNode(); case WalletType.nano: case WalletType.banano: @@ -155,7 +157,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: return requestElectrumServer(); - default: + case WalletType.none: return false; } } catch (_) { diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart index cfc33ef21..9aa721923 100644 --- a/cw_core/lib/pathForWallet.dart +++ b/cw_core/lib/pathForWallet.dart @@ -1,9 +1,10 @@ import 'dart:io'; +import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:path_provider/path_provider.dart'; Future pathForWalletDir({required String name, required WalletType type}) async { - final root = await getApplicationDocumentsDirectory(); + final root = await getAppDir(); final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); final walletDire = Directory('${walletsDir.path}/$prefix/$name'); @@ -20,8 +21,8 @@ Future pathForWallet({required String name, required WalletType type}) a .then((path) => path + '/$name'); Future outdatedAndroidPathForWalletDir({required String name}) async { - final directory = await getApplicationDocumentsDirectory(); + final directory = await getAppDir(); final pathDir = directory.path + '/$name'; return pathDir; -} \ No newline at end of file +} diff --git a/cw_core/lib/root_dir.dart b/cw_core/lib/root_dir.dart new file mode 100644 index 000000000..c2a8170bc --- /dev/null +++ b/cw_core/lib/root_dir.dart @@ -0,0 +1,35 @@ +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; + +String? _rootDirPath; + +void setRootDirFromEnv() => _rootDirPath = Platform.environment['CAKE_WALLET_DIR']; + +Future getAppDir({String appName = 'cake_wallet'}) async { + Directory dir; + + if (_rootDirPath != null && _rootDirPath!.isNotEmpty) { + dir = Directory.fromUri(Uri.file(_rootDirPath!)); + dir.create(recursive: true); + } else { + if (Platform.isWindows) { + dir = await getApplicationSupportDirectory(); + } else if (Platform.isLinux) { + String appDirPath; + + try { + dir = await getApplicationDocumentsDirectory(); + appDirPath = '${dir.path}/$appName'; + } catch (e) { + appDirPath = '/home/${Platform.environment['USER']}/.$appName'; + } + + dir = Directory.fromUri(Uri.file(appDirPath)); + await dir.create(recursive: true); + } else { + dir = await getApplicationDocumentsDirectory(); + } + } + + return dir; +} diff --git a/cw_core/lib/sec_random_native.dart b/cw_core/lib/sec_random_native.dart index ce251efc0..2011602bf 100644 --- a/cw_core/lib/sec_random_native.dart +++ b/cw_core/lib/sec_random_native.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -6,6 +8,12 @@ const utils = const MethodChannel('com.cake_wallet/native_utils'); Future secRandom(int count) async { try { + if (Platform.isWindows || Platform.isLinux) { + // Used method to get securely generated random bytes from cake backups + const byteSize = 256; + final rng = Random.secure(); + return Uint8List.fromList(List.generate(count, (_) => rng.nextInt(byteSize))); + } return await utils.invokeMethod('sec_random', {'count': count}) ?? Uint8List.fromList([]); } on PlatformException catch (_) { return Uint8List.fromList([]); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index b3e41a989..e3957b4e7 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -54,7 +54,10 @@ enum WalletType { solana, @HiveField(11) - tron + tron, + + @HiveField(12) + wownero, } int serializeToInt(WalletType type) { @@ -81,7 +84,9 @@ int serializeToInt(WalletType type) { return 9; case WalletType.tron: return 10; - default: + case WalletType.wownero: + return 11; + case WalletType.none: return -1; } } @@ -110,6 +115,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.solana; case 10: return WalletType.tron; + case 11: + return WalletType.wownero; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -139,7 +146,9 @@ String walletTypeToString(WalletType type) { return 'Solana'; case WalletType.tron: return 'Tron'; - default: + case WalletType.wownero: + return 'Wownero'; + case WalletType.none: return ''; } } @@ -168,7 +177,9 @@ String walletTypeToDisplayName(WalletType type) { return 'Solana (SOL)'; case WalletType.tron: return 'Tron (TRX)'; - default: + case WalletType.wownero: + return 'Wownero (WOW)'; + case WalletType.none: return ''; } } @@ -200,7 +211,9 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.sol; case WalletType.tron: return CryptoCurrency.trx; - default: + case WalletType.wownero: + return CryptoCurrency.wow; + case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_core/lib/wownero_amount_format.dart b/cw_core/lib/wownero_amount_format.dart new file mode 100644 index 000000000..96d2797e8 --- /dev/null +++ b/cw_core/lib/wownero_amount_format.dart @@ -0,0 +1,18 @@ +import 'package:intl/intl.dart'; +import 'package:cw_core/crypto_amount_format.dart'; + +const wowneroAmountLength = 11; +const wowneroAmountDivider = 100000000000; +final wowneroAmountFormat = NumberFormat() + ..maximumFractionDigits = wowneroAmountLength + ..minimumFractionDigits = 1; + +String wowneroAmountToString({required int amount}) => wowneroAmountFormat + .format(cryptoAmountToDouble(amount: amount, divider: wowneroAmountDivider)) + .replaceAll(',', ''); + +double wowneroAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: wowneroAmountDivider); + +int wowneroParseAmount({required String amount}) => + (double.parse(amount) * wowneroAmountDivider).round(); \ No newline at end of file diff --git a/cw_core/lib/wownero_balance.dart b/cw_core/lib/wownero_balance.dart new file mode 100644 index 000000000..2820659f2 --- /dev/null +++ b/cw_core/lib/wownero_balance.dart @@ -0,0 +1,38 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_core/wownero_amount_format.dart'; + +class WowneroBalance extends Balance { + WowneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) + : formattedFullBalance = wowneroAmountToString(amount: fullBalance), + formattedUnlockedBalance = wowneroAmountToString(amount: unlockedBalance - frozenBalance), + formattedLockedBalance = + wowneroAmountToString(amount: frozenBalance + fullBalance - unlockedBalance), + super(unlockedBalance, fullBalance); + + WowneroBalance.fromString( + {required this.formattedFullBalance, + required this.formattedUnlockedBalance, + this.formattedLockedBalance = '0.0'}) + : fullBalance = wowneroParseAmount(amount: formattedFullBalance), + unlockedBalance = wowneroParseAmount(amount: formattedUnlockedBalance), + frozenBalance = wowneroParseAmount(amount: formattedLockedBalance), + super(wowneroParseAmount(amount: formattedUnlockedBalance), + wowneroParseAmount(amount: formattedFullBalance)); + + final int fullBalance; + final int unlockedBalance; + final int frozenBalance; + final String formattedFullBalance; + final String formattedUnlockedBalance; + final String formattedLockedBalance; + + @override + String get formattedUnAvailableBalance => + formattedLockedBalance == '0.0' ? '' : formattedLockedBalance; + + @override + String get formattedAvailableBalance => formattedUnlockedBalance; + + @override + String get formattedAdditionalBalance => formattedFullBalance; +} \ No newline at end of file diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 88fddae09..518c71b94 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -660,10 +660,10 @@ packages: dependency: "direct main" description: name: unorm_dart - sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b" + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.3.0" vector_math: dependency: transitive description: diff --git a/cw_haven/lib/api/account_list.dart b/cw_haven/lib/api/account_list.dart index a05446c8e..87f036206 100644 --- a/cw_haven/lib/api/account_list.dart +++ b/cw_haven/lib/api/account_list.dart @@ -4,7 +4,6 @@ import 'package:cw_haven/api/signatures.dart'; import 'package:cw_haven/api/types.dart'; import 'package:cw_haven/api/haven_api.dart'; import 'package:cw_haven/api/structs/account_row.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_haven/api/wallet.dart'; final accountSizeNative = havenApi @@ -72,12 +71,11 @@ void _setLabelForAccount(Map args) { } Future addAccount({required String label}) async { - await compute(_addAccount, label); + _addAccount(label); await store(); } Future setLabelForAccount({required int accountIndex, required String label}) async { - await compute( - _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); + _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); await store(); } \ No newline at end of file diff --git a/cw_monero/android/.classpath b/cw_monero/android/.classpath deleted file mode 100644 index 4a04201ca..000000000 --- a/cw_monero/android/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/cw_monero/android/.gitignore b/cw_monero/android/.gitignore deleted file mode 100644 index c6cbe562a..000000000 --- a/cw_monero/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures diff --git a/cw_monero/android/.project b/cw_monero/android/.project deleted file mode 100644 index e0799208f..000000000 --- a/cw_monero/android/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - cw_monero - Project android created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.buildship.core.gradleprojectnature - - diff --git a/cw_monero/android/.settings/org.eclipse.buildship.core.prefs b/cw_monero/android/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index a88c4d484..000000000 --- a/cw_monero/android/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,13 +0,0 @@ -arguments= -auto.sync=false -build.scans.enabled=false -connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.0-20191016123526+0000)) -connection.project.dir=../../android -eclipse.preferences.version=1 -gradle.user.home= -java.home= -jvm.arguments= -offline.mode=false -override.workspace.settings=true -show.console.view=true -show.executions.view=true diff --git a/cw_monero/android/CMakeLists.txt b/cw_monero/android/CMakeLists.txt deleted file mode 100644 index f9f98927c..000000000 --- a/cw_monero/android/CMakeLists.txt +++ /dev/null @@ -1,232 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) - -add_library( cw_monero - SHARED - ./jni/monero_jni.cpp - ../ios/Classes/monero_api.cpp) - - find_library( log-lib log ) - -set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../../cw_shared_external/ios/External/android) - -############ -# libsodium -############ - -add_library(sodium STATIC IMPORTED) -set_target_properties(sodium PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libsodium.a) - -############ -# OpenSSL -############ - -add_library(crypto STATIC IMPORTED) -set_target_properties(crypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libcrypto.a) - -add_library(ssl STATIC IMPORTED) -set_target_properties(ssl PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libssl.a) - -############ -# Boost -############ - -add_library(boost_chrono STATIC IMPORTED) -set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_chrono.a) - -add_library(boost_date_time STATIC IMPORTED) -set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_date_time.a) - -add_library(boost_filesystem STATIC IMPORTED) -set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_filesystem.a) - -add_library(boost_program_options STATIC IMPORTED) -set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_program_options.a) - -add_library(boost_regex STATIC IMPORTED) -set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_regex.a) - -add_library(boost_serialization STATIC IMPORTED) -set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_serialization.a) - -add_library(boost_system STATIC IMPORTED) -set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_system.a) - -add_library(boost_thread STATIC IMPORTED) -set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_thread.a) - -add_library(boost_wserialization STATIC IMPORTED) -set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_wserialization.a) - -############# -# Monero -############# - -add_library(wallet_api STATIC IMPORTED) -set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libwallet_api.a) - -add_library(wallet STATIC IMPORTED) -set_target_properties(wallet PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libwallet.a) - -add_library(cryptonote_core STATIC IMPORTED) -set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcryptonote_core.a) - -add_library(cryptonote_basic STATIC IMPORTED) -set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcryptonote_basic.a) - -add_library(cryptonote_format_utils_basic STATIC IMPORTED) -set_target_properties(cryptonote_format_utils_basic PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcryptonote_format_utils_basic.a) - -add_library(mnemonics STATIC IMPORTED) -set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libmnemonics.a) - -add_library(common STATIC IMPORTED) -set_target_properties(common PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcommon.a) - -add_library(cncrypto STATIC IMPORTED) -set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcncrypto.a) - -add_library(ringct STATIC IMPORTED) -set_target_properties(ringct PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libringct.a) - -add_library(ringct_basic STATIC IMPORTED) -set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libringct_basic.a) - -add_library(blockchain_db STATIC IMPORTED) -set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libblockchain_db.a) - -add_library(lmdb STATIC IMPORTED) -set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/liblmdb.a) - -add_library(easylogging STATIC IMPORTED) -set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libeasylogging.a) - -add_library(unbound STATIC IMPORTED) -set_target_properties(unbound PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libunbound.a) - -add_library(epee STATIC IMPORTED) -set_target_properties(epee PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libepee.a) - -add_library(blocks STATIC IMPORTED) -set_target_properties(blocks PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libblocks.a) - -add_library(checkpoints STATIC IMPORTED) -set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libcheckpoints.a) - -add_library(device STATIC IMPORTED) -set_target_properties(device PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libdevice.a) - -add_library(device_trezor STATIC IMPORTED) -set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libdevice_trezor.a) - -add_library(multisig STATIC IMPORTED) -set_target_properties(multisig PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libmultisig.a) - -add_library(version STATIC IMPORTED) -set_target_properties(version PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libversion.a) - -add_library(net STATIC IMPORTED) -set_target_properties(net PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libnet.a) - -add_library(hardforks STATIC IMPORTED) -set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libhardforks.a) - -add_library(randomx STATIC IMPORTED) -set_target_properties(randomx PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/librandomx.a) - -add_library(rpc_base STATIC IMPORTED) -set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/librpc_base.a) - -add_library(wallet-crypto STATIC IMPORTED) -set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION - ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/monero/libwallet-crypto.a) - -set(WALLET_CRYPTO "") - -if(${ANDROID_ABI} STREQUAL "x86_64") - set(WALLET_CRYPTO "wallet-crypto") -endif() - -include_directories( ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/include ) - -target_link_libraries( cw_monero - - wallet_api - wallet - cryptonote_core - cryptonote_basic - cryptonote_format_utils_basic - mnemonics - ringct - ringct_basic - net - common - cncrypto - blockchain_db - lmdb - easylogging - unbound - epee - blocks - checkpoints - device - device_trezor - multisig - version - randomx - hardforks - rpc_base - ${WALLET_CRYPTO} - - boost_chrono - boost_date_time - boost_filesystem - boost_program_options - boost_regex - boost_serialization - boost_system - boost_thread - boost_wserialization - - ssl - crypto - - sodium - - ${log-lib} ) \ No newline at end of file diff --git a/cw_monero/android/build.gradle b/cw_monero/android/build.gradle deleted file mode 100644 index fc4835e81..000000000 --- a/cw_monero/android/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -group 'com.cakewallet.monero' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 28 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } - } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/cw_monero/android/gradle.properties b/cw_monero/android/gradle.properties deleted file mode 100644 index 38c8d4544..000000000 --- a/cw_monero/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/cw_monero/android/gradle/wrapper/gradle-wrapper.properties b/cw_monero/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d..000000000 --- a/cw_monero/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/cw_monero/android/jni/monero_jni.cpp b/cw_monero/android/jni/monero_jni.cpp deleted file mode 100644 index 83e06a41f..000000000 --- a/cw_monero/android/jni/monero_jni.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include -#include -#include "../../ios/Classes/monero_api.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -JNIEXPORT void JNICALL -Java_com_cakewallet_monero_MoneroApi_setNodeAddressJNI( - JNIEnv *env, - jobject inst, - jstring uri, - jstring login, - jstring password, - jboolean use_ssl, - jboolean is_light_wallet) { - const char *_uri = env->GetStringUTFChars(uri, 0); - const char *_login = ""; - const char *_password = ""; - char *error; - - if (login != NULL) { - _login = env->GetStringUTFChars(login, 0); - } - - if (password != NULL) { - _password = env->GetStringUTFChars(password, 0); - } - char *__uri = (char*) _uri; - char *__login = (char*) _login; - char *__password = (char*) _password; - bool inited = setup_node(__uri, __login, __password, false, false, error); - - if (!inited) { - env->ThrowNew(env->FindClass("java/lang/Exception"), error); - } -} - -JNIEXPORT void JNICALL -Java_com_cakewallet_monero_MoneroApi_connectToNodeJNI( - JNIEnv *env, - jobject inst) { - char *error; - bool is_connected = connect_to_node(error); - - if (!is_connected) { - env->ThrowNew(env->FindClass("java/lang/Exception"), error); - } -} - -JNIEXPORT void JNICALL -Java_com_cakewallet_monero_MoneroApi_startSyncJNI( - JNIEnv *env, - jobject inst) { - start_refresh(); -} - -JNIEXPORT void JNICALL -Java_com_cakewallet_monero_MoneroApi_loadWalletJNI( - JNIEnv *env, - jobject inst, - jstring path, - jstring password) { - char *_path = (char *) env->GetStringUTFChars(path, 0); - char *_password = (char *) env->GetStringUTFChars(password, 0); - - load_wallet(_path, _password, 0); -} - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/cw_monero/android/settings.gradle b/cw_monero/android/settings.gradle deleted file mode 100644 index 1f9e2a39d..000000000 --- a/cw_monero/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'cw_monero' diff --git a/cw_monero/android/src/main/AndroidManifest.xml b/cw_monero/android/src/main/AndroidManifest.xml deleted file mode 100644 index 8152415a2..000000000 --- a/cw_monero/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt b/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt deleted file mode 100644 index 37684a16a..000000000 --- a/cw_monero/android/src/main/kotlin/com/cakewallet/monero/CwMoneroPlugin.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.cakewallet.monero - -import android.app.Activity -import android.os.AsyncTask -import android.os.Looper -import android.os.Handler -import android.os.Process - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar - -class doAsync(val handler: () -> Unit) : AsyncTask() { - override fun doInBackground(vararg params: Void?): Void? { - Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); - handler() - return null - } -} - -class CwMoneroPlugin: MethodCallHandler { - companion object { -// val moneroApi = MoneroApi() - val main = Handler(Looper.getMainLooper()); - - init { - System.loadLibrary("cw_monero") - } - - @JvmStatic - fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), "cw_monero") - channel.setMethodCallHandler(CwMoneroPlugin()) - } - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "setupNode") { - val uri = call.argument("address") ?: "" - val login = call.argument("login") ?: "" - val password = call.argument("password") ?: "" - val useSSL = false - val isLightWallet = false -// doAsync { -// try { -// moneroApi.setNodeAddressJNI(uri, login, password, useSSL, isLightWallet) -// main.post({ -// result.success(true) -// }); -// } catch(e: Throwable) { -// main.post({ -// result.error("CONNECTION_ERROR", e.message, null) -// }); -// } -// }.execute() - } - if (call.method == "startSync") { -// doAsync { -// moneroApi.startSyncJNI() -// main.post({ -// result.success(true) -// }); -// }.execute() - } - if (call.method == "loadWallet") { - val path = call.argument("path") ?: "" - val password = call.argument("password") ?: "" -// moneroApi.loadWalletJNI(path, password) - result.success(true) - } - } -} diff --git a/cw_monero/example/.gitignore b/cw_monero/example/.gitignore deleted file mode 100644 index 24476c5d1..000000000 --- a/cw_monero/example/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/cw_monero/example/README.md b/cw_monero/example/README.md deleted file mode 100644 index 18cf6d109..000000000 --- a/cw_monero/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# cw_monero_example - -Demonstrates how to use the cw_monero plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/cw_monero/example/analysis_options.yaml b/cw_monero/example/analysis_options.yaml deleted file mode 100644 index 61b6c4de1..000000000 --- a/cw_monero/example/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/cw_monero/example/lib/main.dart b/cw_monero/example/lib/main.dart deleted file mode 100644 index e4374f097..000000000 --- a/cw_monero/example/lib/main.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:cw_monero/cw_monero.dart'; - -void main() { - runApp(const MyApp()); -} - -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - String _platformVersion = 'Unknown'; - final _cwMoneroPlugin = CwMonero(); - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - // Platform messages may fail, so we use a try/catch PlatformException. - // We also handle the message potentially returning null. - try { - platformVersion = - await _cwMoneroPlugin.getPlatformVersion() ?? 'Unknown platform version'; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Running on: $_platformVersion\n'), - ), - ), - ); - } -} diff --git a/cw_monero/example/macos/.gitignore b/cw_monero/example/macos/.gitignore deleted file mode 100644 index 746adbb6b..000000000 --- a/cw_monero/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig b/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2d..000000000 --- a/cw_monero/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig b/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d157..000000000 --- a/cw_monero/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift b/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index e25d64097..000000000 --- a/cw_monero/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import cw_monero -import path_provider_foundation - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) -} diff --git a/cw_monero/example/macos/Podfile b/cw_monero/example/macos/Podfile deleted file mode 100644 index dade8dfad..000000000 --- a/cw_monero/example/macos/Podfile +++ /dev/null @@ -1,40 +0,0 @@ -platform :osx, '10.11' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/cw_monero/example/macos/Podfile.lock b/cw_monero/example/macos/Podfile.lock deleted file mode 100644 index 692176b30..000000000 --- a/cw_monero/example/macos/Podfile.lock +++ /dev/null @@ -1,22 +0,0 @@ -PODS: - - FlutterMacOS (1.0.0) - - path_provider_macos (0.0.1): - - FlutterMacOS - -DEPENDENCIES: - - FlutterMacOS (from `Flutter/ephemeral`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - -EXTERNAL SOURCES: - FlutterMacOS: - :path: Flutter/ephemeral - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos - -SPEC CHECKSUMS: - FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 - -PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c - -COCOAPODS: 1.11.2 diff --git a/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj b/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 472859e8c..000000000 --- a/cw_monero/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,632 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 51; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 428E7496E2068D0AB138F295 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* cw_monero_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cw_monero_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A9CDA1605413332AB9056C23 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - E434913D71DC2682EF8E9059 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - EEF09839C86335F78056F812 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 428E7496E2068D0AB138F295 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 77870A4C94A9AB6EEC2EE261 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* cw_monero_example.app */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - 77870A4C94A9AB6EEC2EE261 /* Pods */ = { - isa = PBXGroup; - children = ( - EEF09839C86335F78056F812 /* Pods-Runner.debug.xcconfig */, - A9CDA1605413332AB9056C23 /* Pods-Runner.release.xcconfig */, - E434913D71DC2682EF8E9059 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C29B2253BA962B7A415DBA77 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 0A239C1738C005E3F6E4DFC6 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 0CEAA82AE8A029C31B39F234 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* cw_monero_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 0A239C1738C005E3F6E4DFC6 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 0CEAA82AE8A029C31B39F234 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/cw_monero/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 4e44b7ced..000000000 --- a/cw_monero/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c..000000000 --- a/cw_monero/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/cw_monero/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/cw_monero/example/macos/Runner/AppDelegate.swift b/cw_monero/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef6437..000000000 --- a/cw_monero/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19..000000000 --- a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a33e198f5747104729e1fcef999772a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5680 zcmaiYXH?Tqu=Xz`p-L#B_gI#0we$cm_HcmYFP$?wjD#BaCN4mzC5#`>w9y6=ThxrYZc0WPXprg zYjB`UsV}0=eUtY$(P6YW}npdd;%9pi?zS3k-nqCob zSX_AQEf|=wYT3r?f!*Yt)ar^;l3Sro{z(7deUBPd2~(SzZ-s@0r&~Km2S?8r##9-< z)2UOSVaHqq6}%sA9Ww;V2LG=PnNAh6mA2iWOuV7T_lRDR z&N8-eN=U)-T|;wo^Wv=34wtV0g}sAAe}`Ph@~!|<;z7*K8(qkX0}o=!(+N*UWrkEja*$_H6mhK1u{P!AC39} z|3+Z(mAOq#XRYS)TLoHv<)d%$$I@+x+2)V{@o~~J-!YUI-Q9%!Ldi4Op&Lw&B>jj* zwAgC#Y>gbIqv!d|J5f!$dbCXoq(l3GR(S>(rtZ~Z*agXMMKN!@mWT_vmCbSd3dUUm z4M&+gz?@^#RRGal%G3dDvj7C5QTb@9+!MG+>0dcjtZEB45c+qx*c?)d<%htn1o!#1 zpIGonh>P1LHu3s)fGFF-qS}AXjW|M*2Xjkh7(~r(lN=o#mBD9?jt74=Rz85I4Nfx_ z7Z)q?!};>IUjMNM6ee2Thq7))a>My?iWFxQ&}WvsFP5LP+iGz+QiYek+K1`bZiTV- zHHYng?ct@Uw5!gquJ(tEv1wTrRR7cemI>aSzLI^$PxW`wL_zt@RSfZ1M3c2sbebM* ze0=;sy^!90gL~YKISz*x;*^~hcCoO&CRD)zjT(A2b_uRue=QXFe5|!cf0z1m!iwv5GUnLw9Dr*Ux z)3Lc!J@Ei;&&yxGpf2kn@2wJ2?t6~obUg;?tBiD#uo$SkFIasu+^~h33W~`r82rSa ztyE;ehFjC2hjpJ-e__EH&z?!~>UBb=&%DS>NT)1O3Isn-!SElBV2!~m6v0$vx^a<@ISutdTk1@?;i z<8w#b-%|a#?e5(n@7>M|v<<0Kpg?BiHYMRe!3Z{wYc2hN{2`6(;q`9BtXIhVq6t~KMH~J0~XtUuT06hL8c1BYZWhN zk4F2I;|za*R{ToHH2L?MfRAm5(i1Ijw;f+0&J}pZ=A0;A4M`|10ZskA!a4VibFKn^ zdVH4OlsFV{R}vFlD~aA4xxSCTTMW@Gws4bFWI@xume%smAnuJ0b91QIF?ZV!%VSRJ zO7FmG!swKO{xuH{DYZ^##gGrXsUwYfD0dxXX3>QmD&`mSi;k)YvEQX?UyfIjQeIm! z0ME3gmQ`qRZ;{qYOWt}$-mW*>D~SPZKOgP)T-Sg%d;cw^#$>3A9I(%#vsTRQe%moT zU`geRJ16l>FV^HKX1GG7fR9AT((jaVb~E|0(c-WYQscVl(z?W!rJp`etF$dBXP|EG z=WXbcZ8mI)WBN>3<@%4eD597FD5nlZajwh8(c$lum>yP)F}=(D5g1-WVZRc)(!E3} z-6jy(x$OZOwE=~{EQS(Tp`yV2&t;KBpG*XWX!yG+>tc4aoxbXi7u@O*8WWFOxUjcq z^uV_|*818$+@_{|d~VOP{NcNi+FpJ9)aA2So<7sB%j`$Prje&auIiTBb{oD7q~3g0 z>QNIwcz(V-y{Ona?L&=JaV5`o71nIsWUMA~HOdCs10H+Irew#Kr(2cn>orG2J!jvP zqcVX0OiF}c<)+5&p}a>_Uuv)L_j}nqnJ5a?RPBNi8k$R~zpZ33AA4=xJ@Z($s3pG9 zkURJY5ZI=cZGRt_;`hs$kE@B0FrRx(6K{`i1^*TY;Vn?|IAv9|NrN*KnJqO|8$e1& zb?OgMV&q5|w7PNlHLHF) zB+AK#?EtCgCvwvZ6*u|TDhJcCO+%I^@Td8CR}+nz;OZ*4Dn?mSi97m*CXXc=};!P`B?}X`F-B5v-%ACa8fo0W++j&ztmqK z;&A)cT4ob9&MxpQU41agyMU8jFq~RzXOAsy>}hBQdFVL%aTn~M>5t9go2j$i9=(rZ zADmVj;Qntcr3NIPPTggpUxL_z#5~C!Gk2Rk^3jSiDqsbpOXf^f&|h^jT4|l2ehPat zb$<*B+x^qO8Po2+DAmrQ$Zqc`1%?gp*mDk>ERf6I|42^tjR6>}4`F_Mo^N(~Spjcg z_uY$}zui*PuDJjrpP0Pd+x^5ds3TG#f?57dFL{auS_W8|G*o}gcnsKYjS6*t8VI<) zcjqTzW(Hk*t-Qhq`Xe+x%}sxXRerScbPGv8hlJ;CnU-!Nl=# zR=iTFf9`EItr9iAlAGi}i&~nJ-&+)Y| zMZigh{LXe)uR+4D_Yb+1?I93mHQ5{pId2Fq%DBr7`?ipi;CT!Q&|EO3gH~7g?8>~l zT@%*5BbetH)~%TrAF1!-!=)`FIS{^EVA4WlXYtEy^|@y@yr!C~gX+cp2;|O4x1_Ol z4fPOE^nj(}KPQasY#U{m)}TZt1C5O}vz`A|1J!-D)bR%^+=J-yJsQXDzFiqb+PT0! zIaDWWU(AfOKlSBMS};3xBN*1F2j1-_=%o($ETm8@oR_NvtMDVIv_k zlnNBiHU&h8425{MCa=`vb2YP5KM7**!{1O>5Khzu+5OVGY;V=Vl+24fOE;tMfujoF z0M``}MNnTg3f%Uy6hZi$#g%PUA_-W>uVCYpE*1j>U8cYP6m(>KAVCmbsDf39Lqv0^ zt}V6FWjOU@AbruB7MH2XqtnwiXS2scgjVMH&aF~AIduh#^aT1>*V>-st8%=Kk*{bL zzbQcK(l2~)*A8gvfX=RPsNnjfkRZ@3DZ*ff5rmx{@iYJV+a@&++}ZW+za2fU>&(4y`6wgMpQGG5Ah(9oGcJ^P(H< zvYn5JE$2B`Z7F6ihy>_49!6}(-)oZ(zryIXt=*a$bpIw^k?>RJ2 zQYr>-D#T`2ZWDU$pM89Cl+C<;J!EzHwn(NNnWpYFqDDZ_*FZ{9KQRcSrl5T>dj+eA zi|okW;6)6LR5zebZJtZ%6Gx8^=2d9>_670!8Qm$wd+?zc4RAfV!ZZ$jV0qrv(D`db zm_T*KGCh3CJGb(*X6nXzh!h9@BZ-NO8py|wG8Qv^N*g?kouH4%QkPU~Vizh-D3<@% zGomx%q42B7B}?MVdv1DFb!axQ73AUxqr!yTyFlp%Z1IAgG49usqaEbI_RnbweR;Xs zpJq7GKL_iqi8Md?f>cR?^0CA+Uk(#mTlGdZbuC*$PrdB$+EGiW**=$A3X&^lM^K2s zzwc3LtEs5|ho z2>U(-GL`}eNgL-nv3h7E<*<>C%O^=mmmX0`jQb6$mP7jUKaY4je&dCG{x$`0=_s$+ zSpgn!8f~ya&U@c%{HyrmiW2&Wzc#Sw@+14sCpTWReYpF9EQ|7vF*g|sqG3hx67g}9 zwUj5QP2Q-(KxovRtL|-62_QsHLD4Mu&qS|iDp%!rs(~ah8FcrGb?Uv^Qub5ZT_kn%I^U2rxo1DDpmN@8uejxik`DK2~IDi1d?%~pR7i#KTS zA78XRx<(RYO0_uKnw~vBKi9zX8VnjZEi?vD?YAw}y+)wIjIVg&5(=%rjx3xQ_vGCy z*&$A+bT#9%ZjI;0w(k$|*x{I1c!ECMus|TEA#QE%#&LxfGvijl7Ih!B2 z6((F_gwkV;+oSKrtr&pX&fKo3s3`TG@ye+k3Ov)<#J|p8?vKh@<$YE@YIU1~@7{f+ zydTna#zv?)6&s=1gqH<-piG>E6XW8ZI7&b@-+Yk0Oan_CW!~Q2R{QvMm8_W1IV8<+ zQTyy=(Wf*qcQubRK)$B;QF}Y>V6d_NM#=-ydM?%EPo$Q+jkf}*UrzR?Nsf?~pzIj$ z<$wN;7c!WDZ(G_7N@YgZ``l;_eAd3+;omNjlpfn;0(B7L)^;;1SsI6Le+c^ULe;O@ zl+Z@OOAr4$a;=I~R0w4jO`*PKBp?3K+uJ+Tu8^%i<_~bU!p%so z^sjol^slR`W@jiqn!M~eClIIl+`A5%lGT{z^mRbpv}~AyO%R*jmG_Wrng{B9TwIuS z0!@fsM~!57K1l0%{yy(#no}roy#r!?0wm~HT!vLDfEBs9x#`9yCKgufm0MjVRfZ=f z4*ZRc2Lgr(P+j2zQE_JzYmP0*;trl7{*N341Cq}%^M^VC3gKG-hY zmPT>ECyrhIoFhnMB^qpdbiuI}pk{qPbK^}0?Rf7^{98+95zNq6!RuV_zAe&nDk0;f zez~oXlE5%ve^TmBEt*x_X#fs(-En$jXr-R4sb$b~`nS=iOy|OVrph(U&cVS!IhmZ~ zKIRA9X%Wp1J=vTvHZ~SDe_JXOe9*fa zgEPf;gD^|qE=dl>Qkx3(80#SE7oxXQ(n4qQ#by{uppSKoDbaq`U+fRqk0BwI>IXV3 zD#K%ASkzd7u>@|pA=)Z>rQr@dLH}*r7r0ng zxa^eME+l*s7{5TNu!+bD{Pp@2)v%g6^>yj{XP&mShhg9GszNu4ITW=XCIUp2Xro&1 zg_D=J3r)6hp$8+94?D$Yn2@Kp-3LDsci)<-H!wCeQt$e9Jk)K86hvV^*Nj-Ea*o;G zsuhRw$H{$o>8qByz1V!(yV{p_0X?Kmy%g#1oSmlHsw;FQ%j9S#}ha zm0Nx09@jmOtP8Q+onN^BAgd8QI^(y!n;-APUpo5WVdmp8!`yKTlF>cqn>ag`4;o>i zl!M0G-(S*fm6VjYy}J}0nX7nJ$h`|b&KuW4d&W5IhbR;-)*9Y0(Jj|@j`$xoPQ=Cl diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 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 diff --git a/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/cw_monero/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfddf3d9dade342351e627a0a75609fb46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr - - - - - - - - - - - - - - - - - - - - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig b/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index a80a25602..000000000 --- a/cw_monero/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = cw_monero_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.cwMoneroExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2022 com.cakewallet. All rights reserved. diff --git a/cw_monero/example/macos/Runner/Configs/Debug.xcconfig b/cw_monero/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd946..000000000 --- a/cw_monero/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/cw_monero/example/macos/Runner/Configs/Release.xcconfig b/cw_monero/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f4956..000000000 --- a/cw_monero/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig b/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf478..000000000 --- a/cw_monero/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/cw_monero/example/macos/Runner/Info.plist b/cw_monero/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a..000000000 --- a/cw_monero/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/cw_monero/example/macos/Runner/MainFlutterWindow.swift b/cw_monero/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837ec..000000000 --- a/cw_monero/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/cw_monero/example/macos/Runner/Release.entitlements b/cw_monero/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a47..000000000 --- a/cw_monero/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/cw_monero/example/pubspec.yaml b/cw_monero/example/pubspec.yaml deleted file mode 100644 index 2dee5337f..000000000 --- a/cw_monero/example/pubspec.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: cw_monero_example -description: Demonstrates how to use the cw_monero plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -environment: - sdk: '>=2.18.1 <3.0.0' - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - cw_monero: - # When depending on this package from a real application you should use: - # cw_monero: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. - path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^2.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/example/test/widget_test.dart b/cw_monero/example/test/widget_test.dart deleted file mode 100644 index b37e6313d..000000000 --- a/cw_monero/example/test/widget_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:cw_monero_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/cw_monero/ios/.gitignore b/cw_monero/ios/.gitignore deleted file mode 100644 index aa479fd3c..000000000 --- a/cw_monero/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_monero/ios/Classes/CwMoneroPlugin.h b/cw_monero/ios/Classes/CwMoneroPlugin.h deleted file mode 100644 index a42018098..000000000 --- a/cw_monero/ios/Classes/CwMoneroPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface CwMoneroPlugin : NSObject -@end diff --git a/cw_monero/ios/Classes/CwMoneroPlugin.m b/cw_monero/ios/Classes/CwMoneroPlugin.m deleted file mode 100644 index eee251212..000000000 --- a/cw_monero/ios/Classes/CwMoneroPlugin.m +++ /dev/null @@ -1,8 +0,0 @@ -#import "CwMoneroPlugin.h" -#import - -@implementation CwMoneroPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftCwMoneroPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/cw_monero/ios/Classes/CwWalletListener.h b/cw_monero/ios/Classes/CwWalletListener.h deleted file mode 100644 index cbfcb0c4e..000000000 --- a/cw_monero/ios/Classes/CwWalletListener.h +++ /dev/null @@ -1,23 +0,0 @@ -#include - -struct CWMoneroWalletListener; - -typedef int8_t (*on_new_block_callback)(uint64_t height); -typedef int8_t (*on_need_to_refresh_callback)(); - -typedef struct CWMoneroWalletListener -{ - // on_money_spent_callback *on_money_spent; - // on_money_received_callback *on_money_received; - // on_unconfirmed_money_received_callback *on_unconfirmed_money_received; - // on_new_block_callback *on_new_block; - // on_updated_callback *on_updated; - // on_refreshed_callback *on_refreshed; - - on_new_block_callback on_new_block; -} CWMoneroWalletListener; - -struct TestListener { - // int8_t x; - on_new_block_callback on_new_block; -}; \ No newline at end of file diff --git a/cw_monero/ios/Classes/SwiftCwMoneroPlugin.swift b/cw_monero/ios/Classes/SwiftCwMoneroPlugin.swift deleted file mode 100644 index 4c03a3e44..000000000 --- a/cw_monero/ios/Classes/SwiftCwMoneroPlugin.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Flutter -import UIKit - -public class SwiftCwMoneroPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "cw_monero", binaryMessenger: registrar.messenger()) - let instance = SwiftCwMoneroPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - result("iOS " + UIDevice.current.systemVersion) - } -} diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp deleted file mode 100644 index a2a17bd5e..000000000 --- a/cw_monero/ios/Classes/monero_api.cpp +++ /dev/null @@ -1,1044 +0,0 @@ -#include -#include "cstdlib" -#include -#include -#include -#include -#include -#include -#include -#include "thread" -#include "CwWalletListener.h" -#if __APPLE__ -// Fix for randomx on ios -void __clear_cache(void* start, void* end) { } -#include "../External/ios/include/wallet2_api.h" -#else -#include "../External/android/include/wallet2_api.h" -#endif - -using namespace std::chrono_literals; -#ifdef __cplusplus -extern "C" -{ -#endif - const uint64_t MONERO_BLOCK_SIZE = 1000; - - struct Utf8Box - { - char *value; - - Utf8Box(char *_value) - { - value = _value; - } - }; - - struct SubaddressRow - { - uint64_t id; - char *address; - char *label; - - SubaddressRow(std::size_t _id, char *_address, char *_label) - { - id = static_cast(_id); - address = _address; - label = _label; - } - }; - - struct AccountRow - { - uint64_t id; - char *label; - - AccountRow(std::size_t _id, char *_label) - { - id = static_cast(_id); - label = _label; - } - }; - - struct MoneroWalletListener : Monero::WalletListener - { - uint64_t m_height; - bool m_need_to_refresh; - bool m_new_transaction; - - MoneroWalletListener() - { - m_height = 0; - m_need_to_refresh = false; - m_new_transaction = false; - } - - void moneySpent(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void moneyReceived(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void newBlock(uint64_t height) - { - m_height = height; - } - - void updated() - { - m_new_transaction = true; - } - - void refreshed() - { - m_need_to_refresh = true; - } - - void resetNeedToRefresh() - { - m_need_to_refresh = false; - } - - bool isNeedToRefresh() - { - return m_need_to_refresh; - } - - bool isNewTransactionExist() - { - return m_new_transaction; - } - - void resetIsNewTransactionExist() - { - m_new_transaction = false; - } - - uint64_t height() - { - return m_height; - } - }; - - struct TransactionInfoRow - { - uint64_t amount; - uint64_t fee; - uint64_t blockHeight; - uint64_t confirmations; - uint32_t subaddrAccount; - int8_t direction; - int8_t isPending; - uint32_t subaddrIndex; - - char *hash; - char *paymentId; - - int64_t datetime; - - TransactionInfoRow(Monero::TransactionInfo *transaction) - { - amount = transaction->amount(); - fee = transaction->fee(); - blockHeight = transaction->blockHeight(); - subaddrAccount = transaction->subaddrAccount(); - std::set::iterator it = transaction->subaddrIndex().begin(); - subaddrIndex = *it; - confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); - direction = transaction->direction(); - isPending = static_cast(transaction->isPending()); - std::string *hash_str = new std::string(transaction->hash()); - hash = strdup(hash_str->c_str()); - paymentId = strdup(transaction->paymentId().c_str()); - } - }; - - struct PendingTransactionRaw - { - uint64_t amount; - uint64_t fee; - char *hash; - char *hex; - char *txKey; - Monero::PendingTransaction *transaction; - - PendingTransactionRaw(Monero::PendingTransaction *_transaction) - { - transaction = _transaction; - amount = _transaction->amount(); - fee = _transaction->fee(); - hash = strdup(_transaction->txid()[0].c_str()); - hex = strdup(_transaction->hex()[0].c_str()); - txKey = strdup(_transaction->txKey()[0].c_str()); - } - }; - - struct CoinsInfoRow - { - uint64_t blockHeight; - char *hash; - uint64_t internalOutputIndex; - uint64_t globalOutputIndex; - bool spent; - bool frozen; - uint64_t spentHeight; - uint64_t amount; - bool rct; - bool keyImageKnown; - uint64_t pkIndex; - uint32_t subaddrIndex; - uint32_t subaddrAccount; - char *address; - char *addressLabel; - char *keyImage; - uint64_t unlockTime; - bool unlocked; - char *pubKey; - bool coinbase; - char *description; - - CoinsInfoRow(Monero::CoinsInfo *coinsInfo) - { - blockHeight = coinsInfo->blockHeight(); - std::string *hash_str = new std::string(coinsInfo->hash()); - hash = strdup(hash_str->c_str()); - internalOutputIndex = coinsInfo->internalOutputIndex(); - globalOutputIndex = coinsInfo->globalOutputIndex(); - spent = coinsInfo->spent(); - frozen = coinsInfo->frozen(); - spentHeight = coinsInfo->spentHeight(); - amount = coinsInfo->amount(); - rct = coinsInfo->rct(); - keyImageKnown = coinsInfo->keyImageKnown(); - pkIndex = coinsInfo->pkIndex(); - subaddrIndex = coinsInfo->subaddrIndex(); - subaddrAccount = coinsInfo->subaddrAccount(); - address = strdup(coinsInfo->address().c_str()) ; - addressLabel = strdup(coinsInfo->addressLabel().c_str()); - keyImage = strdup(coinsInfo->keyImage().c_str()); - unlockTime = coinsInfo->unlockTime(); - unlocked = coinsInfo->unlocked(); - pubKey = strdup(coinsInfo->pubKey().c_str()); - coinbase = coinsInfo->coinbase(); - description = strdup(coinsInfo->description().c_str()); - } - - void setUnlocked(bool unlocked); - }; - - Monero::Coins *m_coins; - - Monero::Wallet *m_wallet; - Monero::TransactionHistory *m_transaction_history; - MoneroWalletListener *m_listener; - Monero::Subaddress *m_subaddress; - Monero::SubaddressAccount *m_account; - uint64_t m_last_known_wallet_height; - uint64_t m_cached_syncing_blockchain_height = 0; - std::list m_coins_info; - std::mutex store_lock; - bool is_storing = false; - - void change_current_wallet(Monero::Wallet *wallet) - { - m_wallet = wallet; - m_listener = nullptr; - - - if (wallet != nullptr) - { - m_transaction_history = wallet->history(); - } - else - { - m_transaction_history = nullptr; - } - - if (wallet != nullptr) - { - m_account = wallet->subaddressAccount(); - } - else - { - m_account = nullptr; - } - - if (wallet != nullptr) - { - m_subaddress = wallet->subaddress(); - } - else - { - m_subaddress = nullptr; - } - - m_coins_info = std::list(); - - if (wallet != nullptr) - { - m_coins = wallet->coins(); - } - else - { - m_coins = nullptr; - } - } - - Monero::Wallet *get_current_wallet() - { - return m_wallet; - } - - bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (wallet->status() != Monero::Wallet::Status_Ok) - { - error = strdup(wallet->errorString().c_str()); - return false; - } - - change_current_wallet(wallet); - - return true; - } - - bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( - std::string(path), - std::string(password), - std::string(seed), - _networkType, - (uint64_t)restoreHeight); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( - std::string(path), - std::string(password), - std::string(language), - _networkType, - (uint64_t)restoreHeight, - std::string(address), - std::string(viewKey), - std::string(spendKey)); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool restore_wallet_from_spend_key(char *path, char *password, char *seed, char *language, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createDeterministicWalletFromSpendKey( - std::string(path), - std::string(password), - std::string(language), - _networkType, - (uint64_t)restoreHeight, - std::string(spendKey)); - - // Cache Raw to support Polyseed - wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool load_wallet(char *path, char *password, int32_t nettype) - { - nice(19); - Monero::NetworkType networkType = static_cast(nettype); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->openWallet(std::string(path), std::string(password), networkType); - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - change_current_wallet(wallet); - - return !(status != Monero::Wallet::Status_Ok || !errorString.empty()); - } - - char *error_string() { - return strdup(get_current_wallet()->errorString().c_str()); - } - - - bool is_wallet_exist(char *path) - { - return Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(path)); - } - - void close_current_wallet() - { - Monero::WalletManagerFactory::getWalletManager()->closeWallet(get_current_wallet()); - change_current_wallet(nullptr); - } - - char *get_filename() - { - return strdup(get_current_wallet()->filename().c_str()); - } - - char *secret_view_key() - { - return strdup(get_current_wallet()->secretViewKey().c_str()); - } - - char *public_view_key() - { - return strdup(get_current_wallet()->publicViewKey().c_str()); - } - - char *secret_spend_key() - { - return strdup(get_current_wallet()->secretSpendKey().c_str()); - } - - char *public_spend_key() - { - return strdup(get_current_wallet()->publicSpendKey().c_str()); - } - - char *get_address(uint32_t account_index, uint32_t address_index) - { - return strdup(get_current_wallet()->address(account_index, address_index).c_str()); - } - - char *get_cache_attribute(char *name) - { - return strdup(get_current_wallet()->getCacheAttribute(std::string(name)).c_str()); - } - - bool set_cache_attribute(char *name, char *value) - { - get_current_wallet()->setCacheAttribute(std::string(name), std::string(value)); - return true; - } - - const char *seed() - { - std::string _rawSeed = get_current_wallet()->getCacheAttribute("cakewallet.seed"); - if (!_rawSeed.empty()) - { - return strdup(_rawSeed.c_str()); - } - return strdup(get_current_wallet()->seed().c_str()); - } - - uint64_t get_full_balance(uint32_t account_index) - { - return get_current_wallet()->balance(account_index); - } - - uint64_t get_unlocked_balance(uint32_t account_index) - { - return get_current_wallet()->unlockedBalance(account_index); - } - - uint64_t get_current_height() - { - return get_current_wallet()->blockChainHeight(); - } - - uint64_t get_node_height() - { - return get_current_wallet()->daemonBlockChainHeight(); - } - - bool connect_to_node(char *error) - { - nice(19); - bool is_connected = get_current_wallet()->connectToDaemon(); - - if (!is_connected) - { - error = strdup(get_current_wallet()->errorString().c_str()); - } - - return is_connected; - } - - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error) - { - nice(19); - Monero::Wallet *wallet = get_current_wallet(); - - std::string _login = ""; - std::string _password = ""; - std::string _socksProxyAddress = ""; - - if (login != nullptr) - { - _login = std::string(login); - } - - if (password != nullptr) - { - _password = std::string(password); - } - - if (socksProxyAddress != nullptr) - { - _socksProxyAddress = std::string(socksProxyAddress); - } - - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress); - - if (!inited) - { - error = strdup(wallet->errorString().c_str()); - } else if (!wallet->connectToDaemon()) { - error = strdup(wallet->errorString().c_str()); - } - - return inited; - } - - bool is_connected() - { - return get_current_wallet()->connected(); - } - - void start_refresh() - { - get_current_wallet()->refreshAsync(); - get_current_wallet()->startRefresh(); - } - - void set_refresh_from_block_height(uint64_t height) - { - get_current_wallet()->setRefreshFromBlockHeight(height); - } - - void set_recovering_from_seed(bool is_recovery) - { - get_current_wallet()->setRecoveringFromSeed(is_recovery); - } - - void store(char *path) - { - store_lock.lock(); - if (is_storing) { - return; - } - - is_storing = true; - get_current_wallet()->store(std::string(path)); - is_storing = false; - store_lock.unlock(); - } - - bool set_password(char *password, Utf8Box &error) { - bool is_changed = get_current_wallet()->setPassword(std::string(password)); - - if (!is_changed) { - error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); - } - - return is_changed; - } - - bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, - char **preferred_inputs, uint32_t preferred_inputs_size, - Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - std::set _preferred_inputs; - - for (int i = 0; i < preferred_inputs_size; i++) { - _preferred_inputs.insert(std::string(*preferred_inputs)); - preferred_inputs++; - } - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - if (amount != nullptr) - { - uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); - } - else - { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); - } - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, - char **preferred_inputs, uint32_t preferred_inputs_size, - Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - std::vector _addresses; - std::vector _amounts; - - for (int i = 0; i < size; i++) { - _addresses.push_back(std::string(*addresses)); - _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); - addresses++; - amounts++; - } - - std::set _preferred_inputs; - - for (int i = 0; i < preferred_inputs_size; i++) { - _preferred_inputs.insert(std::string(*preferred_inputs)); - preferred_inputs++; - } - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, m_wallet->defaultMixin(), priority, subaddr_account); - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) - { - bool committed = transaction->transaction->commit(); - - if (!committed) - { - error = Utf8Box(strdup(transaction->transaction->errorString().c_str())); - } else if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - return committed; - } - - uint64_t get_node_height_or_update(uint64_t base_eight) - { - if (m_cached_syncing_blockchain_height < base_eight) { - m_cached_syncing_blockchain_height = base_eight; - } - - return m_cached_syncing_blockchain_height; - } - - uint64_t get_syncing_height() - { - if (m_listener == nullptr) { - return 0; - } - - uint64_t height = m_listener->height(); - - if (height <= 1) { - return 0; - } - - if (height != m_last_known_wallet_height) - { - m_last_known_wallet_height = height; - } - - return height; - } - - uint64_t is_needed_to_refresh() - { - if (m_listener == nullptr) { - return false; - } - - bool should_refresh = m_listener->isNeedToRefresh(); - - if (should_refresh) { - m_listener->resetNeedToRefresh(); - } - - return should_refresh; - } - - uint8_t is_new_transaction_exist() - { - if (m_listener == nullptr) { - return false; - } - - bool is_new_transaction_exist = m_listener->isNewTransactionExist(); - - if (is_new_transaction_exist) - { - m_listener->resetIsNewTransactionExist(); - } - - return is_new_transaction_exist; - } - - void set_listener() - { - m_last_known_wallet_height = 0; - - if (m_listener != nullptr) - { - free(m_listener); - } - - m_listener = new MoneroWalletListener(); - get_current_wallet()->setListener(m_listener); - } - - int64_t *subaddrress_get_all() - { - std::vector _subaddresses = m_subaddress->getAll(); - size_t size = _subaddresses.size(); - int64_t *subaddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressRow *row = _subaddresses[i]; - SubaddressRow *_row = new SubaddressRow(row->getRowId(), strdup(row->getAddress().c_str()), strdup(row->getLabel().c_str())); - subaddresses[i] = reinterpret_cast(_row); - } - - return subaddresses; - } - - int32_t subaddrress_size() - { - std::vector _subaddresses = m_subaddress->getAll(); - return _subaddresses.size(); - } - - void subaddress_add_row(uint32_t accountIndex, char *label) - { - m_subaddress->addRow(accountIndex, std::string(label)); - } - - void subaddress_set_label(uint32_t accountIndex, uint32_t addressIndex, char *label) - { - m_subaddress->setLabel(accountIndex, addressIndex, std::string(label)); - } - - void subaddress_refresh(uint32_t accountIndex) - { - m_subaddress->refresh(accountIndex); - } - - int32_t account_size() - { - std::vector _accocunts = m_account->getAll(); - return _accocunts.size(); - } - - int64_t *account_get_all() - { - std::vector _accocunts = m_account->getAll(); - size_t size = _accocunts.size(); - int64_t *accocunts = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressAccountRow *row = _accocunts[i]; - AccountRow *_row = new AccountRow(row->getRowId(), strdup(row->getLabel().c_str())); - accocunts[i] = reinterpret_cast(_row); - } - - return accocunts; - } - - void account_add_row(char *label) - { - m_account->addRow(std::string(label)); - } - - void account_set_label_row(uint32_t account_index, char *label) - { - m_account->setLabel(account_index, label); - } - - void account_refresh() - { - m_account->refresh(); - } - - int64_t *transactions_get_all() - { - std::vector transactions = m_transaction_history->getAll(); - size_t size = transactions.size(); - int64_t *transactionAddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::TransactionInfo *row = transactions[i]; - TransactionInfoRow *tx = new TransactionInfoRow(row); - transactionAddresses[i] = reinterpret_cast(tx); - } - - return transactionAddresses; - } - - void transactions_refresh() - { - m_transaction_history->refresh(); - } - - int64_t transactions_count() - { - return m_transaction_history->count(); - } - - TransactionInfoRow* get_transaction(char * txId) - { - Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); - return new TransactionInfoRow(row); - } - - int LedgerExchange( - unsigned char *command, - unsigned int cmd_len, - unsigned char *response, - unsigned int max_resp_len) - { - return -1; - } - - int LedgerFind(char *buffer, size_t len) - { - return -1; - } - - void on_startup() - { - Monero::Utils::onStartup(); - Monero::WalletManagerFactory::setLogLevel(0); - } - - void rescan_blockchain() - { - m_wallet->rescanBlockchainAsync(); - } - - char * get_tx_key(char * txId) - { - return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); - } - - char *get_subaddress_label(uint32_t accountIndex, uint32_t addressIndex) - { - return strdup(get_current_wallet()->getSubaddressLabel(accountIndex, addressIndex).c_str()); - } - - void set_trusted_daemon(bool arg) - { - m_wallet->setTrustedDaemon(arg); - } - - bool trusted_daemon() - { - return m_wallet->trustedDaemon(); - } - - // Coin Control // - - CoinsInfoRow* coin(int index) - { - if (index >= 0 && index < m_coins_info.size()) { - std::list::iterator it = m_coins_info.begin(); - std::advance(it, index); - Monero::CoinsInfo* element = *it; - std::cout << "Element at index " << index << ": " << element << std::endl; - return new CoinsInfoRow(element); - } else { - std::cout << "Invalid index." << std::endl; - return nullptr; // Return a default value (nullptr) for invalid index - } - } - - void refresh_coins(uint32_t accountIndex) - { - m_coins_info.clear(); - - m_coins->refresh(); - for (const auto i : m_coins->getAll()) { - if (i->subaddrAccount() == accountIndex && !(i->spent())) { - m_coins_info.push_back(i); - } - } - } - - uint64_t coins_count() - { - return m_coins_info.size(); - } - - CoinsInfoRow** coins_from_account(uint32_t accountIndex) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinInfo = coin(i); - if (coinInfo->subaddrAccount == accountIndex) { - matchingCoins.push_back(coinInfo); - } - } - - CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinInfo = coin(i); - if (std::string(coinInfo->hash) == txid) { - matchingCoins.push_back(coinInfo); - } - } - - *count = matchingCoins.size(); - CoinsInfoRow** result = new CoinsInfoRow*[*count]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinsInfoRow = coin(i); - for (size_t j = 0; j < keyimageCount; j++) { - if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { - matchingCoins.push_back(coinsInfoRow); - break; - } - } - } - - *count = matchingCoins.size(); - CoinsInfoRow** result = new CoinsInfoRow*[*count]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - void freeze_coin(int index) - { - m_coins->setFrozen(index); - } - - void thaw_coin(int index) - { - m_coins->thaw(index); - } - - // Sign Messages // - - char *sign_message(char *message, char *address = "") - { - return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str()); - } - -#ifdef __cplusplus -} -#endif diff --git a/cw_monero/ios/Classes/monero_api.h b/cw_monero/ios/Classes/monero_api.h deleted file mode 100644 index fa92a038d..000000000 --- a/cw_monero/ios/Classes/monero_api.h +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include -#include "CwWalletListener.h" - -#ifdef __cplusplus -extern "C" { -#endif - -bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error); -bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error); -bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error); -void load_wallet(char *path, char *password, int32_t nettype); -bool is_wallet_exist(char *path); - -char *get_filename(); -const char *seed(); -char *get_address(uint32_t account_index, uint32_t address_index); -uint64_t get_full_balance(uint32_t account_index); -uint64_t get_unlocked_balance(uint32_t account_index); -uint64_t get_current_height(); -uint64_t get_node_height(); - -bool is_connected(); - -bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error); -bool connect_to_node(char *error); -void start_refresh(); -void set_refresh_from_block_height(uint64_t height); -void set_recovering_from_seed(bool is_recovery); -void store(char *path); - -void set_trusted_daemon(bool arg); -bool trusted_daemon(); -char *sign_message(char *message, char *address); - -#ifdef __cplusplus -} -#endif diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec deleted file mode 100644 index d99bba923..000000000 --- a/cw_monero/ios/cw_monero.podspec +++ /dev/null @@ -1,62 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint cw_monero.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'cw_monero' - s.version = '0.0.2' - s.summary = 'CW Monero' - s.description = 'Cake Wallet wrapper over Monero project.' - s.homepage = 'http://cakewallet.com' - s.license = { :file => '../LICENSE' } - s.author = { 'CakeWallet' => 'support@cakewallet.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/ios/libs/monero/include/External/ios/**/*.h' - s.dependency 'Flutter' - s.dependency 'cw_shared_external' - s.platform = :ios, '10.0' - s.swift_version = '4.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } - s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } - - s.subspec 'OpenSSL' do |openssl| - openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' - openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' - openssl.libraries = 'ssl', 'crypto' - openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Sodium' do |sodium| - sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' - sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libsodium.a' - sodium.libraries = 'sodium' - sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Unbound' do |unbound| - unbound.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' - unbound.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libunbound.a' - unbound.libraries = 'unbound' - unbound.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Boost' do |boost| - boost.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h', - boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libboost.a', - boost.libraries = 'boost' - boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - - s.subspec 'Monero' do |monero| - monero.preserve_paths = 'External/ios/include/**/*.h' - monero.vendored_libraries = 'External/ios/lib/libmonero.a' - monero.libraries = 'monero' - monero.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include" } - end - - # s.subspec 'lmdb' do |lmdb| - # lmdb.vendored_libraries = 'External/ios/lib/liblmdb.a' - # lmdb.libraries = 'lmdb' - # end -end diff --git a/cw_monero/lib/api/account_list.dart b/cw_monero/lib/api/account_list.dart index 451ba5033..199896631 100644 --- a/cw_monero/lib/api/account_list.dart +++ b/cw_monero/lib/api/account_list.dart @@ -1,38 +1,28 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/structs/account_row.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_monero/api/wallet.dart'; +import 'package:monero/monero.dart' as monero; -final accountSizeNative = moneroApi - .lookup>('account_size') - .asFunction(); +monero.wallet? wptr = null; -final accountRefreshNative = moneroApi - .lookup>('account_refresh') - .asFunction(); +int _wlptrForW = 0; +monero.WalletListener? _wlptr = null; -final accountGetAllNative = moneroApi - .lookup>('account_get_all') - .asFunction(); +monero.WalletListener getWlptr() { + if (wptr!.address == _wlptrForW) return _wlptr!; + _wlptrForW = wptr!.address; + _wlptr = monero.MONERO_cw_getWalletListener(wptr!); + return _wlptr!; +} -final accountAddNewNative = moneroApi - .lookup>('account_add_row') - .asFunction(); -final accountSetLabelNative = moneroApi - .lookup>('account_set_label_row') - .asFunction(); +monero.SubaddressAccount? subaddressAccount; bool isUpdating = false; void refreshAccounts() { try { isUpdating = true; - accountRefreshNative(); + subaddressAccount = monero.Wallet_subaddressAccount(wptr!); + monero.SubaddressAccount_refresh(subaddressAccount!); isUpdating = false; } catch (e) { isUpdating = false; @@ -40,26 +30,27 @@ void refreshAccounts() { } } -List getAllAccount() { - final size = accountSizeNative(); - final accountAddressesPointer = accountGetAllNative(); - final accountAddresses = accountAddressesPointer.asTypedList(size); - - return accountAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); +List getAllAccount() { + // final size = monero.Wallet_numSubaddressAccounts(wptr!); + refreshAccounts(); + int size = monero.SubaddressAccount_getAll_size(subaddressAccount!); + print("size: $size"); + if (size == 0) { + monero.Wallet_addSubaddressAccount(wptr!); + return getAllAccount(); + } + return List.generate(size, (index) { + return monero.SubaddressAccount_getAll_byIndex(subaddressAccount!, index: index); + }); } void addAccountSync({required String label}) { - final labelPointer = label.toNativeUtf8(); - accountAddNewNative(labelPointer); - calloc.free(labelPointer); + monero.Wallet_addSubaddressAccount(wptr!, label: label); } void setLabelForAccountSync({required int accountIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - accountSetLabelNative(accountIndex, labelPointer); - calloc.free(labelPointer); + // TODO(mrcyjanek): this may be wrong function? + monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: 0, label: label); } void _addAccount(String label) => addAccountSync(label: label); @@ -72,12 +63,11 @@ void _setLabelForAccount(Map args) { } Future addAccount({required String label}) async { - await compute(_addAccount, label); + _addAccount(label); await store(); } Future setLabelForAccount({required int accountIndex, required String label}) async { - await compute( - _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); + _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); await store(); } \ No newline at end of file diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart index d7350a6e2..c1b634cc6 100644 --- a/cw_monero/lib/api/coins_info.dart +++ b/cw_monero/lib/api/coins_info.dart @@ -1,35 +1,17 @@ -import 'dart:ffi'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/structs/coins_info_row.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; +import 'package:cw_monero/api/account_list.dart'; +import 'package:monero/monero.dart' as monero; -final refreshCoinsNative = moneroApi - .lookup>('refresh_coins') - .asFunction(); +monero.Coins? coins = null; -final coinsCountNative = moneroApi - .lookup>('coins_count') - .asFunction(); +void refreshCoins(int accountIndex) { + coins = monero.Wallet_coins(wptr!); + monero.Coins_refresh(coins!); +} -final coinNative = moneroApi - .lookup>('coin') - .asFunction(); +int countOfCoins() => monero.Coins_count(coins!); -final freezeCoinNative = moneroApi - .lookup>('freeze_coin') - .asFunction(); +monero.CoinsInfo getCoin(int index) => monero.Coins_coin(coins!, index); -final thawCoinNative = moneroApi - .lookup>('thaw_coin') - .asFunction(); +void freezeCoin(int index) => monero.Coins_setFrozen(coins!, index: index); -void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); - -int countOfCoins() => coinsCountNative(); - -CoinsInfoRow getCoin(int index) => coinNative(index).ref; - -void freezeCoin(int index) => freezeCoinNative(index); - -void thawCoin(int index) => thawCoinNative(index); +void thawCoin(int index) => monero.Coins_thaw(coins!, index: index); diff --git a/cw_monero/lib/api/convert_utf8_to_string.dart b/cw_monero/lib/api/convert_utf8_to_string.dart deleted file mode 100644 index 41a6b648a..000000000 --- a/cw_monero/lib/api/convert_utf8_to_string.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -String convertUTF8ToString({required Pointer pointer}) { - final str = pointer.toDartString(); - calloc.free(pointer); - return str; -} \ No newline at end of file diff --git a/cw_monero/lib/api/monero_api.dart b/cw_monero/lib/api/monero_api.dart deleted file mode 100644 index 398d737d1..000000000 --- a/cw_monero/lib/api/monero_api.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:ffi'; -import 'dart:io'; - -final DynamicLibrary moneroApi = Platform.isAndroid - ? DynamicLibrary.open("libcw_monero.so") - : DynamicLibrary.open("cw_monero.framework/cw_monero"); \ No newline at end of file diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart deleted file mode 100644 index 40f338c8c..000000000 --- a/cw_monero/lib/api/signatures.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_monero/api/structs/coins_info_row.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/ut8_box.dart'; -import 'package:ffi/ffi.dart'; - -typedef create_wallet = Int8 Function( - Pointer, Pointer, Pointer, Int32, Pointer); - -typedef restore_wallet_from_seed = Int8 Function( - Pointer, Pointer, Pointer, Int32, Int64, Pointer); - -typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, Pointer, - Pointer, Pointer, Pointer, Int32, Int64, Pointer); - -typedef restore_wallet_from_spend_key = Int8 Function(Pointer, Pointer, Pointer, - Pointer, Pointer, Int32, Int64, Pointer); - -// typedef restore_wallet_from_device = Int8 Function(Pointer, Pointer, Pointer, -// Int32, Int64, Pointer); - -typedef is_wallet_exist = Int8 Function(Pointer); - -typedef load_wallet = Int8 Function(Pointer, Pointer, Int8); - -typedef error_string = Pointer Function(); - -typedef get_filename = Pointer Function(); - -typedef get_seed = Pointer Function(); - -typedef get_address = Pointer Function(Int32, Int32); - -typedef get_full_balanace = Int64 Function(Int32); - -typedef get_unlocked_balanace = Int64 Function(Int32); - -typedef get_current_height = Int64 Function(); - -typedef get_node_height = Int64 Function(); - -typedef is_connected = Int8 Function(); - -typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); - -typedef start_refresh = Void Function(); - -typedef connect_to_node = Int8 Function(); - -typedef set_refresh_from_block_height = Void Function(Int64); - -typedef set_recovering_from_seed = Void Function(Int8); - -typedef store_c = Void Function(Pointer); - -typedef set_password = Int8 Function(Pointer password, Pointer error); - -typedef set_listener = Void Function(); - -typedef get_syncing_height = Int64 Function(); - -typedef is_needed_to_refresh = Int8 Function(); - -typedef is_new_transaction_exist = Int8 Function(); - -typedef subaddrress_size = Int32 Function(); - -typedef subaddrress_refresh = Void Function(Int32); - -typedef subaddress_get_all = Pointer Function(); - -typedef subaddress_add_new = Void Function(Int32 accountIndex, Pointer label); - -typedef subaddress_set_label = Void Function( - Int32 accountIndex, Int32 addressIndex, Pointer label); - -typedef account_size = Int32 Function(); - -typedef account_refresh = Void Function(); - -typedef account_get_all = Pointer Function(); - -typedef account_add_new = Void Function(Pointer label); - -typedef account_set_label = Void Function(Int32 accountIndex, Pointer label); - -typedef transactions_refresh = Void Function(); - -typedef get_transaction = Pointer Function(Pointer txId); - -typedef get_tx_key = Pointer? Function(Pointer txId); - -typedef transactions_count = Int64 Function(); - -typedef transactions_get_all = Pointer Function(); - -typedef transaction_create = Int8 Function( - Pointer address, - Pointer paymentId, - Pointer amount, - Int8 priorityRaw, - Int32 subaddrAccount, - Pointer> preferredInputs, - Int32 preferredInputsSize, - Pointer error, - Pointer pendingTransaction); - -typedef transaction_create_mult_dest = Int8 Function( - Pointer> addresses, - Pointer paymentId, - Pointer> amounts, - Int32 size, - Int8 priorityRaw, - Int32 subaddrAccount, - Pointer> preferredInputs, - Int32 preferredInputsSize, - Pointer error, - Pointer pendingTransaction); - -typedef transaction_commit = Int8 Function(Pointer, Pointer); - -typedef secret_view_key = Pointer Function(); - -typedef public_view_key = Pointer Function(); - -typedef secret_spend_key = Pointer Function(); - -typedef public_spend_key = Pointer Function(); - -typedef close_current_wallet = Void Function(); - -typedef on_startup = Void Function(); - -typedef rescan_blockchain = Void Function(); - -typedef get_subaddress_label = Pointer Function(Int32 accountIndex, Int32 addressIndex); - -typedef set_trusted_daemon = Void Function(Int8 trusted); - -typedef trusted_daemon = Int8 Function(); - -typedef refresh_coins = Void Function(Int32 accountIndex); - -typedef coins_count = Int64 Function(); - -// typedef coins_from_txid = Pointer Function(Pointer txid); - -typedef coin = Pointer Function(Int32 index); - -typedef freeze_coin = Void Function(Int32 index); - -typedef thaw_coin = Void Function(Int32 index); - -typedef sign_message = Pointer Function(Pointer message, Pointer address); - -typedef get_cache_attribute = Pointer Function(Pointer name); - -typedef set_cache_attribute = Int8 Function(Pointer name, Pointer value); diff --git a/cw_monero/lib/api/structs/pending_transaction.dart b/cw_monero/lib/api/structs/pending_transaction.dart index 656ed333f..dc5fbddd0 100644 --- a/cw_monero/lib/api/structs/pending_transaction.dart +++ b/cw_monero/lib/api/structs/pending_transaction.dart @@ -1,25 +1,3 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; - -class PendingTransactionRaw extends Struct { - @Int64() - external int amount; - - @Int64() - external int fee; - - external Pointer hash; - - external Pointer hex; - - external Pointer txKey; - - String getHash() => hash.toDartString(); - - String getHex() => hex.toDartString(); - - String getKey() => txKey.toDartString(); -} class PendingTransactionDescription { PendingTransactionDescription({ diff --git a/cw_monero/lib/api/subaddress_list.dart b/cw_monero/lib/api/subaddress_list.dart index 1c1f1253f..57edea76e 100644 --- a/cw_monero/lib/api/subaddress_list.dart +++ b/cw_monero/lib/api/subaddress_list.dart @@ -1,38 +1,23 @@ -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/structs/subaddress_row.dart'; + +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/wallet.dart'; - -final subaddressSizeNative = moneroApi - .lookup>('subaddrress_size') - .asFunction(); - -final subaddressRefreshNative = moneroApi - .lookup>('subaddress_refresh') - .asFunction(); - -final subaddrressGetAllNative = moneroApi - .lookup>('subaddrress_get_all') - .asFunction(); - -final subaddrressAddNewNative = moneroApi - .lookup>('subaddress_add_row') - .asFunction(); - -final subaddrressSetLabelNative = moneroApi - .lookup>('subaddress_set_label') - .asFunction(); +import 'package:monero/monero.dart' as monero; bool isUpdating = false; +class SubaddressInfoMetadata { + SubaddressInfoMetadata({ + required this.accountIndex, + }); + int accountIndex; +} + +SubaddressInfoMetadata? subaddress = null; + void refreshSubaddresses({required int accountIndex}) { try { isUpdating = true; - subaddressRefreshNative(accountIndex); + subaddress = SubaddressInfoMetadata(accountIndex: accountIndex); isUpdating = false; } catch (e) { isUpdating = false; @@ -40,28 +25,39 @@ void refreshSubaddresses({required int accountIndex}) { } } -List getAllSubaddresses() { - final size = subaddressSizeNative(); - final subaddressAddressesPointer = subaddrressGetAllNative(); - final subaddressAddresses = subaddressAddressesPointer.asTypedList(size); +class Subaddress { + Subaddress({ + required this.addressIndex, + required this.accountIndex, + }); + String get address => monero.Wallet_address( + wptr!, + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + final int addressIndex; + final int accountIndex; + String get label => monero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); +} - return subaddressAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); +List getAllSubaddresses() { + final size = monero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); + return List.generate(size, (index) { + return Subaddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + }).reversed.toList(); } void addSubaddressSync({required int accountIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - subaddrressAddNewNative(accountIndex, labelPointer); - calloc.free(labelPointer); + monero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); + refreshSubaddresses(accountIndex: accountIndex); } void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { - final labelPointer = label.toNativeUtf8(); - - subaddrressSetLabelNative(accountIndex, addressIndex, labelPointer); - calloc.free(labelPointer); + monero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); } void _addSubaddress(Map args) { @@ -81,14 +77,13 @@ void _setLabelForSubaddress(Map args) { } Future addSubaddress({required int accountIndex, required String label}) async { - await compute, void>( - _addSubaddress, {'accountIndex': accountIndex, 'label': label}); - await store(); + _addSubaddress({'accountIndex': accountIndex, 'label': label}); + await store(); } Future setLabelForSubaddress( {required int accountIndex, required int addressIndex, required String label}) async { - await compute, void>(_setLabelForSubaddress, { + _setLabelForSubaddress({ 'accountIndex': accountIndex, 'addressIndex': addressIndex, 'label': label diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 73c8de801..187921ff4 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -1,138 +1,132 @@ + import 'dart:ffi'; +import 'dart:isolate'; -import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart'; -import 'package:cw_monero/api/monero_api.dart'; import 'package:cw_monero/api/monero_output.dart'; -import 'package:cw_monero/api/signatures.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/ut8_box.dart'; -import 'package:cw_monero/api/types.dart'; import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; +import 'package:monero/monero.dart' as monero; +import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen; -final transactionsRefreshNative = moneroApi - .lookup>('transactions_refresh') - .asFunction(); - -final transactionsCountNative = moneroApi - .lookup>('transactions_count') - .asFunction(); - -final transactionsGetAllNative = moneroApi - .lookup>('transactions_get_all') - .asFunction(); - -final transactionCreateNative = moneroApi - .lookup>('transaction_create') - .asFunction(); - -final transactionCreateMultDestNative = moneroApi - .lookup>('transaction_create_mult_dest') - .asFunction(); - -final transactionCommitNative = moneroApi - .lookup>('transaction_commit') - .asFunction(); - -final getTxKeyNative = - moneroApi.lookup>('get_tx_key').asFunction(); - -final getTransactionNative = moneroApi - .lookup>('get_transaction') - .asFunction(); String getTxKey(String txId) { - final txIdPointer = txId.toNativeUtf8(); - final keyPointer = getTxKeyNative(txIdPointer); + return monero.Wallet_getTxKey(wptr!, txid: txId); +} - calloc.free(txIdPointer); +monero.TransactionHistory? txhistory; - if (keyPointer != null) { - return convertUTF8ToString(pointer: keyPointer); +void refreshTransactions() { + txhistory = monero.Wallet_history(wptr!); + monero.TransactionHistory_refresh(txhistory!); +} + +int countOfTransactions() => monero.TransactionHistory_count(txhistory!); + +List getAllTransactions() { + List dummyTxs = []; + + txhistory = monero.Wallet_history(wptr!); + monero.TransactionHistory_refresh(txhistory!); + int size = countOfTransactions(); + final list = List.generate(size, (index) => Transaction(txInfo: monero.TransactionHistory_transaction(txhistory!, index: index)))..addAll(dummyTxs); + + final accts = monero.Wallet_numSubaddressAccounts(wptr!); + for (var i = 0; i < accts; i++) { + final fullBalance = monero.Wallet_balance(wptr!, accountIndex: i); + final availBalance = monero.Wallet_unlockedBalance(wptr!, accountIndex: i); + if (fullBalance > availBalance) { + if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isNotEmpty) { + dummyTxs.add( + Transaction.dummy( + displayLabel: "", + description: "", + fee: 0, + confirmations: 0, + blockheight: 0, + accountIndex: i, + paymentId: "", + amount: fullBalance - availBalance, + isSpend: false, + hash: "pending", + key: "pending", + txInfo: Pointer.fromAddress(0), + )..timeStamp = DateTime.now() + ); + } + } } - - return ''; + list.addAll(dummyTxs); + return list; } -void refreshTransactions() => transactionsRefreshNative(); - -int countOfTransactions() => transactionsCountNative(); - -List getAllTransactions() { - final size = transactionsCountNative(); - final transactionsPointer = transactionsGetAllNative(); - final transactionsAddresses = transactionsPointer.asTypedList(size); - - return transactionsAddresses - .map((addr) => Pointer.fromAddress(addr).ref) - .toList(); +Transaction getTransaction(String txId) { + return Transaction(txInfo: monero.TransactionHistory_transactionById(txhistory!, txid: txId)); } -TransactionInfoRow getTransaction(String txId) { - final txIdPointer = txId.toNativeUtf8(); - return getTransactionNative(txIdPointer).ref; -} - -PendingTransactionDescription createTransactionSync( +Future createTransactionSync( {required String address, required String paymentId, required int priorityRaw, String? amount, int accountIndex = 0, - List preferredInputs = const []}) { - final addressPointer = address.toNativeUtf8(); - final paymentIdPointer = paymentId.toNativeUtf8(); - final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + List preferredInputs = const []}) async { - final int preferredInputsSize = preferredInputs.length; - final List> preferredInputsPointers = - preferredInputs.map((output) => output.toNativeUtf8()).toList(); - final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + final amt = amount == null ? 0 : monero.Wallet_amountFromString(amount); + + final address_ = address.toNativeUtf8(); + final paymentId_ = paymentId.toNativeUtf8(); + final preferredInputs_ = preferredInputs.join(monero.defaultSeparatorStr).toNativeUtf8(); - for (int i = 0; i < preferredInputsSize; i++) { - preferredInputsPointerPointer[i] = preferredInputsPointers[i]; - } + final waddr = wptr!.address; + final addraddr = address_.address; + final paymentIdAddr = paymentId_.address; + final preferredInputsAddr = preferredInputs_.address; + final spaddr = monero.defaultSeparator.address; + final pendingTx = Pointer.fromAddress(await Isolate.run(() { + final tx = monero_gen.MoneroC(DynamicLibrary.open(monero.libPath)).MONERO_Wallet_createTransaction( + Pointer.fromAddress(waddr), + Pointer.fromAddress(addraddr).cast(), + Pointer.fromAddress(paymentIdAddr).cast(), + amt, + 1, + priorityRaw, + accountIndex, + Pointer.fromAddress(preferredInputsAddr).cast(), + Pointer.fromAddress(spaddr), + ); + return tx.address; + })); + calloc.free(address_); + calloc.free(paymentId_); + calloc.free(preferredInputs_); + final String? error = (() { + final status = monero.PendingTransaction_status(pendingTx); + if (status == 0) { + return null; + } + return monero.PendingTransaction_errorString(pendingTx); + })(); - final errorMessagePointer = calloc(); - final pendingTransactionRawPointer = calloc(); - final created = transactionCreateNative( - addressPointer, - paymentIdPointer, - amountPointer, - priorityRaw, - accountIndex, - preferredInputsPointerPointer, - preferredInputsSize, - errorMessagePointer, - pendingTransactionRawPointer) != - 0; - - calloc.free(preferredInputsPointerPointer); - - preferredInputsPointers.forEach((element) => calloc.free(element)); - - calloc.free(addressPointer); - calloc.free(paymentIdPointer); - - if (amountPointer != nullptr) { - calloc.free(amountPointer); - } - - if (!created) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); + if (error != null) { + final message = error; throw CreationTransactionException(message: message); } + final rAmt = monero.PendingTransaction_amount(pendingTx); + final rFee = monero.PendingTransaction_fee(pendingTx); + final rHash = monero.PendingTransaction_txid(pendingTx, ''); + final rTxKey = rHash; + return PendingTransactionDescription( - amount: pendingTransactionRawPointer.ref.amount, - fee: pendingTransactionRawPointer.ref.fee, - hash: pendingTransactionRawPointer.ref.getHash(), - hex: pendingTransactionRawPointer.ref.getHex(), - txKey: pendingTransactionRawPointer.ref.getKey(), - pointerAddress: pendingTransactionRawPointer.address); + amount: rAmt, + fee: rFee, + hash: rHash, + hex: '', + txKey: rTxKey, + pointerAddress: pendingTx.address, + ); } PendingTransactionDescription createTransactionMultDestSync( @@ -141,84 +135,50 @@ PendingTransactionDescription createTransactionMultDestSync( required int priorityRaw, int accountIndex = 0, List preferredInputs = const []}) { - final int size = outputs.length; - final List> addressesPointers = - outputs.map((output) => output.address.toNativeUtf8()).toList(); - final Pointer> addressesPointerPointer = calloc(size); - final List> amountsPointers = - outputs.map((output) => output.amount.toNativeUtf8()).toList(); - final Pointer> amountsPointerPointer = calloc(size); - - for (int i = 0; i < size; i++) { - addressesPointerPointer[i] = addressesPointers[i]; - amountsPointerPointer[i] = amountsPointers[i]; + + final txptr = monero.Wallet_createTransactionMultDest( + wptr!, + dstAddr: outputs.map((e) => e.address).toList(), + isSweepAll: false, + amounts: outputs.map((e) => monero.Wallet_amountFromString(e.amount)).toList(), + mixinCount: 0, + pendingTransactionPriority: priorityRaw, + subaddr_account: accountIndex, + ); + if (monero.PendingTransaction_status(txptr) != 0) { + throw CreationTransactionException(message: monero.PendingTransaction_errorString(txptr)); } - - final int preferredInputsSize = preferredInputs.length; - final List> preferredInputsPointers = - preferredInputs.map((output) => output.toNativeUtf8()).toList(); - final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); - - for (int i = 0; i < preferredInputsSize; i++) { - preferredInputsPointerPointer[i] = preferredInputsPointers[i]; - } - - final paymentIdPointer = paymentId.toNativeUtf8(); - final errorMessagePointer = calloc(); - final pendingTransactionRawPointer = calloc(); - final created = transactionCreateMultDestNative( - addressesPointerPointer, - paymentIdPointer, - amountsPointerPointer, - size, - priorityRaw, - accountIndex, - preferredInputsPointerPointer, - preferredInputsSize, - errorMessagePointer, - pendingTransactionRawPointer) != - 0; - - calloc.free(addressesPointerPointer); - calloc.free(amountsPointerPointer); - calloc.free(preferredInputsPointerPointer); - - addressesPointers.forEach((element) => calloc.free(element)); - amountsPointers.forEach((element) => calloc.free(element)); - preferredInputsPointers.forEach((element) => calloc.free(element)); - - calloc.free(paymentIdPointer); - - if (!created) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw CreationTransactionException(message: message); - } - return PendingTransactionDescription( - amount: pendingTransactionRawPointer.ref.amount, - fee: pendingTransactionRawPointer.ref.fee, - hash: pendingTransactionRawPointer.ref.getHash(), - hex: pendingTransactionRawPointer.ref.getHex(), - txKey: pendingTransactionRawPointer.ref.getKey(), - pointerAddress: pendingTransactionRawPointer.address); + amount: monero.PendingTransaction_amount(txptr), + fee: monero.PendingTransaction_fee(txptr), + hash: monero.PendingTransaction_txid(txptr, ''), + hex: monero.PendingTransaction_txid(txptr, ''), + txKey: monero.PendingTransaction_txid(txptr, ''), + pointerAddress: txptr.address, + ); } void commitTransactionFromPointerAddress({required int address}) => - commitTransaction(transactionPointer: Pointer.fromAddress(address)); + commitTransaction(transactionPointer: monero.PendingTransaction.fromAddress(address)); -void commitTransaction({required Pointer transactionPointer}) { - final errorMessagePointer = calloc(); - final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; +void commitTransaction({required monero.PendingTransaction transactionPointer}) { + + final txCommit = monero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); - if (!isCommited) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw CreationTransactionException(message: message); + final String? error = (() { + final status = monero.PendingTransaction_status(transactionPointer.cast()); + if (status == 0) { + return null; + } + return monero.Wallet_errorString(wptr!); + })(); + + if (error != null) { + throw CreationTransactionException(message: error); } } -PendingTransactionDescription _createTransactionSync(Map args) { +Future _createTransactionSync(Map args) async { final address = args['address'] as String; final paymentId = args['paymentId'] as String; final amount = args['amount'] as String?; @@ -256,8 +216,8 @@ Future createTransaction( String? amount, String paymentId = '', int accountIndex = 0, - List preferredInputs = const []}) => - compute(_createTransactionSync, { + List preferredInputs = const []}) async => + _createTransactionSync({ 'address': address, 'paymentId': paymentId, 'amount': amount, @@ -271,11 +231,94 @@ Future createTransactionMultDest( required int priorityRaw, String paymentId = '', int accountIndex = 0, - List preferredInputs = const []}) => - compute(_createTransactionMultDestSync, { + List preferredInputs = const []}) async => + _createTransactionMultDestSync({ 'outputs': outputs, 'paymentId': paymentId, 'priorityRaw': priorityRaw, 'accountIndex': accountIndex, 'preferredInputs': preferredInputs }); + + +class Transaction { + final String displayLabel; + String subaddressLabel = monero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); + late final String address = monero.Wallet_address( + wptr!, + accountIndex: 0, + addressIndex: 0, + ); + final String description; + final int fee; + final int confirmations; + late final bool isPending = confirmations < 10; + final int blockheight; + final int addressIndex = 0; + final int accountIndex; + final String paymentId; + final int amount; + final bool isSpend; + late DateTime timeStamp; + late final bool isConfirmed = !isPending; + final String hash; + final String key; + + Map toJson() { + return { + "displayLabel": displayLabel, + "subaddressLabel": subaddressLabel, + "address": address, + "description": description, + "fee": fee, + "confirmations": confirmations, + "isPending": isPending, + "blockheight": blockheight, + "accountIndex": accountIndex, + "addressIndex": addressIndex, + "paymentId": paymentId, + "amount": amount, + "isSpend": isSpend, + "timeStamp": timeStamp.toIso8601String(), + "isConfirmed": isConfirmed, + "hash": hash, + }; + } + + // S finalubAddress? subAddress; + // List transfers = []; + // final int txIndex; + final monero.TransactionInfo txInfo; + Transaction({ + required this.txInfo, + }) : displayLabel = monero.TransactionInfo_label(txInfo), + hash = monero.TransactionInfo_hash(txInfo), + timeStamp = DateTime.fromMillisecondsSinceEpoch( + monero.TransactionInfo_timestamp(txInfo) * 1000, + ), + isSpend = monero.TransactionInfo_direction(txInfo) == + monero.TransactionInfo_Direction.Out, + amount = monero.TransactionInfo_amount(txInfo), + paymentId = monero.TransactionInfo_paymentId(txInfo), + accountIndex = monero.TransactionInfo_subaddrAccount(txInfo), + blockheight = monero.TransactionInfo_blockHeight(txInfo), + confirmations = monero.TransactionInfo_confirmations(txInfo), + fee = monero.TransactionInfo_fee(txInfo), + description = monero.TransactionInfo_description(txInfo), + key = monero.Wallet_getTxKey(wptr!, txid: monero.TransactionInfo_hash(txInfo)); + + Transaction.dummy({ + required this.displayLabel, + required this.description, + required this.fee, + required this.confirmations, + required this.blockheight, + required this.accountIndex, + required this.paymentId, + required this.amount, + required this.isSpend, + required this.hash, + required this.key, + required this.txInfo + }); +} \ No newline at end of file diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart deleted file mode 100644 index 6b36ab5e3..000000000 --- a/cw_monero/lib/api/types.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:ffi'; -import 'package:cw_monero/api/structs/coins_info_row.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:cw_monero/api/structs/transaction_info_row.dart'; -import 'package:cw_monero/api/structs/ut8_box.dart'; -import 'package:ffi/ffi.dart'; - -typedef CreateWallet = int Function( - Pointer, Pointer, Pointer, int, Pointer); - -typedef RestoreWalletFromSeed = int Function( - Pointer, Pointer, Pointer, int, int, Pointer); - -typedef RestoreWalletFromKeys = int Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, int, int, Pointer); - -typedef RestoreWalletFromSpendKey = int Function(Pointer, Pointer, Pointer, - Pointer, Pointer, int, int, Pointer); - -typedef RestoreWalletFromDevice = int Function(Pointer, Pointer, Pointer, - int, int, Pointer); - -typedef IsWalletExist = int Function(Pointer); - -typedef LoadWallet = int Function(Pointer, Pointer, int); - -typedef ErrorString = Pointer Function(); - -typedef GetFilename = Pointer Function(); - -typedef GetSeed = Pointer Function(); - -typedef GetAddress = Pointer Function(int, int); - -typedef GetFullBalance = int Function(int); - -typedef GetUnlockedBalance = int Function(int); - -typedef GetCurrentHeight = int Function(); - -typedef GetNodeHeight = int Function(); - -typedef IsConnected = int Function(); - -typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); - -typedef StartRefresh = void Function(); - -typedef ConnectToNode = int Function(); - -typedef SetRefreshFromBlockHeight = void Function(int); - -typedef SetRecoveringFromSeed = void Function(int); - -typedef Store = void Function(Pointer); - -typedef SetPassword = int Function(Pointer password, Pointer error); - -typedef SetListener = void Function(); - -typedef GetSyncingHeight = int Function(); - -typedef IsNeededToRefresh = int Function(); - -typedef IsNewTransactionExist = int Function(); - -typedef SubaddressSize = int Function(); - -typedef SubaddressRefresh = void Function(int); - -typedef SubaddressGetAll = Pointer Function(); - -typedef SubaddressAddNew = void Function(int accountIndex, Pointer label); - -typedef SubaddressSetLabel = void Function( - int accountIndex, int addressIndex, Pointer label); - -typedef AccountSize = int Function(); - -typedef AccountRefresh = void Function(); - -typedef AccountGetAll = Pointer Function(); - -typedef AccountAddNew = void Function(Pointer label); - -typedef AccountSetLabel = void Function(int accountIndex, Pointer label); - -typedef TransactionsRefresh = void Function(); - -typedef GetTransaction = Pointer Function(Pointer txId); - -typedef GetTxKey = Pointer? Function(Pointer txId); - -typedef TransactionsCount = int Function(); - -typedef TransactionsGetAll = Pointer Function(); - -typedef TransactionCreate = int Function( - Pointer address, - Pointer paymentId, - Pointer amount, - int priorityRaw, - int subaddrAccount, - Pointer> preferredInputs, - int preferredInputsSize, - Pointer error, - Pointer pendingTransaction); - -typedef TransactionCreateMultDest = int Function( - Pointer> addresses, - Pointer paymentId, - Pointer> amounts, - int size, - int priorityRaw, - int subaddrAccount, - Pointer> preferredInputs, - int preferredInputsSize, - Pointer error, - Pointer pendingTransaction); - -typedef TransactionCommit = int Function(Pointer, Pointer); - -typedef SecretViewKey = Pointer Function(); - -typedef PublicViewKey = Pointer Function(); - -typedef SecretSpendKey = Pointer Function(); - -typedef PublicSpendKey = Pointer Function(); - -typedef CloseCurrentWallet = void Function(); - -typedef OnStartup = void Function(); - -typedef RescanBlockchainAsync = void Function(); - -typedef GetSubaddressLabel = Pointer Function( - int accountIndex, - int addressIndex); - -typedef SetTrustedDaemon = void Function(int); - -typedef TrustedDaemon = int Function(); - -typedef RefreshCoins = void Function(int); - -typedef CoinsCount = int Function(); - -typedef GetCoin = Pointer Function(int); - -typedef FreezeCoin = void Function(int); - -typedef ThawCoin = void Function(int); - -typedef SignMessage = Pointer Function(Pointer, Pointer); - -typedef GetCacheAttribute = Pointer Function(Pointer); - -typedef SetCacheAttribute = int Function(Pointer, Pointer); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 448c661e6..59eeb1498 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -1,245 +1,155 @@ import 'dart:async'; import 'dart:ffi'; +import 'dart:isolate'; -import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/structs/ut8_box.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; +import 'package:monero/monero.dart' as monero; +import 'package:mutex/mutex.dart'; -int _boolToInt(bool value) => value ? 1 : 0; +int getSyncingHeight() { + // final height = monero.MONERO_cw_WalletListener_height(getWlptr()); + final h2 = monero.Wallet_blockChainHeight(wptr!); + // print("height: $height / $h2"); + return h2; +} -final getFileNameNative = - moneroApi.lookup>('get_filename').asFunction(); +bool isNeededToRefresh() { + final ret = monero.MONERO_cw_WalletListener_isNeedToRefresh(getWlptr()); + monero.MONERO_cw_WalletListener_resetNeedToRefresh(getWlptr()); + return ret; +} -final getSeedNative = moneroApi.lookup>('seed').asFunction(); +bool isNewTransactionExist() { + final ret = monero.MONERO_cw_WalletListener_isNewTransactionExist(getWlptr()); + monero.MONERO_cw_WalletListener_resetIsNewTransactionExist(getWlptr()); + return ret; +} -final getAddressNative = - moneroApi.lookup>('get_address').asFunction(); +String getFilename() => monero.Wallet_filename(wptr!); -final getFullBalanceNative = moneroApi - .lookup>('get_full_balance') - .asFunction(); +String getSeed() { + // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + final cakepolyseed = + monero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + if (cakepolyseed != "") { + return cakepolyseed; + } + final polyseed = monero.Wallet_getPolyseed(wptr!, passphrase: ''); + if (polyseed != "") { + return polyseed; + } + final legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + return legacy; +} -final getUnlockedBalanceNative = moneroApi - .lookup>('get_unlocked_balance') - .asFunction(); - -final getCurrentHeightNative = moneroApi - .lookup>('get_current_height') - .asFunction(); - -final getNodeHeightNative = moneroApi - .lookup>('get_node_height') - .asFunction(); - -final isConnectedNative = - moneroApi.lookup>('is_connected').asFunction(); - -final setupNodeNative = - moneroApi.lookup>('setup_node').asFunction(); - -final startRefreshNative = - moneroApi.lookup>('start_refresh').asFunction(); - -final connecToNodeNative = moneroApi - .lookup>('connect_to_node') - .asFunction(); - -final setRefreshFromBlockHeightNative = moneroApi - .lookup>('set_refresh_from_block_height') - .asFunction(); - -final setRecoveringFromSeedNative = moneroApi - .lookup>('set_recovering_from_seed') - .asFunction(); - -final storeNative = moneroApi.lookup>('store').asFunction(); - -final setPasswordNative = - moneroApi.lookup>('set_password').asFunction(); - -final setListenerNative = - moneroApi.lookup>('set_listener').asFunction(); - -final getSyncingHeightNative = moneroApi - .lookup>('get_syncing_height') - .asFunction(); - -final isNeededToRefreshNative = moneroApi - .lookup>('is_needed_to_refresh') - .asFunction(); - -final isNewTransactionExistNative = moneroApi - .lookup>('is_new_transaction_exist') - .asFunction(); - -final getSecretViewKeyNative = moneroApi - .lookup>('secret_view_key') - .asFunction(); - -final getPublicViewKeyNative = moneroApi - .lookup>('public_view_key') - .asFunction(); - -final getSecretSpendKeyNative = moneroApi - .lookup>('secret_spend_key') - .asFunction(); - -final getPublicSpendKeyNative = moneroApi - .lookup>('public_spend_key') - .asFunction(); - -final closeCurrentWalletNative = moneroApi - .lookup>('close_current_wallet') - .asFunction(); - -final onStartupNative = - moneroApi.lookup>('on_startup').asFunction(); - -final rescanBlockchainAsyncNative = moneroApi - .lookup>('rescan_blockchain') - .asFunction(); - -final getSubaddressLabelNative = moneroApi - .lookup>('get_subaddress_label') - .asFunction(); - -final setTrustedDaemonNative = moneroApi - .lookup>('set_trusted_daemon') - .asFunction(); - -final trustedDaemonNative = - moneroApi.lookup>('trusted_daemon').asFunction(); - -final signMessageNative = - moneroApi.lookup>('sign_message').asFunction(); - -final getCacheAttributeNative = moneroApi - .lookup>('get_cache_attribute') - .asFunction(); - -final setCacheAttributeNative = moneroApi - .lookup>('set_cache_attribute') - .asFunction(); - -int getSyncingHeight() => getSyncingHeightNative(); - -bool isNeededToRefresh() => isNeededToRefreshNative() != 0; - -bool isNewTransactionExist() => isNewTransactionExistNative() != 0; - -String getFilename() => convertUTF8ToString(pointer: getFileNameNative()); - -String getSeed() => convertUTF8ToString(pointer: getSeedNative()); +String getSeedLegacy(String? language) { + var legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + if (monero.Wallet_status(wptr!) != 0) { + monero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); + legacy = monero.Wallet_seed(wptr!, seedOffset: ''); + } + return legacy; +} String getAddress({int accountIndex = 0, int addressIndex = 0}) => - convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex)); + monero.Wallet_address(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); -int getFullBalance({int accountIndex = 0}) => getFullBalanceNative(accountIndex); +int getFullBalance({int accountIndex = 0}) => + monero.Wallet_balance(wptr!, accountIndex: accountIndex); -int getUnlockedBalance({int accountIndex = 0}) => getUnlockedBalanceNative(accountIndex); +int getUnlockedBalance({int accountIndex = 0}) => + monero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex); -int getCurrentHeight() => getCurrentHeightNative(); +int getCurrentHeight() => monero.Wallet_blockChainHeight(wptr!); -int getNodeHeightSync() => getNodeHeightNative(); +int getNodeHeightSync() => monero.Wallet_daemonBlockChainHeight(wptr!); -bool isConnectedSync() => isConnectedNative() != 0; +bool isConnectedSync() => monero.Wallet_connected(wptr!) != 0; -bool setupNodeSync( +Future setupNodeSync( {required String address, String? login, String? password, bool useSSL = false, bool isLightWallet = false, - String? socksProxyAddress}) { - final addressPointer = address.toNativeUtf8(); - Pointer? loginPointer; - Pointer? socksProxyAddressPointer; - Pointer? passwordPointer; + String? socksProxyAddress}) async { + print(''' +{ + wptr!, + daemonAddress: $address, + useSsl: $useSSL, + proxyAddress: $socksProxyAddress ?? '', + daemonUsername: $login ?? '', + daemonPassword: $password ?? '' +} +'''); + final addr = wptr!.address; + await Isolate.run(() { + monero.Wallet_init(Pointer.fromAddress(addr), + daemonAddress: address, + useSsl: useSSL, + proxyAddress: socksProxyAddress ?? '', + daemonUsername: login ?? '', + daemonPassword: password ?? ''); + }); + // monero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'moneroc', console: true); - if (login != null) { - loginPointer = login.toNativeUtf8(); + final status = monero.Wallet_status(wptr!); + + if (status != 0) { + final error = monero.Wallet_errorString(wptr!); + print("error: $error"); + throw SetupWalletException(message: error); } - if (password != null) { - passwordPointer = password.toNativeUtf8(); - } - - if (socksProxyAddress != null) { - socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); - } - - final errorMessagePointer = ''.toNativeUtf8(); - final isSetupNode = setupNodeNative( - addressPointer, - loginPointer, - passwordPointer, - _boolToInt(useSSL), - _boolToInt(isLightWallet), - socksProxyAddressPointer, - errorMessagePointer) != - 0; - - calloc.free(addressPointer); - - if (loginPointer != null) { - calloc.free(loginPointer); - } - - if (passwordPointer != null) { - calloc.free(passwordPointer); - } - - if (!isSetupNode) { - throw SetupWalletException(message: convertUTF8ToString(pointer: errorMessagePointer)); - } - - return isSetupNode; + return status == 0; } -void startRefreshSync() => startRefreshNative(); +void startRefreshSync() { + monero.Wallet_refreshAsync(wptr!); + monero.Wallet_startRefresh(wptr!); +} -Future connectToNode() async => connecToNodeNative() != 0; -void setRefreshFromBlockHeight({required int height}) => setRefreshFromBlockHeightNative(height); +void setRefreshFromBlockHeight({required int height}) => + monero.Wallet_setRefreshFromBlockHeight(wptr!, + refresh_from_block_height: height); void setRecoveringFromSeed({required bool isRecovery}) => - setRecoveringFromSeedNative(_boolToInt(isRecovery)); + monero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery); -void storeSync() { - final pathPointer = ''.toNativeUtf8(); - storeNative(pathPointer); - calloc.free(pathPointer); +final storeMutex = Mutex(); +void storeSync() async { + await storeMutex.acquire(); + final addr = wptr!.address; + await Isolate.run(() { + monero.Wallet_store(Pointer.fromAddress(addr)); + }); + storeMutex.release(); } void setPasswordSync(String password) { - final passwordPointer = password.toNativeUtf8(); - final errorMessagePointer = calloc(); - final changed = setPasswordNative(passwordPointer, errorMessagePointer) != 0; - calloc.free(passwordPointer); + monero.Wallet_setPassword(wptr!, password: password); - if (!changed) { - final message = errorMessagePointer.ref.getValue(); - calloc.free(errorMessagePointer); - throw Exception(message); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + throw Exception(monero.Wallet_errorString(wptr!)); } - - calloc.free(errorMessagePointer); } -void closeCurrentWallet() => closeCurrentWalletNative(); +void closeCurrentWallet() { + monero.Wallet_stop(wptr!); +} -String getSecretViewKey() => convertUTF8ToString(pointer: getSecretViewKeyNative()); +String getSecretViewKey() => monero.Wallet_secretViewKey(wptr!); -String getPublicViewKey() => convertUTF8ToString(pointer: getPublicViewKeyNative()); +String getPublicViewKey() => monero.Wallet_publicViewKey(wptr!); -String getSecretSpendKey() => convertUTF8ToString(pointer: getSecretSpendKeyNative()); +String getSecretSpendKey() => monero.Wallet_secretSpendKey(wptr!); -String getPublicSpendKey() => convertUTF8ToString(pointer: getPublicSpendKeyNative()); +String getPublicSpendKey() => monero.Wallet_publicSpendKey(wptr!); class SyncListener { SyncListener(this.onNewBlock, this.onNewTransaction) @@ -267,7 +177,8 @@ class SyncListener { _cachedBlockchainHeight = 0; _lastKnownBlockHeight = 0; _initialSyncHeight = 0; - _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async { + _updateSyncInfoTimer ??= + Timer.periodic(Duration(milliseconds: 1200), (_) async { if (isNewTransactionExist()) { onNewTransaction(); } @@ -306,18 +217,18 @@ class SyncListener { void stop() => _updateSyncInfoTimer?.cancel(); } -SyncListener setListeners( - void Function(int, int, double) onNewBlock, void Function() onNewTransaction) { +SyncListener setListeners(void Function(int, int, double) onNewBlock, + void Function() onNewTransaction) { final listener = SyncListener(onNewBlock, onNewTransaction); - setListenerNative(); + // setListenerNative(); return listener; } -void onStartup() => onStartupNative(); +void onStartup() {} void _storeSync(Object _) => storeSync(); -bool _setupNodeSync(Map args) { +Future _setupNodeSync(Map args) async { final address = args['address'] as String; final login = (args['login'] ?? '') as String; final password = (args['password'] ?? '') as String; @@ -346,8 +257,8 @@ Future setupNode( String? password, bool useSSL = false, String? socksProxyAddress, - bool isLightWallet = false}) => - compute, void>(_setupNodeSync, { + bool isLightWallet = false}) async => + _setupNodeSync({ 'address': address, 'login': login, 'password': password, @@ -356,49 +267,24 @@ Future setupNode( 'socksProxyAddress': socksProxyAddress }); -Future store() => compute(_storeSync, 0); +Future store() async => _storeSync(0); -Future isConnected() => compute(_isConnected, 0); +Future isConnected() async => _isConnected(0); -Future getNodeHeight() => compute(_getNodeHeight, 0); +Future getNodeHeight() async => _getNodeHeight(0); -void rescanBlockchainAsync() => rescanBlockchainAsyncNative(); +void rescanBlockchainAsync() => monero.Wallet_rescanBlockchainAsync(wptr!); String getSubaddressLabel(int accountIndex, int addressIndex) { - return convertUTF8ToString(pointer: getSubaddressLabelNative(accountIndex, addressIndex)); + return monero.Wallet_getSubaddressLabel(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); } -Future setTrustedDaemon(bool trusted) async => setTrustedDaemonNative(_boolToInt(trusted)); +Future setTrustedDaemon(bool trusted) async => + monero.Wallet_setTrustedDaemon(wptr!, arg: trusted); -Future trustedDaemon() async => trustedDaemonNative() != 0; +Future trustedDaemon() async => monero.Wallet_trustedDaemon(wptr!); String signMessage(String message, {String address = ""}) { - final messagePointer = message.toNativeUtf8(); - final addressPointer = address.toNativeUtf8(); - - final signature = convertUTF8ToString(pointer: signMessageNative(messagePointer, addressPointer)); - calloc.free(messagePointer); - calloc.free(addressPointer); - - return signature; -} - -bool setCacheAttribute(String name, String value) { - final namePointer = name.toNativeUtf8(); - final valuePointer = value.toNativeUtf8(); - - final isSet = setCacheAttributeNative(namePointer, valuePointer); - calloc.free(namePointer); - calloc.free(valuePointer); - - return isSet == 1; -} - -String getCacheAttribute(String name) { - final namePointer = name.toNativeUtf8(); - - final value = convertUTF8ToString(pointer: getCacheAttributeNative(namePointer)); - calloc.free(namePointer); - - return value; + return monero.Wallet_signMessage(wptr!, message: message, address: address); } diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index ae88f76ab..1873f734e 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -1,85 +1,50 @@ import 'dart:ffi'; +import 'dart:isolate'; -import 'package:cw_monero/api/convert_utf8_to_string.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; import 'package:cw_monero/api/wallet.dart'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; +import 'package:monero/monero.dart' as monero; -final createWalletNative = moneroApi - .lookup>('create_wallet') - .asFunction(); - -final restoreWalletFromSeedNative = moneroApi - .lookup>( - 'restore_wallet_from_seed') - .asFunction(); - -final restoreWalletFromKeysNative = moneroApi - .lookup>( - 'restore_wallet_from_keys') - .asFunction(); - -final restoreWalletFromSpendKeyNative = moneroApi - .lookup>( - 'restore_wallet_from_spend_key') - .asFunction(); - -// final restoreWalletFromDeviceNative = moneroApi -// .lookup>( -// 'restore_wallet_from_device') -// .asFunction(); - -final isWalletExistNative = moneroApi - .lookup>('is_wallet_exist') - .asFunction(); - -final loadWalletNative = moneroApi - .lookup>('load_wallet') - .asFunction(); - -final errorStringNative = moneroApi - .lookup>('error_string') - .asFunction(); +monero.WalletManager? _wmPtr; +final monero.WalletManager wmPtr = Pointer.fromAddress((() { + try { + // Problems with the wallet? Crashes? Lags? this will print all calls to xmr + // codebase, so it will be easier to debug what happens. At least easier + // than plugging gdb in. Especially on windows/android. + monero.printStarts = false; + _wmPtr ??= monero.WalletManagerFactory_getWalletManager(); + print("ptr: $_wmPtr"); + } catch (e) { + print(e); + } + return _wmPtr!.address; +})()); void createWalletSync( {required String path, - required String password, - required String language, - int nettype = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final languagePointer = language.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletCreated = createWalletNative(pathPointer, passwordPointer, - languagePointer, nettype, errorMessagePointer) != - 0; + required String password, + required String language, + int nettype = 0}) { + wptr = monero.WalletManager_createWallet(wmPtr, + path: path, password: password, language: language, networkType: 0); - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(languagePointer); - - if (!isWalletCreated) { - throw WalletCreationException( - message: convertUTF8ToString(pointer: errorMessagePointer)); + final status = monero.Wallet_status(wptr!); + if (status != 0) { + throw WalletCreationException(message: monero.Wallet_errorString(wptr!)); } + monero.Wallet_store(wptr!, path: path); + openedWalletsByPath[path] = wptr!; + // is the line below needed? // setupNodeSync(address: "node.moneroworld.com:18089"); } bool isWalletExistSync({required String path}) { - final pathPointer = path.toNativeUtf8(); - final isExist = isWalletExistNative(pathPointer) != 0; - - calloc.free(pathPointer); - - return isExist; + return monero.WalletManager_walletExists(wmPtr, path); } void restoreWalletFromSeedSync( @@ -88,27 +53,24 @@ void restoreWalletFromSeedSync( required String seed, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final seedPointer = seed.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletRestored = restoreWalletFromSeedNative( - pathPointer, - passwordPointer, - seedPointer, - nettype, - restoreHeight, - errorMessagePointer) != - 0; + wptr = monero.WalletManager_recoveryWallet( + wmPtr, + path: path, + password: password, + mnemonic: seed, + restoreHeight: restoreHeight, + seedOffset: '', + networkType: 0, + ); - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(seedPointer); + final status = monero.Wallet_status(wptr!); - if (!isWalletRestored) { - throw WalletRestoreFromSeedException( - message: convertUTF8ToString(pointer: errorMessagePointer)); + if (status != 0) { + final error = monero.Wallet_errorString(wptr!); + throw WalletRestoreFromSeedException(message: error); } + + openedWalletsByPath[path] = wptr!; } void restoreWalletFromKeysSync( @@ -120,76 +82,72 @@ void restoreWalletFromKeysSync( required String spendKey, int nettype = 0, int restoreHeight = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final languagePointer = language.toNativeUtf8(); - final addressPointer = address.toNativeUtf8(); - final viewKeyPointer = viewKey.toNativeUtf8(); - final spendKeyPointer = spendKey.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletRestored = restoreWalletFromKeysNative( - pathPointer, - passwordPointer, - languagePointer, - addressPointer, - viewKeyPointer, - spendKeyPointer, - nettype, - restoreHeight, - errorMessagePointer) != - 0; + wptr = monero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(languagePointer); - calloc.free(addressPointer); - calloc.free(viewKeyPointer); - calloc.free(spendKeyPointer); - - if (!isWalletRestored) { + final status = monero.Wallet_status(wptr!); + if (status != 0) { throw WalletRestoreFromKeysException( - message: convertUTF8ToString(pointer: errorMessagePointer)); + message: monero.Wallet_errorString(wptr!)); } + + openedWalletsByPath[path] = wptr!; } void restoreWalletFromSpendKeySync( {required String path, - required String password, - required String seed, - required String language, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final seedPointer = seed.toNativeUtf8(); - final languagePointer = language.toNativeUtf8(); - final spendKeyPointer = spendKey.toNativeUtf8(); - final errorMessagePointer = ''.toNativeUtf8(); - final isWalletRestored = restoreWalletFromSpendKeyNative( - pathPointer, - passwordPointer, - seedPointer, - languagePointer, - spendKeyPointer, - nettype, - restoreHeight, - errorMessagePointer) != - 0; + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) { + // wptr = monero.WalletManager_createWalletFromKeys( + // wmPtr, + // path: path, + // password: password, + // restoreHeight: restoreHeight, + // addressString: '', + // spendKeyString: spendKey, + // viewKeyString: '', + // nettype: 0, + // ); - calloc.free(pathPointer); - calloc.free(passwordPointer); - calloc.free(languagePointer); - calloc.free(spendKeyPointer); + wptr = monero.WalletManager_createDeterministicWalletFromSpendKey( + wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight, + ); + + final status = monero.Wallet_status(wptr!); + + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + print("err: $err"); + throw WalletRestoreFromKeysException(message: err); + } + + monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); storeSync(); - if (!isWalletRestored) { - throw WalletRestoreFromKeysException( - message: convertUTF8ToString(pointer: errorMessagePointer)); - } + openedWalletsByPath[path] = wptr!; } +String _lastOpenedWallet = ""; + // void restoreMoneroWalletFromDevice( // {required String path, // required String password, @@ -221,20 +179,35 @@ void restoreWalletFromSpendKeySync( // } // } +Map openedWalletsByPath = {}; -void loadWallet({ - required String path, - required String password, - int nettype = 0}) { - final pathPointer = path.toNativeUtf8(); - final passwordPointer = password.toNativeUtf8(); - final loaded = loadWalletNative(pathPointer, passwordPointer, nettype) != 0; - calloc.free(pathPointer); - calloc.free(passwordPointer); - - if (!loaded) { - throw WalletOpeningException( - message: convertUTF8ToString(pointer: errorStringNative())); +void loadWallet( + {required String path, required String password, int nettype = 0}) { + if (openedWalletsByPath[path] != null) { + wptr = openedWalletsByPath[path]!; + return; + } + try { + if (wptr == null || path != _lastOpenedWallet) { + if (wptr != null) { + final addr = wptr!.address; + Isolate.run(() { + monero.Wallet_store(Pointer.fromAddress(addr)); + }); + } + wptr = monero.WalletManager_openWallet(wmPtr, + path: path, password: password); + openedWalletsByPath[path] = wptr!; + _lastOpenedWallet = path; + } + } catch (e) { + print(e); + } + final status = monero.Wallet_status(wptr!); + if (status != 0) { + final err = monero.Wallet_errorString(wptr!); + print(err); + throw WalletOpeningException(message: err); } } @@ -292,23 +265,26 @@ void _restoreFromSpendKey(Map args) { spendKey: spendKey); } -Future _openWallet(Map args) async => - loadWallet(path: args['path'] as String, password: args['password'] as String); +Future _openWallet(Map args) async => loadWallet( + path: args['path'] as String, password: args['password'] as String); bool _isWalletExist(String path) => isWalletExistSync(path: path); -void openWallet({required String path, required String password, int nettype = 0}) async => +void openWallet( + {required String path, + required String password, + int nettype = 0}) async => loadWallet(path: path, password: password, nettype: nettype); Future openWalletAsync(Map args) async => - compute(_openWallet, args); + _openWallet(args); Future createWallet( {required String path, required String password, required String language, int nettype = 0}) async => - compute(_createWallet, { + _createWallet({ 'path': path, 'password': password, 'language': language, @@ -321,7 +297,7 @@ Future restoreFromSeed( required String seed, int nettype = 0, int restoreHeight = 0}) async => - compute, void>(_restoreFromSeed, { + _restoreFromSeed({ 'path': path, 'password': password, 'seed': seed, @@ -338,7 +314,7 @@ Future restoreFromKeys( required String spendKey, int nettype = 0, int restoreHeight = 0}) async => - compute, void>(_restoreFromKeys, { + _restoreFromKeys({ 'path': path, 'password': password, 'language': language, @@ -350,14 +326,14 @@ Future restoreFromKeys( }); Future restoreFromSpendKey( - {required String path, - required String password, - required String seed, - required String language, - required String spendKey, - int nettype = 0, - int restoreHeight = 0}) async => - compute, void>(_restoreFromSpendKey, { + {required String path, + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) async => + _restoreFromSpendKey({ 'path': path, 'password': password, 'seed': seed, @@ -367,4 +343,4 @@ Future restoreFromSpendKey( 'restoreHeight': restoreHeight }); -Future isWalletExist({required String path}) => compute(_isWalletExist, path); +bool isWalletExist({required String path}) => _isWalletExist(path); diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index 2fd11b3ba..29d096efd 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -2,7 +2,7 @@ import 'package:cw_core/monero_amount_format.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/account.dart'; import 'package:cw_monero/api/account_list.dart' as account_list; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:monero/monero.dart' as monero; part 'monero_account_list.g.dart'; @@ -44,13 +44,12 @@ abstract class MoneroAccountListBase with Store { } List getAll() => account_list.getAllAccount().map((accountRow) { - final accountIndex = accountRow.getId(); - final balance = monero_wallet.getFullBalance(accountIndex: accountIndex); + final balance = monero.SubaddressAccountRow_getUnlockedBalance(accountRow); return Account( - id: accountRow.getId(), - label: accountRow.getLabel(), - balance: moneroAmountToString(amount: balance), + id: monero.SubaddressAccountRow_getRowId(accountRow), + label: monero.SubaddressAccountRow_getLabel(accountRow), + balance: moneroAmountToString(amount: monero.Wallet_amountFromString(balance)), ); }).toList(); diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index dbd1a89ae..676a9536c 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,8 +1,8 @@ -import 'package:flutter/services.dart'; -import 'package:mobx/mobx.dart'; +import 'package:cw_core/subaddress.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; -import 'package:cw_core/subaddress.dart'; +import 'package:flutter/services.dart'; +import 'package:mobx/mobx.dart'; part 'monero_subaddress_list.g.dart'; @@ -50,19 +50,22 @@ abstract class MoneroSubaddressListBase with Store { subaddresses = [primary] + rest.toList(); } - return subaddresses.map((subaddressRow) { + return subaddresses.map((s) { + final address = s.address; + final label = s.label; + final id = s.addressIndex; final hasDefaultAddressName = - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() || - subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase(); - final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName; + label.toLowerCase() == 'Primary account'.toLowerCase() || + label.toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( - id: subaddressRow.getId(), - address: subaddressRow.getAddress(), + id: id, + address: address, label: isPrimaryAddress ? 'Primary address' : hasDefaultAddressName ? '' - : subaddressRow.getLabel()); + : label); }).toList(); } @@ -121,8 +124,8 @@ abstract class MoneroSubaddressListBase with Store { Future> _getAllUnusedAddresses( {required int accountIndex, required String label}) async { final allAddresses = subaddress_list.getAllSubaddresses(); - - if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) { + final lastAddress = allAddresses.last.address; + if (allAddresses.isEmpty || _usedAddresses.contains(lastAddress)) { final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); if (!isAddressUnused) { return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); @@ -130,13 +133,18 @@ abstract class MoneroSubaddressListBase with Store { } return allAddresses - .map((subaddressRow) => Subaddress( - id: subaddressRow.getId(), - address: subaddressRow.getAddress(), - label: subaddressRow.getId() == 0 && - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + .map((s) { + final id = s.addressIndex; + final address = s.address; + final label = s.label; + return Subaddress( + id: id, + address: address, + label: id == 0 && + label.toLowerCase() == 'Primary account'.toLowerCase() ? 'Primary address' - : subaddressRow.getLabel())) + : label); + }) .toList(); } @@ -145,7 +153,10 @@ abstract class MoneroSubaddressListBase with Store { return subaddress_list .getAllSubaddresses() - .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress())) + .where((s) { + final address = s.address; + return !_usedAddresses.contains(address); + }) .isNotEmpty; } } diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 8af6b653c..4b596648e 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:ffi'; import 'dart:io'; +import 'dart:isolate'; import 'package:cw_core/account.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -12,15 +14,18 @@ import 'package:cw_core/node.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/account_list.dart'; import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/transaction_history.dart' as transaction_history; import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; @@ -32,6 +37,7 @@ import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; +import 'package:monero/monero.dart' as monero; part 'monero_wallet.g.dart'; @@ -41,10 +47,11 @@ const MIN_RESTORE_HEIGHT = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase - extends WalletBase with Store { +abstract class MoneroWalletBase extends WalletBase with Store { MoneroWalletBase( - {required WalletInfo walletInfo, required Box unspentCoinsInfo}) + {required WalletInfo walletInfo, + required Box unspentCoinsInfo}) : balance = ObservableMap.of({ CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), @@ -60,13 +67,16 @@ abstract class MoneroWalletBase transactionHistory = MoneroTransactionHistory(); walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { + _onAccountChangeReaction = + reaction((_) => walletAddresses.account, (Account? account) { if (account == null) return; - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: account.id)) }); _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); _askForUpdateTransactionHistory(); @@ -100,6 +110,9 @@ abstract class MoneroWalletBase @override String get seed => monero_wallet.getSeed(); + String seedLegacy(String? language) { + return monero_wallet.getSeedLegacy(language); + } @override MoneroWalletKeys get keys => MoneroWalletKeys( @@ -108,7 +121,8 @@ abstract class MoneroWalletBase publicSpendKey: monero_wallet.getPublicSpendKey(), publicViewKey: monero_wallet.getPublicViewKey()); - int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height; + int? get restoreHeight => + transactionHistory.transactions.values.firstOrNull?.height; monero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -119,11 +133,13 @@ abstract class MoneroWalletBase Future init() async { await walletAddresses.init(); - balance = ObservableMap.of({ + balance = ObservableMap.of({ currency: MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + fullBalance: monero_wallet.getFullBalance( + accountIndex: walletAddresses.account!.id), + unlockedBalance: monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id)) }); _setListeners(); await updateTransactions(); @@ -132,19 +148,20 @@ abstract class MoneroWalletBase monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); if (monero_wallet.getCurrentHeight() <= 1) { - monero_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight); + monero_wallet.setRefreshFromBlockHeight( + height: walletInfo.restoreHeight); } } - _autoSaveTimer = - Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + _autoSaveTimer = Timer.periodic( + Duration(seconds: _autoSaveInterval), (_) async => await save()); } @override Future? updateBalance() => null; @override - void close() { + void close() async { _listener?.stop(); _onAccountChangeReaction?.reaction.dispose(); _autoSaveTimer?.cancel(); @@ -209,8 +226,8 @@ abstract class MoneroWalletBase final inputs = []; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; - final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + final unlockedBalance = monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id); var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -232,16 +249,20 @@ abstract class MoneroWalletBase final spendAllCoins = inputs.length == unspentCoins.length; if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); + if (outputs.any( + (item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw MoneroTransactionCreationException( + 'You do not have enough XMR to send this amount.'); } - final int totalAmount = - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = outputs.fold( + 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); - final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); + final estimatedFee = + calculateEstimatedFee(_credentials.priority, totalAmount); if (unlockedBalance < totalAmount) { - throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.'); + throw MoneroTransactionCreationException( + 'You do not have enough XMR to send this amount.'); } if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) { @@ -249,22 +270,28 @@ abstract class MoneroWalletBase } final moneroOutputs = outputs.map((output) { - final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; + final outputAddress = + output.isParsedAddress ? output.extractedAddress : output.address; return MoneroOutput( - address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); + address: outputAddress!, + amount: output.cryptoAmount!.replaceAll(',', '.')); }).toList(); - pendingTransactionDescription = await transaction_history.createTransactionMultDest( - outputs: moneroOutputs, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id, - preferredInputs: inputs); + pendingTransactionDescription = + await transaction_history.createTransactionMultDest( + outputs: moneroOutputs, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } else { final output = outputs.first; - final address = output.isParsedAddress ? output.extractedAddress : output.address; - final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); - final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; + final address = + output.isParsedAddress ? output.extractedAddress : output.address; + final amount = + output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final formattedAmount = + output.sendAll ? null : output.formattedCryptoAmount; if ((formattedAmount != null && unlockedBalance < formattedAmount) || (formattedAmount == null && unlockedBalance <= 0)) { @@ -274,19 +301,22 @@ abstract class MoneroWalletBase 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); } - final estimatedFee = calculateEstimatedFee(_credentials.priority, formattedAmount); + final estimatedFee = + calculateEstimatedFee(_credentials.priority, formattedAmount); if (!spendAllCoins && - ((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) || + ((formattedAmount != null && + allInputsAmount < (formattedAmount + estimatedFee)) || formattedAmount == null)) { throw MoneroTransactionNoInputsException(inputs.length); } - pendingTransactionDescription = await transaction_history.createTransaction( - address: address!, - amount: amount, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account!.id, - preferredInputs: inputs); + pendingTransactionDescription = + await transaction_history.createTransaction( + address: address!, + amount: amount, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); } return PendingMoneroTransaction(pendingTransactionDescription); @@ -325,18 +355,36 @@ abstract class MoneroWalletBase } await walletAddresses.updateAddressesInBox(); - await backupWalletFiles(name); await monero_wallet.store(); + try { + await backupWalletFiles(name); + } catch (e) { + print("¯\\_(ツ)_/¯"); + print(e); + } } @override Future renameWalletFiles(String newWalletName) async { final currentWalletDirPath = await pathForWalletDir(name: name, type: type); - + if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) { + // NOTE: this is realistically only required on windows. + print("closing wallet"); + final wmaddr = wmPtr.address; + final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address; + await Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + }); + openedWalletsByPath.remove("$currentWalletDirPath/$name"); + print("wallet closed"); + } try { // -- rename the waller folder -- - final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type)); - final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type); + final currentWalletDir = + Directory(await pathForWalletDir(name: name, type: type)); + final newWalletDirPath = + await pathForWalletDir(name: newWalletName, type: type); await currentWalletDir.rename(newWalletDirPath); // -- use new waller folder to rename files with old names still -- @@ -346,7 +394,8 @@ abstract class MoneroWalletBase final currentKeysFile = File('$renamedWalletPath.keys'); final currentAddressListFile = File('$renamedWalletPath.address.txt'); - final newWalletPath = await pathForWallet(name: newWalletName, type: type); + final newWalletPath = + await pathForWallet(name: newWalletName, type: type); if (currentCacheFile.existsSync()) { await currentCacheFile.rename(newWalletPath); @@ -366,7 +415,8 @@ abstract class MoneroWalletBase final currentKeysFile = File('$currentWalletPath.keys'); final currentAddressListFile = File('$currentWalletPath.address.txt'); - final newWalletPath = await pathForWallet(name: newWalletName, type: type); + final newWalletPath = + await pathForWallet(name: newWalletName, type: type); // Copies current wallet files into new wallet name's dir and files if (currentCacheFile.existsSync()) { @@ -385,7 +435,8 @@ abstract class MoneroWalletBase } @override - Future changePassword(String password) async => monero_wallet.setPasswordSync(password); + Future changePassword(String password) async => + monero_wallet.setPasswordSync(password); Future getNodeHeight() async => monero_wallet.getNodeHeight(); @@ -419,10 +470,19 @@ abstract class MoneroWalletBase final coinCount = countOfCoins(); for (var i = 0; i < coinCount; i++) { final coin = getCoin(i); - if (coin.spent == 0) { - final unspent = MoneroUnspent.fromCoinsInfoRow(coin); + final coinSpent = monero.CoinsInfo_spent(coin); + if (coinSpent == false) { + final unspent = MoneroUnspent( + monero.CoinsInfo_address(coin), + monero.CoinsInfo_hash(coin), + monero.CoinsInfo_keyImage(coin), + monero.CoinsInfo_amount(coin), + monero.CoinsInfo_frozen(coin), + monero.CoinsInfo_unlocked(coin), + ); + // TODO: double-check the logic here if (unspent.hash.isNotEmpty) { - unspent.isChange = transaction_history.getTransaction(unspent.hash).direction == 1; + unspent.isChange = transaction_history.getTransaction(unspent.hash).isSpend == true; } unspentCoins.add(unspent); } @@ -484,13 +544,15 @@ abstract class MoneroWalletBase Future _refreshUnspentCoinsInfo() async { try { final List keys = []; - final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => - element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); + final currentWalletUnspentCoins = unspentCoinsInfo.values.where( + (element) => + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id); if (currentWalletUnspentCoins.isNotEmpty) { currentWalletUnspentCoins.forEach((element) { - final existUnspentCoins = - unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); + final existUnspentCoins = unspentCoins + .where((coin) => element.keyImage!.contains(coin.keyImage!)); if (existUnspentCoins.isEmpty) { keys.add(element.key); @@ -507,13 +569,15 @@ abstract class MoneroWalletBase } String getTransactionAddress(int accountIndex, int addressIndex) => - monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); + monero_wallet.getAddress( + accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { transaction_history.refreshTransactions(); return _getAllTransactionsOfAccount(walletAddresses.account?.id) - .fold>({}, + .fold>( + {}, (Map acc, MoneroTransactionInfo tx) { acc[tx.id] = tx; return acc; @@ -541,11 +605,31 @@ abstract class MoneroWalletBase String getSubaddressLabel(int accountIndex, int addressIndex) => monero_wallet.getSubaddressLabel(accountIndex, addressIndex); - List _getAllTransactionsOfAccount(int? accountIndex) => transaction_history - .getAllTransactions() - .map((row) => MoneroTransactionInfo.fromRow(row)) - .where((element) => element.accountIndex == (accountIndex ?? 0)) - .toList(); + List _getAllTransactionsOfAccount(int? accountIndex) => + transaction_history + .getAllTransactions() + .map( + (row) => MoneroTransactionInfo( + row.hash, + row.blockheight, + row.isSpend + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + row.timeStamp, + row.isPending, + row.amount, + row.accountIndex, + 0, + row.fee, + row.confirmations, + )..additionalInfo = { + 'key': row.key, + 'accountIndex': row.accountIndex, + 'addressIndex': row.addressIndex + }, + ) + .where((element) => element.accountIndex == (accountIndex ?? 0)) + .toList(); void _setListeners() { _listener?.stop(); @@ -583,7 +667,8 @@ abstract class MoneroWalletBase } int _getHeightDistance(DateTime date) { - final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final distance = + DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; final daysTmp = (distance / 86400).round(); final days = daysTmp < 1 ? 1 : daysTmp; @@ -611,22 +696,27 @@ abstract class MoneroWalletBase balance[currency]!.unlockedBalance != unlockedBalance || balance[currency]!.frozenBalance != frozenBalance) { balance[currency] = MoneroBalance( - fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance); + fullBalance: fullBalance, + unlockedBalance: unlockedBalance, + frozenBalance: frozenBalance); } } - Future _askForUpdateTransactionHistory() async => await updateTransactions(); + Future _askForUpdateTransactionHistory() async => + await updateTransactions(); - int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + int _getFullBalance() => + monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); - int _getUnlockedBalance() => - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + int _getUnlockedBalance() => monero_wallet.getUnlockedBalance( + accountIndex: walletAddresses.account!.id); int _getFrozenBalance() { var frozenBalance = 0; for (var coin in unspentCoinsInfo.values.where((element) => - element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { + element.walletId == id && + element.accountIndex == walletAddresses.account!.id)) { if (coin.isFrozen) frozenBalance += coin.value; } diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index bc59499f9..ea2f3b766 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -1,3 +1,4 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_core/pathForWallet.dart'; @@ -10,10 +11,12 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; +import 'package:cw_monero/api/wallet_manager.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/widgets.dart'; import 'package:hive/hive.dart'; import 'package:polyseed/polyseed.dart'; +import 'package:monero/monero.dart' as monero; class MoneroNewWalletCredentials extends WalletCredentials { MoneroNewWalletCredentials( @@ -174,6 +177,22 @@ class MoneroWalletService extends WalletService remove(String wallet) async { final path = await pathForWalletDir(name: wallet, type: getType()); + if (openedWalletsByPath["$path/$wallet"] != null) { + // NOTE: this is realistically only required on windows. + print("closing wallet"); + final wmaddr = wmPtr.address; + final waddr = openedWalletsByPath["$path/$wallet"]!.address; + // await Isolate.run(() { + monero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), + Pointer.fromAddress(waddr), + false + ); + // }); + openedWalletsByPath.remove("$path/$wallet"); + print("wallet closed"); + } + final file = Directory(path); final isExist = file.existsSync(); diff --git a/cw_monero/macos/Classes/CwMoneroPlugin.swift b/cw_monero/macos/Classes/CwMoneroPlugin.swift deleted file mode 100644 index d4ff81e1c..000000000 --- a/cw_monero/macos/Classes/CwMoneroPlugin.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Cocoa -import FlutterMacOS - -public class CwMoneroPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "cw_monero", binaryMessenger: registrar.messenger) - let instance = CwMoneroPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getPlatformVersion": - result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) - default: - result(FlutterMethodNotImplemented) - } - } -} diff --git a/cw_monero/macos/Classes/CwWalletListener.h b/cw_monero/macos/Classes/CwWalletListener.h deleted file mode 100644 index cbfcb0c4e..000000000 --- a/cw_monero/macos/Classes/CwWalletListener.h +++ /dev/null @@ -1,23 +0,0 @@ -#include - -struct CWMoneroWalletListener; - -typedef int8_t (*on_new_block_callback)(uint64_t height); -typedef int8_t (*on_need_to_refresh_callback)(); - -typedef struct CWMoneroWalletListener -{ - // on_money_spent_callback *on_money_spent; - // on_money_received_callback *on_money_received; - // on_unconfirmed_money_received_callback *on_unconfirmed_money_received; - // on_new_block_callback *on_new_block; - // on_updated_callback *on_updated; - // on_refreshed_callback *on_refreshed; - - on_new_block_callback on_new_block; -} CWMoneroWalletListener; - -struct TestListener { - // int8_t x; - on_new_block_callback on_new_block; -}; \ No newline at end of file diff --git a/cw_monero/macos/Classes/monero_api.cpp b/cw_monero/macos/Classes/monero_api.cpp deleted file mode 100644 index fe75dea98..000000000 --- a/cw_monero/macos/Classes/monero_api.cpp +++ /dev/null @@ -1,1032 +0,0 @@ -#include -#include "cstdlib" -#include -#include -#include -#include -#include -#include -#include -#include "thread" -#include "CwWalletListener.h" -#if __APPLE__ -// Fix for randomx on ios -void __clear_cache(void* start, void* end) { } -#include "../External/macos/include/wallet2_api.h" -#else -#include "../External/android/include/wallet2_api.h" -#endif - -using namespace std::chrono_literals; -#ifdef __cplusplus -extern "C" -{ -#endif - const uint64_t MONERO_BLOCK_SIZE = 1000; - - struct Utf8Box - { - char *value; - - Utf8Box(char *_value) - { - value = _value; - } - }; - - struct SubaddressRow - { - uint64_t id; - char *address; - char *label; - - SubaddressRow(std::size_t _id, char *_address, char *_label) - { - id = static_cast(_id); - address = _address; - label = _label; - } - }; - - struct AccountRow - { - uint64_t id; - char *label; - - AccountRow(std::size_t _id, char *_label) - { - id = static_cast(_id); - label = _label; - } - }; - - struct MoneroWalletListener : Monero::WalletListener - { - uint64_t m_height; - bool m_need_to_refresh; - bool m_new_transaction; - - MoneroWalletListener() - { - m_height = 0; - m_need_to_refresh = false; - m_new_transaction = false; - } - - void moneySpent(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void moneyReceived(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) - { - m_new_transaction = true; - } - - void newBlock(uint64_t height) - { - m_height = height; - } - - void updated() - { - m_new_transaction = true; - } - - void refreshed() - { - m_need_to_refresh = true; - } - - void resetNeedToRefresh() - { - m_need_to_refresh = false; - } - - bool isNeedToRefresh() - { - return m_need_to_refresh; - } - - bool isNewTransactionExist() - { - return m_new_transaction; - } - - void resetIsNewTransactionExist() - { - m_new_transaction = false; - } - - uint64_t height() - { - return m_height; - } - }; - - struct TransactionInfoRow - { - uint64_t amount; - uint64_t fee; - uint64_t blockHeight; - uint64_t confirmations; - uint32_t subaddrAccount; - int8_t direction; - int8_t isPending; - uint32_t subaddrIndex; - - char *hash; - char *paymentId; - - int64_t datetime; - - TransactionInfoRow(Monero::TransactionInfo *transaction) - { - amount = transaction->amount(); - fee = transaction->fee(); - blockHeight = transaction->blockHeight(); - subaddrAccount = transaction->subaddrAccount(); - std::set::iterator it = transaction->subaddrIndex().begin(); - subaddrIndex = *it; - confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); - direction = transaction->direction(); - isPending = static_cast(transaction->isPending()); - std::string *hash_str = new std::string(transaction->hash()); - hash = strdup(hash_str->c_str()); - paymentId = strdup(transaction->paymentId().c_str()); - } - }; - - struct PendingTransactionRaw - { - uint64_t amount; - uint64_t fee; - char *hash; - char *hex; - char *txKey; - Monero::PendingTransaction *transaction; - - PendingTransactionRaw(Monero::PendingTransaction *_transaction) - { - transaction = _transaction; - amount = _transaction->amount(); - fee = _transaction->fee(); - hash = strdup(_transaction->txid()[0].c_str()); - hex = strdup(_transaction->hex()[0].c_str()); - txKey = strdup(_transaction->txKey()[0].c_str()); - } - }; - - struct CoinsInfoRow - { - uint64_t blockHeight; - char *hash; - uint64_t internalOutputIndex; - uint64_t globalOutputIndex; - bool spent; - bool frozen; - uint64_t spentHeight; - uint64_t amount; - bool rct; - bool keyImageKnown; - uint64_t pkIndex; - uint32_t subaddrIndex; - uint32_t subaddrAccount; - char *address; - char *addressLabel; - char *keyImage; - uint64_t unlockTime; - bool unlocked; - char *pubKey; - bool coinbase; - char *description; - - CoinsInfoRow(Monero::CoinsInfo *coinsInfo) - { - blockHeight = coinsInfo->blockHeight(); - std::string *hash_str = new std::string(coinsInfo->hash()); - hash = strdup(hash_str->c_str()); - internalOutputIndex = coinsInfo->internalOutputIndex(); - globalOutputIndex = coinsInfo->globalOutputIndex(); - spent = coinsInfo->spent(); - frozen = coinsInfo->frozen(); - spentHeight = coinsInfo->spentHeight(); - amount = coinsInfo->amount(); - rct = coinsInfo->rct(); - keyImageKnown = coinsInfo->keyImageKnown(); - pkIndex = coinsInfo->pkIndex(); - subaddrIndex = coinsInfo->subaddrIndex(); - subaddrAccount = coinsInfo->subaddrAccount(); - address = strdup(coinsInfo->address().c_str()) ; - addressLabel = strdup(coinsInfo->addressLabel().c_str()); - keyImage = strdup(coinsInfo->keyImage().c_str()); - unlockTime = coinsInfo->unlockTime(); - unlocked = coinsInfo->unlocked(); - pubKey = strdup(coinsInfo->pubKey().c_str()); - coinbase = coinsInfo->coinbase(); - description = strdup(coinsInfo->description().c_str()); - } - - void setUnlocked(bool unlocked); - }; - - Monero::Coins *m_coins; - - Monero::Wallet *m_wallet; - Monero::TransactionHistory *m_transaction_history; - MoneroWalletListener *m_listener; - Monero::Subaddress *m_subaddress; - Monero::SubaddressAccount *m_account; - uint64_t m_last_known_wallet_height; - uint64_t m_cached_syncing_blockchain_height = 0; - std::list m_coins_info; - std::mutex store_lock; - bool is_storing = false; - - void change_current_wallet(Monero::Wallet *wallet) - { - m_wallet = wallet; - m_listener = nullptr; - - - if (wallet != nullptr) - { - m_transaction_history = wallet->history(); - } - else - { - m_transaction_history = nullptr; - } - - if (wallet != nullptr) - { - m_account = wallet->subaddressAccount(); - } - else - { - m_account = nullptr; - } - - if (wallet != nullptr) - { - m_subaddress = wallet->subaddress(); - } - else - { - m_subaddress = nullptr; - } - - m_coins_info = std::list(); - - if (wallet != nullptr) - { - m_coins = wallet->coins(); - } - else - { - m_coins = nullptr; - } - } - - Monero::Wallet *get_current_wallet() - { - return m_wallet; - } - - bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (wallet->status() != Monero::Wallet::Status_Ok) - { - error = strdup(wallet->errorString().c_str()); - return false; - } - - change_current_wallet(wallet); - - return true; - } - - bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( - std::string(path), - std::string(password), - std::string(seed), - _networkType, - (uint64_t)restoreHeight); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( - std::string(path), - std::string(password), - std::string(language), - _networkType, - (uint64_t)restoreHeight, - std::string(address), - std::string(viewKey), - std::string(spendKey)); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool restore_wallet_from_spend_key(char *path, char *password, char *seed, char *language, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) - { - Monero::NetworkType _networkType = static_cast(networkType); - Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createDeterministicWalletFromSpendKey( - std::string(path), - std::string(password), - std::string(language), - _networkType, - (uint64_t)restoreHeight, - std::string(spendKey)); - - // Cache Raw to support Polyseed - wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); - - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - - if (status != Monero::Wallet::Status_Ok || !errorString.empty()) - { - error = strdup(errorString.c_str()); - return false; - } - - change_current_wallet(wallet); - return true; - } - - bool load_wallet(char *path, char *password, int32_t nettype) - { - nice(19); - Monero::NetworkType networkType = static_cast(nettype); - Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); - Monero::Wallet *wallet = walletManager->openWallet(std::string(path), std::string(password), networkType); - int status; - std::string errorString; - - wallet->statusWithErrorString(status, errorString); - change_current_wallet(wallet); - - return !(status != Monero::Wallet::Status_Ok || !errorString.empty()); - } - - char *error_string() { - return strdup(get_current_wallet()->errorString().c_str()); - } - - - bool is_wallet_exist(char *path) - { - return Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(path)); - } - - void close_current_wallet() - { - Monero::WalletManagerFactory::getWalletManager()->closeWallet(get_current_wallet()); - change_current_wallet(nullptr); - } - - char *get_filename() - { - return strdup(get_current_wallet()->filename().c_str()); - } - - char *secret_view_key() - { - return strdup(get_current_wallet()->secretViewKey().c_str()); - } - - char *public_view_key() - { - return strdup(get_current_wallet()->publicViewKey().c_str()); - } - - char *secret_spend_key() - { - return strdup(get_current_wallet()->secretSpendKey().c_str()); - } - - char *public_spend_key() - { - return strdup(get_current_wallet()->publicSpendKey().c_str()); - } - - char *get_address(uint32_t account_index, uint32_t address_index) - { - return strdup(get_current_wallet()->address(account_index, address_index).c_str()); - } - - - const char *seed() - { - std::string _rawSeed = get_current_wallet()->getCacheAttribute("cakewallet.seed"); - if (!_rawSeed.empty()) - { - return strdup(_rawSeed.c_str()); - } - return strdup(get_current_wallet()->seed().c_str()); - } - - uint64_t get_full_balance(uint32_t account_index) - { - return get_current_wallet()->balance(account_index); - } - - uint64_t get_unlocked_balance(uint32_t account_index) - { - return get_current_wallet()->unlockedBalance(account_index); - } - - uint64_t get_current_height() - { - return get_current_wallet()->blockChainHeight(); - } - - uint64_t get_node_height() - { - return get_current_wallet()->daemonBlockChainHeight(); - } - - bool connect_to_node(char *error) - { - nice(19); - bool is_connected = get_current_wallet()->connectToDaemon(); - - if (!is_connected) - { - error = strdup(get_current_wallet()->errorString().c_str()); - } - - return is_connected; - } - - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error) - { - nice(19); - Monero::Wallet *wallet = get_current_wallet(); - - std::string _login = ""; - std::string _password = ""; - std::string _socksProxyAddress = ""; - - if (login != nullptr) - { - _login = std::string(login); - } - - if (password != nullptr) - { - _password = std::string(password); - } - - if (socksProxyAddress != nullptr) - { - _socksProxyAddress = std::string(socksProxyAddress); - } - - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress); - - if (!inited) - { - error = strdup(wallet->errorString().c_str()); - } else if (!wallet->connectToDaemon()) { - error = strdup(wallet->errorString().c_str()); - } - - return inited; - } - - bool is_connected() - { - return get_current_wallet()->connected(); - } - - void start_refresh() - { - get_current_wallet()->refreshAsync(); - get_current_wallet()->startRefresh(); - } - - void set_refresh_from_block_height(uint64_t height) - { - get_current_wallet()->setRefreshFromBlockHeight(height); - } - - void set_recovering_from_seed(bool is_recovery) - { - get_current_wallet()->setRecoveringFromSeed(is_recovery); - } - - void store(char *path) - { - store_lock.lock(); - if (is_storing) { - return; - } - - is_storing = true; - get_current_wallet()->store(std::string(path)); - is_storing = false; - store_lock.unlock(); - } - - bool set_password(char *password, Utf8Box &error) { - bool is_changed = get_current_wallet()->setPassword(std::string(password)); - - if (!is_changed) { - error = Utf8Box(strdup(get_current_wallet()->errorString().c_str())); - } - - return is_changed; - } - - bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, - char **preferred_inputs, uint32_t preferred_inputs_size, - Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - std::set _preferred_inputs; - - for (int i = 0; i < preferred_inputs_size; i++) { - _preferred_inputs.insert(std::string(*preferred_inputs)); - preferred_inputs++; - } - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - if (amount != nullptr) - { - uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); - } - else - { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); - } - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, - char **preferred_inputs, uint32_t preferred_inputs_size, - Utf8Box &error, PendingTransactionRaw &pendingTransaction) - { - nice(19); - - std::vector _addresses; - std::vector _amounts; - - for (int i = 0; i < size; i++) { - _addresses.push_back(std::string(*addresses)); - _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); - addresses++; - amounts++; - } - - std::set _preferred_inputs; - - for (int i = 0; i < preferred_inputs_size; i++) { - _preferred_inputs.insert(std::string(*preferred_inputs)); - preferred_inputs++; - } - - auto priority = static_cast(priority_raw); - std::string _payment_id; - Monero::PendingTransaction *transaction; - - if (payment_id != nullptr) - { - _payment_id = std::string(payment_id); - } - - transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, m_wallet->defaultMixin(), priority, subaddr_account); - - int status = transaction->status(); - - if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) - { - error = Utf8Box(strdup(transaction->errorString().c_str())); - return false; - } - - if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - pendingTransaction = PendingTransactionRaw(transaction); - return true; - } - - bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) - { - bool committed = transaction->transaction->commit(); - - if (!committed) - { - error = Utf8Box(strdup(transaction->transaction->errorString().c_str())); - } else if (m_listener != nullptr) { - m_listener->m_new_transaction = true; - } - - return committed; - } - - uint64_t get_node_height_or_update(uint64_t base_eight) - { - if (m_cached_syncing_blockchain_height < base_eight) { - m_cached_syncing_blockchain_height = base_eight; - } - - return m_cached_syncing_blockchain_height; - } - - uint64_t get_syncing_height() - { - if (m_listener == nullptr) { - return 0; - } - - uint64_t height = m_listener->height(); - - if (height <= 1) { - return 0; - } - - if (height != m_last_known_wallet_height) - { - m_last_known_wallet_height = height; - } - - return height; - } - - uint64_t is_needed_to_refresh() - { - if (m_listener == nullptr) { - return false; - } - - bool should_refresh = m_listener->isNeedToRefresh(); - - if (should_refresh) { - m_listener->resetNeedToRefresh(); - } - - return should_refresh; - } - - uint8_t is_new_transaction_exist() - { - if (m_listener == nullptr) { - return false; - } - - bool is_new_transaction_exist = m_listener->isNewTransactionExist(); - - if (is_new_transaction_exist) - { - m_listener->resetIsNewTransactionExist(); - } - - return is_new_transaction_exist; - } - - void set_listener() - { - m_last_known_wallet_height = 0; - - if (m_listener != nullptr) - { - free(m_listener); - } - - m_listener = new MoneroWalletListener(); - get_current_wallet()->setListener(m_listener); - } - - int64_t *subaddrress_get_all() - { - std::vector _subaddresses = m_subaddress->getAll(); - size_t size = _subaddresses.size(); - int64_t *subaddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressRow *row = _subaddresses[i]; - SubaddressRow *_row = new SubaddressRow(row->getRowId(), strdup(row->getAddress().c_str()), strdup(row->getLabel().c_str())); - subaddresses[i] = reinterpret_cast(_row); - } - - return subaddresses; - } - - int32_t subaddrress_size() - { - std::vector _subaddresses = m_subaddress->getAll(); - return _subaddresses.size(); - } - - void subaddress_add_row(uint32_t accountIndex, char *label) - { - m_subaddress->addRow(accountIndex, std::string(label)); - } - - void subaddress_set_label(uint32_t accountIndex, uint32_t addressIndex, char *label) - { - m_subaddress->setLabel(accountIndex, addressIndex, std::string(label)); - } - - void subaddress_refresh(uint32_t accountIndex) - { - m_subaddress->refresh(accountIndex); - } - - int32_t account_size() - { - std::vector _accocunts = m_account->getAll(); - return _accocunts.size(); - } - - int64_t *account_get_all() - { - std::vector _accocunts = m_account->getAll(); - size_t size = _accocunts.size(); - int64_t *accocunts = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::SubaddressAccountRow *row = _accocunts[i]; - AccountRow *_row = new AccountRow(row->getRowId(), strdup(row->getLabel().c_str())); - accocunts[i] = reinterpret_cast(_row); - } - - return accocunts; - } - - void account_add_row(char *label) - { - m_account->addRow(std::string(label)); - } - - void account_set_label_row(uint32_t account_index, char *label) - { - m_account->setLabel(account_index, label); - } - - void account_refresh() - { - m_account->refresh(); - } - - int64_t *transactions_get_all() - { - std::vector transactions = m_transaction_history->getAll(); - size_t size = transactions.size(); - int64_t *transactionAddresses = (int64_t *)malloc(size * sizeof(int64_t)); - - for (int i = 0; i < size; i++) - { - Monero::TransactionInfo *row = transactions[i]; - TransactionInfoRow *tx = new TransactionInfoRow(row); - transactionAddresses[i] = reinterpret_cast(tx); - } - - return transactionAddresses; - } - - void transactions_refresh() - { - m_transaction_history->refresh(); - } - - int64_t transactions_count() - { - return m_transaction_history->count(); - } - - TransactionInfoRow* get_transaction(char * txId) - { - Monero::TransactionInfo *row = m_transaction_history->transaction(std::string(txId)); - return new TransactionInfoRow(row); - } - - int LedgerExchange( - unsigned char *command, - unsigned int cmd_len, - unsigned char *response, - unsigned int max_resp_len) - { - return -1; - } - - int LedgerFind(char *buffer, size_t len) - { - return -1; - } - - void on_startup() - { - Monero::Utils::onStartup(); - Monero::WalletManagerFactory::setLogLevel(0); - } - - void rescan_blockchain() - { - m_wallet->rescanBlockchainAsync(); - } - - char * get_tx_key(char * txId) - { - return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); - } - - char *get_subaddress_label(uint32_t accountIndex, uint32_t addressIndex) - { - return strdup(get_current_wallet()->getSubaddressLabel(accountIndex, addressIndex).c_str()); - } - - void set_trusted_daemon(bool arg) - { - m_wallet->setTrustedDaemon(arg); - } - - bool trusted_daemon() - { - return m_wallet->trustedDaemon(); - } - - CoinsInfoRow* coin(int index) - { - if (index >= 0 && index < m_coins_info.size()) { - std::list::iterator it = m_coins_info.begin(); - std::advance(it, index); - Monero::CoinsInfo* element = *it; - std::cout << "Element at index " << index << ": " << element << std::endl; - return new CoinsInfoRow(element); - } else { - std::cout << "Invalid index." << std::endl; - return nullptr; // Return a default value (nullptr) for invalid index - } - } - - void refresh_coins(uint32_t accountIndex) - { - m_coins_info.clear(); - - m_coins->refresh(); - for (const auto i : m_coins->getAll()) { - if (i->subaddrAccount() == accountIndex && !(i->spent())) { - m_coins_info.push_back(i); - } - } - } - - uint64_t coins_count() - { - return m_coins_info.size(); - } - - CoinsInfoRow** coins_from_account(uint32_t accountIndex) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinInfo = coin(i); - if (coinInfo->subaddrAccount == accountIndex) { - matchingCoins.push_back(coinInfo); - } - } - - CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinInfo = coin(i); - if (std::string(coinInfo->hash) == txid) { - matchingCoins.push_back(coinInfo); - } - } - - *count = matchingCoins.size(); - CoinsInfoRow** result = new CoinsInfoRow*[*count]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) - { - std::vector matchingCoins; - - for (int i = 0; i < coins_count(); i++) { - CoinsInfoRow* coinsInfoRow = coin(i); - for (size_t j = 0; j < keyimageCount; j++) { - if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { - matchingCoins.push_back(coinsInfoRow); - break; - } - } - } - - *count = matchingCoins.size(); - CoinsInfoRow** result = new CoinsInfoRow*[*count]; - std::copy(matchingCoins.begin(), matchingCoins.end(), result); - return result; - } - - void freeze_coin(int index) - { - m_coins->setFrozen(index); - } - - void thaw_coin(int index) - { - m_coins->thaw(index); - } - - // Sign Messages // - - char *sign_message(char *message, char *address = "") - { - return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str()); - } - -#ifdef __cplusplus -} -#endif diff --git a/cw_monero/macos/Classes/monero_api.h b/cw_monero/macos/Classes/monero_api.h deleted file mode 100644 index fa92a038d..000000000 --- a/cw_monero/macos/Classes/monero_api.h +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include -#include "CwWalletListener.h" - -#ifdef __cplusplus -extern "C" { -#endif - -bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error); -bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error); -bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error); -void load_wallet(char *path, char *password, int32_t nettype); -bool is_wallet_exist(char *path); - -char *get_filename(); -const char *seed(); -char *get_address(uint32_t account_index, uint32_t address_index); -uint64_t get_full_balance(uint32_t account_index); -uint64_t get_unlocked_balance(uint32_t account_index); -uint64_t get_current_height(); -uint64_t get_node_height(); - -bool is_connected(); - -bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error); -bool connect_to_node(char *error); -void start_refresh(); -void set_refresh_from_block_height(uint64_t height); -void set_recovering_from_seed(bool is_recovery); -void store(char *path); - -void set_trusted_daemon(bool arg); -bool trusted_daemon(); -char *sign_message(char *message, char *address); - -#ifdef __cplusplus -} -#endif diff --git a/cw_monero/macos/cw_monero_base.podspec b/cw_monero/macos/cw_monero_base.podspec deleted file mode 100644 index aac972c0f..000000000 --- a/cw_monero/macos/cw_monero_base.podspec +++ /dev/null @@ -1,56 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint cw_monero.podspec` to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'cw_monero' - s.version = '0.0.1' - s.summary = 'CW Monero' - s.description = 'Cake Wallet wrapper over Monero project.' - s.homepage = 'http://cakewallet.com' - s.license = { :file => '../LICENSE' } - s.author = { 'CakeWallet' => 'support@cakewallet.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/macos/libs/monero/include/External/ios/**/*.h' - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => '#___VALID_ARCHS___#', 'ENABLE_BITCODE' => 'NO' } - s.swift_version = '5.0' - s.libraries = 'iconv' - - s.subspec 'OpenSSL' do |openssl| - openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' - openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' - openssl.libraries = 'ssl', 'crypto' - openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } - end - - s.subspec 'Sodium' do |sodium| - sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' - sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libsodium.a' - sodium.libraries = 'sodium' - sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } - end - - s.subspec 'Unbound' do |unbound| - unbound.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h' - unbound.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libunbound.a' - unbound.libraries = 'unbound' - unbound.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } - end - - s.subspec 'Boost' do |boost| - boost.preserve_paths = '../../../../../cw_shared_external/ios/External/macos/include/**/*.h', - boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/macos/lib/libboost.a', - boost.libraries = 'boost' - boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include/**" } - end - - s.subspec 'Monero' do |monero| - monero.preserve_paths = 'External/macos/include/**/*.h' - monero.vendored_libraries = 'External/macos/lib/libmonero.a' - monero.libraries = 'monero' - monero.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/macos/include" } - end -end diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index adb50bd02..f5dc3de3f 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -204,10 +204,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -434,6 +434,23 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + monero: + dependency: "direct main" + description: + path: "." + ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + resolved-ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + url: "https://github.com/mrcyjanek/monero.dart" + source: git + version: "0.0.0" + mutex: + dependency: "direct main" + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" package_config: dependency: transitive description: @@ -526,10 +543,10 @@ packages: dependency: "direct main" description: name: polyseed - sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f" + sha256: a340962242d7917b0f3e6bd02c4acc3f90eae8ff766f1244f793ae7a6414dd68 url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.4" pool: dependency: transitive description: diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 56f5d2fa6..53e50877f 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -6,7 +6,7 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: ">=2.17.5 <3.0.0" + sdk: ">=2.19.0 <3.0.0" flutter: ">=1.20.0" dependencies: @@ -22,6 +22,11 @@ dependencies: polyseed: ^0.0.5 cw_core: path: ../cw_core + monero: + git: + url: https://github.com/mrcyjanek/monero.dart + ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + mutex: ^3.1.0 dev_dependencies: flutter_test: @@ -43,16 +48,7 @@ flutter: # The androidPackage and pluginClass identifiers should not ordinarily # be modified. They are used by the tooling to maintain consistency when # adding or updating assets for this project. - plugin: - platforms: - android: - package: com.cakewallet.monero - pluginClass: CwMoneroPlugin - ios: - pluginClass: CwMoneroPlugin - macos: - pluginClass: CwMoneroPlugin # To add assets to your plugin package, add an assets section, like this: # assets: diff --git a/cw_monero/test/cw_monero_method_channel_test.dart b/cw_monero/test/cw_monero_method_channel_test.dart deleted file mode 100644 index 8c1f329f0..000000000 --- a/cw_monero/test/cw_monero_method_channel_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_monero/cw_monero_method_channel.dart'; - -void main() { - MethodChannelCwMonero platform = MethodChannelCwMonero(); - const MethodChannel channel = MethodChannel('cw_monero'); - - TestWidgetsFlutterBinding.ensureInitialized(); - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); - }); - - tearDown(() { - channel.setMockMethodCallHandler(null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); -} diff --git a/cw_monero/test/cw_monero_test.dart b/cw_monero/test/cw_monero_test.dart deleted file mode 100644 index 1eb8d6f79..000000000 --- a/cw_monero/test/cw_monero_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:cw_monero/cw_monero.dart'; -import 'package:cw_monero/cw_monero_platform_interface.dart'; -import 'package:cw_monero/cw_monero_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockCwMoneroPlatform - with MockPlatformInterfaceMixin - implements CwMoneroPlatform { - - @override - Future getPlatformVersion() => Future.value('42'); -} - -void main() { - final CwMoneroPlatform initialPlatform = CwMoneroPlatform.instance; - - test('$MethodChannelCwMonero is the default instance', () { - expect(initialPlatform, isInstanceOf()); - }); - - test('getPlatformVersion', () async { - CwMonero cwMoneroPlugin = CwMonero(); - MockCwMoneroPlatform fakePlatform = MockCwMoneroPlatform(); - CwMoneroPlatform.instance = fakePlatform; - - expect(await cwMoneroPlugin.getPlatformVersion(), '42'); - }); -} diff --git a/cw_wownero/.gitignore b/cw_wownero/.gitignore new file mode 100644 index 000000000..03f8ac268 --- /dev/null +++ b/cw_wownero/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ diff --git a/cw_wownero/.metadata b/cw_wownero/.metadata new file mode 100644 index 000000000..46a2f7f6f --- /dev/null +++ b/cw_wownero/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + - platform: macos + create_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + base_revision: e3c29ec00c9c825c891d75054c63fcc46454dca1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_wownero/CHANGELOG.md b/cw_wownero/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_wownero/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_wownero/LICENSE b/cw_wownero/LICENSE new file mode 100644 index 000000000..6a825df76 --- /dev/null +++ b/cw_wownero/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Cake Technologies LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/cw_wownero/README.md b/cw_wownero/README.md new file mode 100644 index 000000000..191316437 --- /dev/null +++ b/cw_wownero/README.md @@ -0,0 +1,5 @@ +# cw_wownero + +This project is part of Cake Wallet app. + +Copyright (c) 2020 Cake Technologies LLC. \ No newline at end of file diff --git a/cw_wownero/analysis_options.yaml b/cw_wownero/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_wownero/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_wownero/lib/api/account_list.dart b/cw_wownero/lib/api/account_list.dart new file mode 100644 index 000000000..a73e4dcd2 --- /dev/null +++ b/cw_wownero/lib/api/account_list.dart @@ -0,0 +1,72 @@ +import 'package:cw_wownero/api/wallet.dart'; +import 'package:monero/wownero.dart' as wownero; + +wownero.wallet? wptr = null; + +int _wlptrForW = 0; +wownero.WalletListener? _wlptr = null; + +wownero.WalletListener getWlptr() { + if (wptr!.address == _wlptrForW) return _wlptr!; + _wlptrForW = wptr!.address; + _wlptr = wownero.WOWNERO_cw_getWalletListener(wptr!); + return _wlptr!; +} + + +wownero.SubaddressAccount? subaddressAccount; + +bool isUpdating = false; + +void refreshAccounts() { + try { + isUpdating = true; + subaddressAccount = wownero.Wallet_subaddressAccount(wptr!); + wownero.SubaddressAccount_refresh(subaddressAccount!); + isUpdating = false; + } catch (e) { + isUpdating = false; + rethrow; + } +} + +List getAllAccount() { + // final size = wownero.Wallet_numSubaddressAccounts(wptr!); + refreshAccounts(); + int size = wownero.SubaddressAccount_getAll_size(subaddressAccount!); + if (size == 0) { + wownero.Wallet_addSubaddressAccount(wptr!); + return getAllAccount(); + } + return List.generate(size, (index) { + return wownero.SubaddressAccount_getAll_byIndex(subaddressAccount!, index: index); + }); +} + +void addAccountSync({required String label}) { + wownero.Wallet_addSubaddressAccount(wptr!, label: label); +} + +void setLabelForAccountSync({required int accountIndex, required String label}) { + // TODO(mrcyjanek): this may be wrong function? + wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: 0, label: label); +} + +void _addAccount(String label) => addAccountSync(label: label); + +void _setLabelForAccount(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + + setLabelForAccountSync(label: label, accountIndex: accountIndex); +} + +Future addAccount({required String label}) async { + _addAccount(label); + await store(); +} + +Future setLabelForAccount({required int accountIndex, required String label}) async { + _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); + await store(); +} \ No newline at end of file diff --git a/cw_wownero/lib/api/coins_info.dart b/cw_wownero/lib/api/coins_info.dart new file mode 100644 index 000000000..2fd6c097d --- /dev/null +++ b/cw_wownero/lib/api/coins_info.dart @@ -0,0 +1,17 @@ +import 'package:cw_wownero/api/account_list.dart'; +import 'package:monero/wownero.dart' as wownero; + +wownero.Coins? coins = null; + +void refreshCoins(int accountIndex) { + coins = wownero.Wallet_coins(wptr!); + wownero.Coins_refresh(coins!); +} + +int countOfCoins() => wownero.Coins_count(coins!); + +wownero.CoinsInfo getCoin(int index) => wownero.Coins_coin(coins!, index); + +void freezeCoin(int index) => wownero.Coins_setFrozen(coins!, index: index); + +void thawCoin(int index) => wownero.Coins_thaw(coins!, index: index); diff --git a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart new file mode 100644 index 000000000..483b0a174 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart @@ -0,0 +1,5 @@ +class ConnectionToNodeException implements Exception { + ConnectionToNodeException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart b/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart new file mode 100644 index 000000000..7b55ec074 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart @@ -0,0 +1,8 @@ +class CreationTransactionException implements Exception { + CreationTransactionException({required this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart new file mode 100644 index 000000000..b6e0c1f18 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart @@ -0,0 +1,5 @@ +class SetupWalletException implements Exception { + SetupWalletException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart b/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart new file mode 100644 index 000000000..6052366b9 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart @@ -0,0 +1,8 @@ +class WalletCreationException implements Exception { + WalletCreationException({required this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart b/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart new file mode 100644 index 000000000..df7a850a4 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart @@ -0,0 +1,8 @@ +class WalletOpeningException implements Exception { + WalletOpeningException({required this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart new file mode 100644 index 000000000..c6b6c6ef7 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -0,0 +1,5 @@ +class WalletRestoreFromKeysException implements Exception { + WalletRestoreFromKeysException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart new file mode 100644 index 000000000..004cd7958 --- /dev/null +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -0,0 +1,5 @@ +class WalletRestoreFromSeedException implements Exception { + WalletRestoreFromSeedException({required this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/account_row.dart b/cw_wownero/lib/api/structs/account_row.dart new file mode 100644 index 000000000..aa492ee0f --- /dev/null +++ b/cw_wownero/lib/api/structs/account_row.dart @@ -0,0 +1,12 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class AccountRow extends Struct { + @Int64() + external int id; + + external Pointer label; + + String getLabel() => label.toDartString(); + int getId() => id; +} diff --git a/cw_wownero/lib/api/structs/coins_info_row.dart b/cw_wownero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_wownero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_wownero/lib/api/structs/pending_transaction.dart b/cw_wownero/lib/api/structs/pending_transaction.dart new file mode 100644 index 000000000..dc5fbddd0 --- /dev/null +++ b/cw_wownero/lib/api/structs/pending_transaction.dart @@ -0,0 +1,17 @@ + +class PendingTransactionDescription { + PendingTransactionDescription({ + required this.amount, + required this.fee, + required this.hash, + required this.hex, + required this.txKey, + required this.pointerAddress}); + + final int amount; + final int fee; + final String hash; + final String hex; + final String txKey; + final int pointerAddress; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/subaddress_row.dart b/cw_wownero/lib/api/structs/subaddress_row.dart new file mode 100644 index 000000000..d593a793d --- /dev/null +++ b/cw_wownero/lib/api/structs/subaddress_row.dart @@ -0,0 +1,15 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class SubaddressRow extends Struct { + @Int64() + external int id; + + external Pointer address; + + external Pointer label; + + String getLabel() => label.toDartString(); + String getAddress() => address.toDartString(); + int getId() => id; +} \ No newline at end of file diff --git a/cw_wownero/lib/api/structs/transaction_info_row.dart b/cw_wownero/lib/api/structs/transaction_info_row.dart new file mode 100644 index 000000000..bdcc64d3f --- /dev/null +++ b/cw_wownero/lib/api/structs/transaction_info_row.dart @@ -0,0 +1,41 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class TransactionInfoRow extends Struct { + @Uint64() + external int amount; + + @Uint64() + external int fee; + + @Uint64() + external int blockHeight; + + @Uint64() + external int confirmations; + + @Uint32() + external int subaddrAccount; + + @Int8() + external int direction; + + @Int8() + external int isPending; + + @Uint32() + external int subaddrIndex; + + external Pointer hash; + + external Pointer paymentId; + + @Int64() + external int datetime; + + int getDatetime() => datetime; + int getAmount() => amount >= 0 ? amount : amount * -1; + bool getIsPending() => isPending != 0; + String getHash() => hash.toDartString(); + String getPaymentId() => paymentId.toDartString(); +} diff --git a/cw_wownero/lib/api/structs/ut8_box.dart b/cw_wownero/lib/api/structs/ut8_box.dart new file mode 100644 index 000000000..53e678c88 --- /dev/null +++ b/cw_wownero/lib/api/structs/ut8_box.dart @@ -0,0 +1,8 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class Utf8Box extends Struct { + external Pointer value; + + String getValue() => value.toDartString(); +} diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart new file mode 100644 index 000000000..cec7d94cb --- /dev/null +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -0,0 +1,91 @@ +import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/wallet.dart'; +import 'package:monero/wownero.dart' as wownero; + +bool isUpdating = false; + +class SubaddressInfoMetadata { + SubaddressInfoMetadata({ + required this.accountIndex, + }); + int accountIndex; +} + +SubaddressInfoMetadata? subaddress = null; + +void refreshSubaddresses({required int accountIndex}) { + try { + isUpdating = true; + subaddress = SubaddressInfoMetadata(accountIndex: accountIndex); + isUpdating = false; + } catch (e) { + isUpdating = false; + rethrow; + } +} + +class Subaddress { + Subaddress({ + required this.addressIndex, + required this.accountIndex, + }); + String get address => wownero.Wallet_address( + wptr!, + accountIndex: accountIndex, + addressIndex: addressIndex, + ); + final int addressIndex; + final int accountIndex; + String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); +} + +List getAllSubaddresses() { + final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); + return List.generate(size, (index) { + return Subaddress( + accountIndex: subaddress!.accountIndex, + addressIndex: index, + ); + }).reversed.toList(); +} + +void addSubaddressSync({required int accountIndex, required String label}) { + wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex, label: label); + refreshSubaddresses(accountIndex: accountIndex); +} + +void setLabelForSubaddressSync( + {required int accountIndex, required int addressIndex, required String label}) { + wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); +} + +void _addSubaddress(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + + addSubaddressSync(accountIndex: accountIndex, label: label); +} + +void _setLabelForSubaddress(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + final addressIndex = args['addressIndex'] as int; + + setLabelForSubaddressSync( + accountIndex: accountIndex, addressIndex: addressIndex, label: label); +} + +Future addSubaddress({required int accountIndex, required String label}) async { + _addSubaddress({'accountIndex': accountIndex, 'label': label}); + await store(); +} + +Future setLabelForSubaddress( + {required int accountIndex, required int addressIndex, required String label}) async { + _setLabelForSubaddress({ + 'accountIndex': accountIndex, + 'addressIndex': addressIndex, + 'label': label + }); + await store(); +} diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart new file mode 100644 index 000000000..42b2ef6f3 --- /dev/null +++ b/cw_wownero/lib/api/transaction_history.dart @@ -0,0 +1,324 @@ +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart'; +import 'package:cw_wownero/api/wownero_output.dart'; +import 'package:cw_wownero/api/structs/pending_transaction.dart'; +import 'package:ffi/ffi.dart'; +import 'package:monero/wownero.dart' as wownero; +import 'package:monero/src/generated_bindings_wownero.g.dart' as wownero_gen; + + +String getTxKey(String txId) { + return wownero.Wallet_getTxKey(wptr!, txid: txId); +} + +wownero.TransactionHistory? txhistory; + +void refreshTransactions() { + txhistory = wownero.Wallet_history(wptr!); + wownero.TransactionHistory_refresh(txhistory!); +} + +int countOfTransactions() => wownero.TransactionHistory_count(txhistory!); + +List getAllTransactions() { + List dummyTxs = []; + + txhistory = wownero.Wallet_history(wptr!); + wownero.TransactionHistory_refresh(txhistory!); + int size = countOfTransactions(); + final list = List.generate(size, (index) => Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index)))..addAll(dummyTxs); + + final accts = wownero.Wallet_numSubaddressAccounts(wptr!); + for (var i = 0; i < accts; i++) { + final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i); + final availBalance = wownero.Wallet_unlockedBalance(wptr!, accountIndex: i); + if (fullBalance > availBalance) { + if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isNotEmpty) { + dummyTxs.add( + Transaction.dummy( + displayLabel: "", + description: "", + fee: 0, + confirmations: 0, + blockheight: 0, + accountIndex: i, + paymentId: "", + amount: fullBalance - availBalance, + isSpend: false, + hash: "pending", + key: "pending", + txInfo: Pointer.fromAddress(0), + )..timeStamp = DateTime.now() + ); + } + } + } + list.addAll(dummyTxs); + return list; +} + +// TODO(mrcyjanek): ... +Transaction getTransaction(String txId) { + return Transaction(txInfo: wownero.TransactionHistory_transactionById(txhistory!, txid: txId)); +} + +Future createTransactionSync( + {required String address, + required String paymentId, + required int priorityRaw, + String? amount, + int accountIndex = 0, + List preferredInputs = const []}) async { + + final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount); + + final address_ = address.toNativeUtf8(); + final paymentId_ = paymentId.toNativeUtf8(); + final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8(); + + final waddr = wptr!.address; + final addraddr = address_.address; + final paymentIdAddr = paymentId_.address; + final preferredInputsAddr = preferredInputs_.address; + final spaddr = wownero.defaultSeparator.address; + final pendingTx = Pointer.fromAddress(await Isolate.run(() { + final tx = wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction( + Pointer.fromAddress(waddr), + Pointer.fromAddress(addraddr).cast(), + Pointer.fromAddress(paymentIdAddr).cast(), + amt, + 1, + priorityRaw, + accountIndex, + Pointer.fromAddress(preferredInputsAddr).cast(), + Pointer.fromAddress(spaddr), + ); + return tx.address; + })); + calloc.free(address_); + calloc.free(paymentId_); + calloc.free(preferredInputs_); + final String? error = (() { + final status = wownero.PendingTransaction_status(pendingTx); + if (status == 0) { + return null; + } + return wownero.PendingTransaction_errorString(pendingTx); + })(); + + if (error != null) { + final message = error; + throw CreationTransactionException(message: message); + } + + final rAmt = wownero.PendingTransaction_amount(pendingTx); + final rFee = wownero.PendingTransaction_fee(pendingTx); + final rHash = wownero.PendingTransaction_txid(pendingTx, ''); + final rTxKey = rHash; + + return PendingTransactionDescription( + amount: rAmt, + fee: rFee, + hash: rHash, + hex: '', + txKey: rTxKey, + pointerAddress: pendingTx.address, + ); +} + +PendingTransactionDescription createTransactionMultDestSync( + {required List outputs, + required String paymentId, + required int priorityRaw, + int accountIndex = 0, + List preferredInputs = const []}) { + + final txptr = wownero.Wallet_createTransactionMultDest( + wptr!, + dstAddr: outputs.map((e) => e.address).toList(), + isSweepAll: false, + amounts: outputs.map((e) => wownero.Wallet_amountFromString(e.amount)).toList(), + mixinCount: 0, + pendingTransactionPriority: priorityRaw, + subaddr_account: accountIndex, + ); + if (wownero.PendingTransaction_status(txptr) != 0) { + throw CreationTransactionException(message: wownero.PendingTransaction_errorString(txptr)); + } + return PendingTransactionDescription( + amount: wownero.PendingTransaction_amount(txptr), + fee: wownero.PendingTransaction_fee(txptr), + hash: wownero.PendingTransaction_txid(txptr, ''), + hex: wownero.PendingTransaction_txid(txptr, ''), + txKey: wownero.PendingTransaction_txid(txptr, ''), + pointerAddress: txptr.address, + ); +} + +void commitTransactionFromPointerAddress({required int address}) => + commitTransaction(transactionPointer: wownero.PendingTransaction.fromAddress(address)); + +void commitTransaction({required wownero.PendingTransaction transactionPointer}) { + + final txCommit = wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); + + final String? error = (() { + final status = wownero.PendingTransaction_status(transactionPointer.cast()); + if (status == 0) { + return null; + } + return wownero.Wallet_errorString(wptr!); + })(); + + if (error != null) { + throw CreationTransactionException(message: error); + } +} + +Future _createTransactionSync(Map args) async { + final address = args['address'] as String; + final paymentId = args['paymentId'] as String; + final amount = args['amount'] as String?; + final priorityRaw = args['priorityRaw'] as int; + final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; + + return createTransactionSync( + address: address, + paymentId: paymentId, + amount: amount, + priorityRaw: priorityRaw, + accountIndex: accountIndex, + preferredInputs: preferredInputs); +} + +PendingTransactionDescription _createTransactionMultDestSync(Map args) { + final outputs = args['outputs'] as List; + final paymentId = args['paymentId'] as String; + final priorityRaw = args['priorityRaw'] as int; + final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; + + return createTransactionMultDestSync( + outputs: outputs, + paymentId: paymentId, + priorityRaw: priorityRaw, + accountIndex: accountIndex, + preferredInputs: preferredInputs); +} + +Future createTransaction( + {required String address, + required int priorityRaw, + String? amount, + String paymentId = '', + int accountIndex = 0, + List preferredInputs = const []}) async => + _createTransactionSync({ + 'address': address, + 'paymentId': paymentId, + 'amount': amount, + 'priorityRaw': priorityRaw, + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs + }); + +Future createTransactionMultDest( + {required List outputs, + required int priorityRaw, + String paymentId = '', + int accountIndex = 0, + List preferredInputs = const []}) async => + _createTransactionMultDestSync({ + 'outputs': outputs, + 'paymentId': paymentId, + 'priorityRaw': priorityRaw, + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs + }); + + +class Transaction { + final String displayLabel; + String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: 0, addressIndex: 0); + late final String address = wownero.Wallet_address( + wptr!, + accountIndex: 0, + addressIndex: 0, + ); + final String description; + final int fee; + final int confirmations; + late final bool isPending = confirmations < 3; + final int blockheight; + final int addressIndex = 0; + final int accountIndex; + final String paymentId; + final int amount; + final bool isSpend; + late DateTime timeStamp; + late final bool isConfirmed = !isPending; + final String hash; + final String key; + + Map toJson() { + return { + "displayLabel": displayLabel, + "subaddressLabel": subaddressLabel, + "address": address, + "description": description, + "fee": fee, + "confirmations": confirmations, + "isPending": isPending, + "blockheight": blockheight, + "accountIndex": accountIndex, + "addressIndex": addressIndex, + "paymentId": paymentId, + "amount": amount, + "isSpend": isSpend, + "timeStamp": timeStamp.toIso8601String(), + "isConfirmed": isConfirmed, + "hash": hash, + }; + } + + // S finalubAddress? subAddress; + // List transfers = []; + // final int txIndex; + final wownero.TransactionInfo txInfo; + Transaction({ + required this.txInfo, + }) : displayLabel = wownero.TransactionInfo_label(txInfo), + hash = wownero.TransactionInfo_hash(txInfo), + timeStamp = DateTime.fromMillisecondsSinceEpoch( + wownero.TransactionInfo_timestamp(txInfo) * 1000, + ), + isSpend = wownero.TransactionInfo_direction(txInfo) == + wownero.TransactionInfo_Direction.Out, + amount = wownero.TransactionInfo_amount(txInfo), + paymentId = wownero.TransactionInfo_paymentId(txInfo), + accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), + blockheight = wownero.TransactionInfo_blockHeight(txInfo), + confirmations = wownero.TransactionInfo_confirmations(txInfo), + fee = wownero.TransactionInfo_fee(txInfo), + description = wownero.TransactionInfo_description(txInfo), + key = wownero.Wallet_getTxKey(wptr!, txid: wownero.TransactionInfo_hash(txInfo)); + + Transaction.dummy({ + required this.displayLabel, + required this.description, + required this.fee, + required this.confirmations, + required this.blockheight, + required this.accountIndex, + required this.paymentId, + required this.amount, + required this.isSpend, + required this.hash, + required this.key, + required this.txInfo + }); +} \ No newline at end of file diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart new file mode 100644 index 000000000..6a7550d84 --- /dev/null +++ b/cw_wownero/lib/api/wallet.dart @@ -0,0 +1,295 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/exceptions/setup_wallet_exception.dart'; +import 'package:monero/wownero.dart' as wownero; +import 'package:mutex/mutex.dart'; + +int getSyncingHeight() { + // final height = wownero.WOWNERO_cw_WalletListener_height(getWlptr()); + final h2 = wownero.Wallet_blockChainHeight(wptr!); + // print("height: $height / $h2"); + return h2; +} + +bool isNeededToRefresh() { + // final ret = wownero.WOWNERO_cw_WalletListener_isNeedToRefresh(getWlptr()); + // wownero.WOWNERO_cw_WalletListener_resetNeedToRefresh(getWlptr()); + return true; +} + +bool isNewTransactionExist() { + // final ret = + // wownero.WOWNERO_cw_WalletListener_isNewTransactionExist(getWlptr()); + // wownero.WOWNERO_cw_WalletListener_resetIsNewTransactionExist(getWlptr()); + // NOTE: I don't know why wownero is being funky, but + return true; +} + +String getFilename() => wownero.Wallet_filename(wptr!); + +String getSeed() { + // wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + final cakepolyseed = + wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + if (cakepolyseed != "") { + return cakepolyseed; + } + final polyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: ''); + if (polyseed != "") { + return polyseed; + } + final legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + return legacy; +} + +String getSeedLegacy(String? language) { + var legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + if (wownero.Wallet_status(wptr!) != 0) { + wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); + legacy = wownero.Wallet_seed(wptr!, seedOffset: ''); + } + return legacy; +} + +String getAddress({int accountIndex = 0, int addressIndex = 1}) => + wownero.Wallet_address(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); + +int getFullBalance({int accountIndex = 0}) => + wownero.Wallet_balance(wptr!, accountIndex: accountIndex); + +int getUnlockedBalance({int accountIndex = 0}) => + wownero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex); + +int getCurrentHeight() => wownero.Wallet_blockChainHeight(wptr!); + +int getNodeHeightSync() => wownero.Wallet_daemonBlockChainHeight(wptr!); + +bool isConnectedSync() => wownero.Wallet_connected(wptr!) != 0; + +Future setupNodeSync( + {required String address, + String? login, + String? password, + bool useSSL = false, + bool isLightWallet = false, + String? socksProxyAddress}) async { + print(''' +{ + wptr!, + daemonAddress: $address, + useSsl: $useSSL, + proxyAddress: $socksProxyAddress ?? '', + daemonUsername: $login ?? '', + daemonPassword: $password ?? '' +} +'''); + final addr = wptr!.address; + await Isolate.run(() { + wownero.Wallet_init(Pointer.fromAddress(addr), + daemonAddress: address, + useSsl: useSSL, + proxyAddress: socksProxyAddress ?? '', + daemonUsername: login ?? '', + daemonPassword: password ?? ''); + }); + // wownero.Wallet_init3(wptr!, argv0: '', defaultLogBaseName: 'wowneroc', console: true); + + final status = wownero.Wallet_status(wptr!); + + if (status != 0) { + final error = wownero.Wallet_errorString(wptr!); + print("error: $error"); + throw SetupWalletException(message: error); + } + + return status == 0; +} + +void startRefreshSync() { + wownero.Wallet_refreshAsync(wptr!); + wownero.Wallet_startRefresh(wptr!); +} + +Future connectToNode() async { + return true; +} + +void setRefreshFromBlockHeight({required int height}) => + wownero.Wallet_setRefreshFromBlockHeight(wptr!, + refresh_from_block_height: height); + +void setRecoveringFromSeed({required bool isRecovery}) => + wownero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery); + +final storeMutex = Mutex(); +void storeSync() async { + await storeMutex.acquire(); + final addr = wptr!.address; + Isolate.run(() { + wownero.Wallet_store(Pointer.fromAddress(addr)); + }); + storeMutex.release(); +} + +void setPasswordSync(String password) { + wownero.Wallet_setPassword(wptr!, password: password); + + final status = wownero.Wallet_status(wptr!); + if (status != 0) { + throw Exception(wownero.Wallet_errorString(wptr!)); + } +} + +void closeCurrentWallet() { + wownero.Wallet_stop(wptr!); +} + +String getSecretViewKey() => wownero.Wallet_secretViewKey(wptr!); + +String getPublicViewKey() => wownero.Wallet_publicViewKey(wptr!); + +String getSecretSpendKey() => wownero.Wallet_secretSpendKey(wptr!); + +String getPublicSpendKey() => wownero.Wallet_publicSpendKey(wptr!); + +class SyncListener { + SyncListener(this.onNewBlock, this.onNewTransaction) + : _cachedBlockchainHeight = 0, + _lastKnownBlockHeight = 0, + _initialSyncHeight = 0; + + void Function(int, int, double) onNewBlock; + void Function() onNewTransaction; + + Timer? _updateSyncInfoTimer; + int _cachedBlockchainHeight; + int _lastKnownBlockHeight; + int _initialSyncHeight; + + Future getNodeHeightOrUpdate(int baseHeight) async { + if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) { + _cachedBlockchainHeight = await getNodeHeight(); + } + + return _cachedBlockchainHeight; + } + + void start() { + _cachedBlockchainHeight = 0; + _lastKnownBlockHeight = 0; + _initialSyncHeight = 0; + _updateSyncInfoTimer ??= + Timer.periodic(Duration(milliseconds: 1200), (_) async { + if (isNewTransactionExist()) { + onNewTransaction(); + } + + var syncHeight = getSyncingHeight(); + + if (syncHeight <= 0) { + syncHeight = getCurrentHeight(); + } + + if (_initialSyncHeight <= 0) { + _initialSyncHeight = syncHeight; + } + + final bchHeight = await getNodeHeightOrUpdate(syncHeight); + + if (_lastKnownBlockHeight == syncHeight) { + return; + } + + _lastKnownBlockHeight = syncHeight; + final track = bchHeight - _initialSyncHeight; + final diff = track - (bchHeight - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = bchHeight - syncHeight; + + if (syncHeight < 0 || left < 0) { + return; + } + + // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; + onNewBlock.call(syncHeight, left, ptc); + }); + } + + void stop() => _updateSyncInfoTimer?.cancel(); +} + +SyncListener setListeners(void Function(int, int, double) onNewBlock, + void Function() onNewTransaction) { + final listener = SyncListener(onNewBlock, onNewTransaction); + // setListenerNative(); + return listener; +} + +void onStartup() {} + +void _storeSync(Object _) => storeSync(); + +Future _setupNodeSync(Map args) async { + final address = args['address'] as String; + final login = (args['login'] ?? '') as String; + final password = (args['password'] ?? '') as String; + final useSSL = args['useSSL'] as bool; + final isLightWallet = args['isLightWallet'] as bool; + final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String; + + return setupNodeSync( + address: address, + login: login, + password: password, + useSSL: useSSL, + isLightWallet: isLightWallet, + socksProxyAddress: socksProxyAddress); +} + +bool _isConnected(Object _) => isConnectedSync(); + +int _getNodeHeight(Object _) => getNodeHeightSync(); + +void startRefresh() => startRefreshSync(); + +Future setupNode( + {required String address, + String? login, + String? password, + bool useSSL = false, + String? socksProxyAddress, + bool isLightWallet = false}) async => + _setupNodeSync({ + 'address': address, + 'login': login, + 'password': password, + 'useSSL': useSSL, + 'isLightWallet': isLightWallet, + 'socksProxyAddress': socksProxyAddress + }); + +Future store() async => _storeSync(0); + +Future isConnected() async => _isConnected(0); + +Future getNodeHeight() async => _getNodeHeight(0); + +void rescanBlockchainAsync() => wownero.Wallet_rescanBlockchainAsync(wptr!); + +String getSubaddressLabel(int accountIndex, int addressIndex) { + return wownero.Wallet_getSubaddressLabel(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); +} + +Future setTrustedDaemon(bool trusted) async => + wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted); + +Future trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!); + +String signMessage(String message, {String address = ""}) { + return wownero.Wallet_signMessage(wptr!, message: message, address: address); +} diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart new file mode 100644 index 000000000..53d62f1cf --- /dev/null +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -0,0 +1,359 @@ +import 'dart:ffi'; +import 'dart:isolate'; + +import 'package:cw_wownero/api/account_list.dart'; +import 'package:cw_wownero/api/exceptions/wallet_creation_exception.dart'; +import 'package:cw_wownero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_wownero/api/exceptions/wallet_restore_from_keys_exception.dart'; +import 'package:cw_wownero/api/exceptions/wallet_restore_from_seed_exception.dart'; +import 'package:cw_wownero/api/wallet.dart'; +import 'package:monero/wownero.dart' as wownero; + +wownero.WalletManager? _wmPtr; +final wownero.WalletManager wmPtr = Pointer.fromAddress((() { + try { + // Problems with the wallet? Crashes? Lags? this will print all calls to wow + // codebase, so it will be easier to debug what happens. At least easier + // than plugging gdb in. Especially on windows/android. + wownero.printStarts = false; + _wmPtr ??= wownero.WalletManagerFactory_getWalletManager(); + print("ptr: $_wmPtr"); + } catch (e) { + print(e); + } + return _wmPtr!.address; +})()); + +void createWalletSync( + {required String path, + required String password, + required String language, + int nettype = 0}) { + wptr = wownero.WalletManager_createWallet(wmPtr, + path: path, password: password, language: language, networkType: 0); + + final status = wownero.Wallet_status(wptr!); + if (status != 0) { + throw WalletCreationException(message: wownero.Wallet_errorString(wptr!)); + } + wownero.Wallet_store(wptr!, path: path); + openedWalletsByPath[path] = wptr!; + + // is the line below needed? + // setupNodeSync(address: "node.wowneroworld.com:18089"); +} + +bool isWalletExistSync({required String path}) { + return wownero.WalletManager_walletExists(wmPtr, path); +} + +void restoreWalletFromSeedSync( + {required String path, + required String password, + required String seed, + int nettype = 0, + int restoreHeight = 0}) { + if (seed.split(" ").length == 14) { + wptr = wownero.WOWNERO_deprecated_restore14WordSeed( + path: path, + password: password, + language: seed, // I KNOW - this is supposed to be called seed + networkType: 0, + ); + + setRefreshFromBlockHeight( + height: wownero.WOWNERO_deprecated_14WordSeedHeight(seed: seed), + ); + } else { + wptr = wownero.WalletManager_recoveryWallet( + wmPtr, + path: path, + password: password, + mnemonic: seed, + restoreHeight: restoreHeight, + seedOffset: '', + networkType: 0, + ); + } + + final status = wownero.Wallet_status(wptr!); + + if (status != 0) { + final error = wownero.Wallet_errorString(wptr!); + throw WalletRestoreFromSeedException(message: error); + } + + openedWalletsByPath[path] = wptr!; +} + +void restoreWalletFromKeysSync( + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) { + wptr = wownero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); + + final status = wownero.Wallet_status(wptr!); + if (status != 0) { + throw WalletRestoreFromKeysException( + message: wownero.Wallet_errorString(wptr!)); + } + + openedWalletsByPath[path] = wptr!; +} + +void restoreWalletFromSpendKeySync( + {required String path, + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) { + // wptr = wownero.WalletManager_createWalletFromKeys( + // wmPtr, + // path: path, + // password: password, + // restoreHeight: restoreHeight, + // addressString: '', + // spendKeyString: spendKey, + // viewKeyString: '', + // nettype: 0, + // ); + + wptr = wownero.WalletManager_createDeterministicWalletFromSpendKey( + wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight, + ); + + final status = wownero.Wallet_status(wptr!); + + if (status != 0) { + final err = wownero.Wallet_errorString(wptr!); + print("err: $err"); + throw WalletRestoreFromKeysException(message: err); + } + + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); + + storeSync(); + + openedWalletsByPath[path] = wptr!; +} + +String _lastOpenedWallet = ""; + +// void restoreWowneroWalletFromDevice( +// {required String path, +// required String password, +// required String deviceName, +// int nettype = 0, +// int restoreHeight = 0}) { +// +// final pathPointer = path.toNativeUtf8(); +// final passwordPointer = password.toNativeUtf8(); +// final deviceNamePointer = deviceName.toNativeUtf8(); +// final errorMessagePointer = ''.toNativeUtf8(); +// +// final isWalletRestored = restoreWalletFromDeviceNative( +// pathPointer, +// passwordPointer, +// deviceNamePointer, +// nettype, +// restoreHeight, +// errorMessagePointer) != 0; +// +// calloc.free(pathPointer); +// calloc.free(passwordPointer); +// +// storeSync(); +// +// if (!isWalletRestored) { +// throw WalletRestoreFromKeysException( +// message: convertUTF8ToString(pointer: errorMessagePointer)); +// } +// } + +Map openedWalletsByPath = {}; + +void loadWallet( + {required String path, required String password, int nettype = 0}) { + if (openedWalletsByPath[path] != null) { + wptr = openedWalletsByPath[path]!; + return; + } + try { + if (wptr == null || path != _lastOpenedWallet) { + if (wptr != null) { + final addr = wptr!.address; + Isolate.run(() { + wownero.Wallet_store(Pointer.fromAddress(addr)); + }); + } + wptr = wownero.WalletManager_openWallet(wmPtr, + path: path, password: password); + openedWalletsByPath[path] = wptr!; + _lastOpenedWallet = path; + } + } catch (e) { + print(e); + } + final status = wownero.Wallet_status(wptr!); + if (status != 0) { + final err = wownero.Wallet_errorString(wptr!); + print(err); + throw WalletOpeningException(message: err); + } +} + +void _createWallet(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final language = args['language'] as String; + + createWalletSync(path: path, password: password, language: language); +} + +void _restoreFromSeed(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final seed = args['seed'] as String; + final restoreHeight = args['restoreHeight'] as int; + + restoreWalletFromSeedSync( + path: path, password: password, seed: seed, restoreHeight: restoreHeight); +} + +void _restoreFromKeys(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final language = args['language'] as String; + final restoreHeight = args['restoreHeight'] as int; + final address = args['address'] as String; + final viewKey = args['viewKey'] as String; + final spendKey = args['spendKey'] as String; + + restoreWalletFromKeysSync( + path: path, + password: password, + language: language, + restoreHeight: restoreHeight, + address: address, + viewKey: viewKey, + spendKey: spendKey); +} + +void _restoreFromSpendKey(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final seed = args['seed'] as String; + final language = args['language'] as String; + final spendKey = args['spendKey'] as String; + final restoreHeight = args['restoreHeight'] as int; + + restoreWalletFromSpendKeySync( + path: path, + password: password, + seed: seed, + language: language, + restoreHeight: restoreHeight, + spendKey: spendKey); +} + +Future _openWallet(Map args) async => loadWallet( + path: args['path'] as String, password: args['password'] as String); + +Future _isWalletExist(String path) async => isWalletExistSync(path: path); + +void openWallet( + {required String path, + required String password, + int nettype = 0}) async => + loadWallet(path: path, password: password, nettype: nettype); + +Future openWalletAsync(Map args) async => + _openWallet(args); + +Future createWallet( + {required String path, + required String password, + required String language, + int nettype = 0}) async => + _createWallet({ + 'path': path, + 'password': password, + 'language': language, + 'nettype': nettype + }); + +Future restoreFromSeed( + {required String path, + required String password, + required String seed, + int nettype = 0, + int restoreHeight = 0}) async => + _restoreFromSeed({ + 'path': path, + 'password': password, + 'seed': seed, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + +Future restoreFromKeys( + {required String path, + required String password, + required String language, + required String address, + required String viewKey, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) async => + _restoreFromKeys({ + 'path': path, + 'password': password, + 'language': language, + 'address': address, + 'viewKey': viewKey, + 'spendKey': spendKey, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + +Future restoreFromSpendKey( + {required String path, + required String password, + required String seed, + required String language, + required String spendKey, + int nettype = 0, + int restoreHeight = 0}) async => + _restoreFromSpendKey({ + 'path': path, + 'password': password, + 'seed': seed, + 'language': language, + 'spendKey': spendKey, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + +Future isWalletExist({required String path}) => _isWalletExist(path); diff --git a/cw_wownero/lib/api/wownero_output.dart b/cw_wownero/lib/api/wownero_output.dart new file mode 100644 index 000000000..a4756c4c5 --- /dev/null +++ b/cw_wownero/lib/api/wownero_output.dart @@ -0,0 +1,6 @@ +class WowneroOutput { + WowneroOutput({required this.address, required this.amount}); + + final String address; + final String amount; +} \ No newline at end of file diff --git a/cw_wownero/lib/cw_wownero.dart b/cw_wownero/lib/cw_wownero.dart new file mode 100644 index 000000000..33a55e305 --- /dev/null +++ b/cw_wownero/lib/cw_wownero.dart @@ -0,0 +1,8 @@ + +import 'cw_wownero_platform_interface.dart'; + +class CwWownero { + Future getPlatformVersion() { + return CwWowneroPlatform.instance.getPlatformVersion(); + } +} diff --git a/cw_wownero/lib/cw_wownero_method_channel.dart b/cw_wownero/lib/cw_wownero_method_channel.dart new file mode 100644 index 000000000..d797f5f81 --- /dev/null +++ b/cw_wownero/lib/cw_wownero_method_channel.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'cw_wownero_platform_interface.dart'; + +/// An implementation of [CwWowneroPlatform] that uses method channels. +class MethodChannelCwWownero extends CwWowneroPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('cw_wownero'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_wownero/lib/cw_wownero_platform_interface.dart b/cw_wownero/lib/cw_wownero_platform_interface.dart new file mode 100644 index 000000000..78b21592c --- /dev/null +++ b/cw_wownero/lib/cw_wownero_platform_interface.dart @@ -0,0 +1,29 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'cw_wownero_method_channel.dart'; + +abstract class CwWowneroPlatform extends PlatformInterface { + /// Constructs a CwWowneroPlatform. + CwWowneroPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CwWowneroPlatform _instance = MethodChannelCwWownero(); + + /// The default instance of [CwWowneroPlatform] to use. + /// + /// Defaults to [MethodChannelCwWownero]. + static CwWowneroPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [CwWowneroPlatform] when + /// they register themselves. + static set instance(CwWowneroPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart b/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart new file mode 100644 index 000000000..3b24be87b --- /dev/null +++ b/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart @@ -0,0 +1,8 @@ +class WowneroTransactionCreationException implements Exception { + WowneroTransactionCreationException(this.message); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart b/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..e42344abd --- /dev/null +++ b/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart @@ -0,0 +1,8 @@ +class WowneroTransactionNoInputsException implements Exception { + WowneroTransactionNoInputsException(this.inputsSize); + + int inputsSize; + + @override + String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control'; +} diff --git a/cw_wownero/lib/mnemonics/chinese_simplified.dart b/cw_wownero/lib/mnemonics/chinese_simplified.dart new file mode 100644 index 000000000..da3225041 --- /dev/null +++ b/cw_wownero/lib/mnemonics/chinese_simplified.dart @@ -0,0 +1,1630 @@ +class ChineseSimplifiedMnemonics { + static const words = [ + "的", + "一", + "是", + "在", + "不", + "了", + "有", + "和", + "人", + "这", + "中", + "大", + "为", + "上", + "个", + "国", + "我", + "以", + "要", + "他", + "时", + "来", + "用", + "们", + "生", + "到", + "作", + "地", + "于", + "出", + "就", + "分", + "对", + "成", + "会", + "可", + "主", + "发", + "年", + "动", + "同", + "工", + "也", + "能", + "下", + "过", + "子", + "说", + "产", + "种", + "面", + "而", + "方", + "后", + "多", + "定", + "行", + "学", + "法", + "所", + "民", + "得", + "经", + "十", + "三", + "之", + "进", + "着", + "等", + "部", + "度", + "家", + "电", + "力", + "里", + "如", + "水", + "化", + "高", + "自", + "二", + "理", + "起", + "小", + "物", + "现", + "实", + "加", + "量", + "都", + "两", + "体", + "制", + "机", + "当", + "使", + "点", + "从", + "业", + "本", + "去", + "把", + "性", + "好", + "应", + "开", + "它", + "合", + "还", + "因", + "由", + "其", + "些", + "然", + "前", + "外", + "天", + "政", + "四", + "日", + "那", + "社", + "义", + "事", + "平", + "形", + "相", + "全", + "表", + "间", + "样", + "与", + "关", + "各", + "重", + "新", + "线", + "内", + "数", + "正", + "心", + "反", + "你", + "明", + "看", + "原", + "又", + "么", + "利", + "比", + "或", + "但", + "质", + "气", + "第", + "向", + "道", + "命", + "此", + "变", + "条", + "只", + "没", + "结", + "解", + "问", + "意", + "建", + "月", + "公", + "无", + "系", + "军", + "很", + "情", + "者", + "最", + "立", + "代", + "想", + "已", + "通", + "并", + "提", + "直", + "题", + "党", + "程", + "展", + "五", + "果", + "料", + "象", + "员", + "革", + "位", + "入", + "常", + "文", + "总", + "次", + "品", + "式", + "活", + "设", + "及", + "管", + "特", + "件", + "长", + "求", + "老", + "头", + "基", + "资", + "边", + "流", + "路", + "级", + "少", + "图", + "山", + "统", + "接", + "知", + "较", + "将", + "组", + "见", + "计", + "别", + "她", + "手", + "角", + "期", + "根", + "论", + "运", + "农", + "指", + "几", + "九", + "区", + "强", + "放", + "决", + "西", + "被", + "干", + "做", + "必", + "战", + "先", + "回", + "则", + "任", + "取", + "据", + "处", + "队", + "南", + "给", + "色", + "光", + "门", + "即", + "保", + "治", + "北", + "造", + "百", + "规", + "热", + "领", + "七", + "海", + "口", + "东", + "导", + "器", + "压", + "志", + "世", + "金", + "增", + "争", + "济", + "阶", + "油", + "思", + "术", + "极", + "交", + "受", + "联", + "什", + "认", + "六", + "共", + "权", + "收", + "证", + "改", + "清", + "美", + "再", + "采", + "转", + "更", + "单", + "风", + "切", + "打", + "白", + "教", + "速", + "花", + "带", + "安", + "场", + "身", + "车", + "例", + "真", + "务", + "具", + "万", + "每", + "目", + "至", + "达", + "走", + "积", + "示", + "议", + "声", + "报", + "斗", + "完", + "类", + "八", + "离", + "华", + "名", + "确", + "才", + "科", + "张", + "信", + "马", + "节", + "话", + "米", + "整", + "空", + "元", + "况", + "今", + "集", + "温", + "传", + "土", + "许", + "步", + "群", + "广", + "石", + "记", + "需", + "段", + "研", + "界", + "拉", + "林", + "律", + "叫", + "且", + "究", + "观", + "越", + "织", + "装", + "影", + "算", + "低", + "持", + "音", + "众", + "书", + "布", + "复", + "容", + "儿", + "须", + "际", + "商", + "非", + "验", + "连", + "断", + "深", + "难", + "近", + "矿", + "千", + "周", + "委", + "素", + "技", + "备", + "半", + "办", + "青", + "省", + "列", + "习", + "响", + "约", + "支", + "般", + "史", + "感", + "劳", + "便", + "团", + "往", + "酸", + "历", + "市", + "克", + "何", + "除", + "消", + "构", + "府", + "称", + "太", + "准", + "精", + "值", + "号", + "率", + "族", + "维", + "划", + "选", + "标", + "写", + "存", + "候", + "毛", + "亲", + "快", + "效", + "斯", + "院", + "查", + "江", + "型", + "眼", + "王", + "按", + "格", + "养", + "易", + "置", + "派", + "层", + "片", + "始", + "却", + "专", + "状", + "育", + "厂", + "京", + "识", + "适", + "属", + "圆", + "包", + "火", + "住", + "调", + "满", + "县", + "局", + "照", + "参", + "红", + "细", + "引", + "听", + "该", + "铁", + "价", + "严", + "首", + "底", + "液", + "官", + "德", + "随", + "病", + "苏", + "失", + "尔", + "死", + "讲", + "配", + "女", + "黄", + "推", + "显", + "谈", + "罪", + "神", + "艺", + "呢", + "席", + "含", + "企", + "望", + "密", + "批", + "营", + "项", + "防", + "举", + "球", + "英", + "氧", + "势", + "告", + "李", + "台", + "落", + "木", + "帮", + "轮", + "破", + "亚", + "师", + "围", + "注", + "远", + "字", + "材", + "排", + "供", + "河", + "态", + "封", + "另", + "施", + "减", + "树", + "溶", + "怎", + "止", + "案", + "言", + "士", + "均", + "武", + "固", + "叶", + "鱼", + "波", + "视", + "仅", + "费", + "紧", + "爱", + "左", + "章", + "早", + "朝", + "害", + "续", + "轻", + "服", + "试", + "食", + "充", + "兵", + "源", + "判", + "护", + "司", + "足", + "某", + "练", + "差", + "致", + "板", + "田", + "降", + "黑", + "犯", + "负", + "击", + "范", + "继", + "兴", + "似", + "余", + "坚", + "曲", + "输", + "修", + "故", + "城", + "夫", + "够", + "送", + "笔", + "船", + "占", + "右", + "财", + "吃", + "富", + "春", + "职", + "觉", + "汉", + "画", + "功", + "巴", + "跟", + "虽", + "杂", + "飞", + "检", + "吸", + "助", + "升", + "阳", + "互", + "初", + "创", + "抗", + "考", + "投", + "坏", + "策", + "古", + "径", + "换", + "未", + "跑", + "留", + "钢", + "曾", + "端", + "责", + "站", + "简", + "述", + "钱", + "副", + "尽", + "帝", + "射", + "草", + "冲", + "承", + "独", + "令", + "限", + "阿", + "宣", + "环", + "双", + "请", + "超", + "微", + "让", + "控", + "州", + "良", + "轴", + "找", + "否", + "纪", + "益", + "依", + "优", + "顶", + "础", + "载", + "倒", + "房", + "突", + "坐", + "粉", + "敌", + "略", + "客", + "袁", + "冷", + "胜", + "绝", + "析", + "块", + "剂", + "测", + "丝", + "协", + "诉", + "念", + "陈", + "仍", + "罗", + "盐", + "友", + "洋", + "错", + "苦", + "夜", + "刑", + "移", + "频", + "逐", + "靠", + "混", + "母", + "短", + "皮", + "终", + "聚", + "汽", + "村", + "云", + "哪", + "既", + "距", + "卫", + "停", + "烈", + "央", + "察", + "烧", + "迅", + "境", + "若", + "印", + "洲", + "刻", + "括", + "激", + "孔", + "搞", + "甚", + "室", + "待", + "核", + "校", + "散", + "侵", + "吧", + "甲", + "游", + "久", + "菜", + "味", + "旧", + "模", + "湖", + "货", + "损", + "预", + "阻", + "毫", + "普", + "稳", + "乙", + "妈", + "植", + "息", + "扩", + "银", + "语", + "挥", + "酒", + "守", + "拿", + "序", + "纸", + "医", + "缺", + "雨", + "吗", + "针", + "刘", + "啊", + "急", + "唱", + "误", + "训", + "愿", + "审", + "附", + "获", + "茶", + "鲜", + "粮", + "斤", + "孩", + "脱", + "硫", + "肥", + "善", + "龙", + "演", + "父", + "渐", + "血", + "欢", + "械", + "掌", + "歌", + "沙", + "刚", + "攻", + "谓", + "盾", + "讨", + "晚", + "粒", + "乱", + "燃", + "矛", + "乎", + "杀", + "药", + "宁", + "鲁", + "贵", + "钟", + "煤", + "读", + "班", + "伯", + "香", + "介", + "迫", + "句", + "丰", + "培", + "握", + "兰", + "担", + "弦", + "蛋", + "沉", + "假", + "穿", + "执", + "答", + "乐", + "谁", + "顺", + "烟", + "缩", + "征", + "脸", + "喜", + "松", + "脚", + "困", + "异", + "免", + "背", + "星", + "福", + "买", + "染", + "井", + "概", + "慢", + "怕", + "磁", + "倍", + "祖", + "皇", + "促", + "静", + "补", + "评", + "翻", + "肉", + "践", + "尼", + "衣", + "宽", + "扬", + "棉", + "希", + "伤", + "操", + "垂", + "秋", + "宜", + "氢", + "套", + "督", + "振", + "架", + "亮", + "末", + "宪", + "庆", + "编", + "牛", + "触", + "映", + "雷", + "销", + "诗", + "座", + "居", + "抓", + "裂", + "胞", + "呼", + "娘", + "景", + "威", + "绿", + "晶", + "厚", + "盟", + "衡", + "鸡", + "孙", + "延", + "危", + "胶", + "屋", + "乡", + "临", + "陆", + "顾", + "掉", + "呀", + "灯", + "岁", + "措", + "束", + "耐", + "剧", + "玉", + "赵", + "跳", + "哥", + "季", + "课", + "凯", + "胡", + "额", + "款", + "绍", + "卷", + "齐", + "伟", + "蒸", + "殖", + "永", + "宗", + "苗", + "川", + "炉", + "岩", + "弱", + "零", + "杨", + "奏", + "沿", + "露", + "杆", + "探", + "滑", + "镇", + "饭", + "浓", + "航", + "怀", + "赶", + "库", + "夺", + "伊", + "灵", + "税", + "途", + "灭", + "赛", + "归", + "召", + "鼓", + "播", + "盘", + "裁", + "险", + "康", + "唯", + "录", + "菌", + "纯", + "借", + "糖", + "盖", + "横", + "符", + "私", + "努", + "堂", + "域", + "枪", + "润", + "幅", + "哈", + "竟", + "熟", + "虫", + "泽", + "脑", + "壤", + "碳", + "欧", + "遍", + "侧", + "寨", + "敢", + "彻", + "虑", + "斜", + "薄", + "庭", + "纳", + "弹", + "饲", + "伸", + "折", + "麦", + "湿", + "暗", + "荷", + "瓦", + "塞", + "床", + "筑", + "恶", + "户", + "访", + "塔", + "奇", + "透", + "梁", + "刀", + "旋", + "迹", + "卡", + "氯", + "遇", + "份", + "毒", + "泥", + "退", + "洗", + "摆", + "灰", + "彩", + "卖", + "耗", + "夏", + "择", + "忙", + "铜", + "献", + "硬", + "予", + "繁", + "圈", + "雪", + "函", + "亦", + "抽", + "篇", + "阵", + "阴", + "丁", + "尺", + "追", + "堆", + "雄", + "迎", + "泛", + "爸", + "楼", + "避", + "谋", + "吨", + "野", + "猪", + "旗", + "累", + "偏", + "典", + "馆", + "索", + "秦", + "脂", + "潮", + "爷", + "豆", + "忽", + "托", + "惊", + "塑", + "遗", + "愈", + "朱", + "替", + "纤", + "粗", + "倾", + "尚", + "痛", + "楚", + "谢", + "奋", + "购", + "磨", + "君", + "池", + "旁", + "碎", + "骨", + "监", + "捕", + "弟", + "暴", + "割", + "贯", + "殊", + "释", + "词", + "亡", + "壁", + "顿", + "宝", + "午", + "尘", + "闻", + "揭", + "炮", + "残", + "冬", + "桥", + "妇", + "警", + "综", + "招", + "吴", + "付", + "浮", + "遭", + "徐", + "您", + "摇", + "谷", + "赞", + "箱", + "隔", + "订", + "男", + "吹", + "园", + "纷", + "唐", + "败", + "宋", + "玻", + "巨", + "耕", + "坦", + "荣", + "闭", + "湾", + "键", + "凡", + "驻", + "锅", + "救", + "恩", + "剥", + "凝", + "碱", + "齿", + "截", + "炼", + "麻", + "纺", + "禁", + "废", + "盛", + "版", + "缓", + "净", + "睛", + "昌", + "婚", + "涉", + "筒", + "嘴", + "插", + "岸", + "朗", + "庄", + "街", + "藏", + "姑", + "贸", + "腐", + "奴", + "啦", + "惯", + "乘", + "伙", + "恢", + "匀", + "纱", + "扎", + "辩", + "耳", + "彪", + "臣", + "亿", + "璃", + "抵", + "脉", + "秀", + "萨", + "俄", + "网", + "舞", + "店", + "喷", + "纵", + "寸", + "汗", + "挂", + "洪", + "贺", + "闪", + "柬", + "爆", + "烯", + "津", + "稻", + "墙", + "软", + "勇", + "像", + "滚", + "厘", + "蒙", + "芳", + "肯", + "坡", + "柱", + "荡", + "腿", + "仪", + "旅", + "尾", + "轧", + "冰", + "贡", + "登", + "黎", + "削", + "钻", + "勒", + "逃", + "障", + "氨", + "郭", + "峰", + "币", + "港", + "伏", + "轨", + "亩", + "毕", + "擦", + "莫", + "刺", + "浪", + "秘", + "援", + "株", + "健", + "售", + "股", + "岛", + "甘", + "泡", + "睡", + "童", + "铸", + "汤", + "阀", + "休", + "汇", + "舍", + "牧", + "绕", + "炸", + "哲", + "磷", + "绩", + "朋", + "淡", + "尖", + "启", + "陷", + "柴", + "呈", + "徒", + "颜", + "泪", + "稍", + "忘", + "泵", + "蓝", + "拖", + "洞", + "授", + "镜", + "辛", + "壮", + "锋", + "贫", + "虚", + "弯", + "摩", + "泰", + "幼", + "廷", + "尊", + "窗", + "纲", + "弄", + "隶", + "疑", + "氏", + "宫", + "姐", + "震", + "瑞", + "怪", + "尤", + "琴", + "循", + "描", + "膜", + "违", + "夹", + "腰", + "缘", + "珠", + "穷", + "森", + "枝", + "竹", + "沟", + "催", + "绳", + "忆", + "邦", + "剩", + "幸", + "浆", + "栏", + "拥", + "牙", + "贮", + "礼", + "滤", + "钠", + "纹", + "罢", + "拍", + "咱", + "喊", + "袖", + "埃", + "勤", + "罚", + "焦", + "潜", + "伍", + "墨", + "欲", + "缝", + "姓", + "刊", + "饱", + "仿", + "奖", + "铝", + "鬼", + "丽", + "跨", + "默", + "挖", + "链", + "扫", + "喝", + "袋", + "炭", + "污", + "幕", + "诸", + "弧", + "励", + "梅", + "奶", + "洁", + "灾", + "舟", + "鉴", + "苯", + "讼", + "抱", + "毁", + "懂", + "寒", + "智", + "埔", + "寄", + "届", + "跃", + "渡", + "挑", + "丹", + "艰", + "贝", + "碰", + "拔", + "爹", + "戴", + "码", + "梦", + "芽", + "熔", + "赤", + "渔", + "哭", + "敬", + "颗", + "奔", + "铅", + "仲", + "虎", + "稀", + "妹", + "乏", + "珍", + "申", + "桌", + "遵", + "允", + "隆", + "螺", + "仓", + "魏", + "锐", + "晓", + "氮", + "兼", + "隐", + "碍", + "赫", + "拨", + "忠", + "肃", + "缸", + "牵", + "抢", + "博", + "巧", + "壳", + "兄", + "杜", + "讯", + "诚", + "碧", + "祥", + "柯", + "页", + "巡", + "矩", + "悲", + "灌", + "龄", + "伦", + "票", + "寻", + "桂", + "铺", + "圣", + "恐", + "恰", + "郑", + "趣", + "抬", + "荒", + "腾", + "贴", + "柔", + "滴", + "猛", + "阔", + "辆", + "妻", + "填", + "撤", + "储", + "签", + "闹", + "扰", + "紫", + "砂", + "递", + "戏", + "吊", + "陶", + "伐", + "喂", + "疗", + "瓶", + "婆", + "抚", + "臂", + "摸", + "忍", + "虾", + "蜡", + "邻", + "胸", + "巩", + "挤", + "偶", + "弃", + "槽", + "劲", + "乳", + "邓", + "吉", + "仁", + "烂", + "砖", + "租", + "乌", + "舰", + "伴", + "瓜", + "浅", + "丙", + "暂", + "燥", + "橡", + "柳", + "迷", + "暖", + "牌", + "秧", + "胆", + "详", + "簧", + "踏", + "瓷", + "谱", + "呆", + "宾", + "糊", + "洛", + "辉", + "愤", + "竞", + "隙", + "怒", + "粘", + "乃", + "绪", + "肩", + "籍", + "敏", + "涂", + "熙", + "皆", + "侦", + "悬", + "掘", + "享", + "纠", + "醒", + "狂", + "锁", + "淀", + "恨", + "牲", + "霸", + "爬", + "赏", + "逆", + "玩", + "陵", + "祝", + "秒", + "浙", + "貌" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/dutch.dart b/cw_wownero/lib/mnemonics/dutch.dart new file mode 100644 index 000000000..3a1d00cfc --- /dev/null +++ b/cw_wownero/lib/mnemonics/dutch.dart @@ -0,0 +1,1630 @@ +class DutchMnemonics { + static const words = [ + "aalglad", + "aalscholver", + "aambeeld", + "aangeef", + "aanlandig", + "aanvaard", + "aanwakker", + "aapmens", + "aarten", + "abdicatie", + "abnormaal", + "abrikoos", + "accu", + "acuut", + "adjudant", + "admiraal", + "advies", + "afbidding", + "afdracht", + "affaire", + "affiche", + "afgang", + "afkick", + "afknap", + "aflees", + "afmijner", + "afname", + "afpreekt", + "afrader", + "afspeel", + "aftocht", + "aftrek", + "afzijdig", + "ahornboom", + "aktetas", + "akzo", + "alchemist", + "alcohol", + "aldaar", + "alexander", + "alfabet", + "alfredo", + "alice", + "alikruik", + "allrisk", + "altsax", + "alufolie", + "alziend", + "amai", + "ambacht", + "ambieer", + "amina", + "amnestie", + "amok", + "ampul", + "amuzikaal", + "angela", + "aniek", + "antje", + "antwerpen", + "anya", + "aorta", + "apache", + "apekool", + "appelaar", + "arganolie", + "argeloos", + "armoede", + "arrenslee", + "artritis", + "arubaan", + "asbak", + "ascii", + "asgrauw", + "asjes", + "asml", + "aspunt", + "asurn", + "asveld", + "aterling", + "atomair", + "atrium", + "atsma", + "atypisch", + "auping", + "aura", + "avifauna", + "axiaal", + "azoriaan", + "azteek", + "azuur", + "bachelor", + "badderen", + "badhotel", + "badmantel", + "badsteden", + "balie", + "ballans", + "balvers", + "bamibal", + "banneling", + "barracuda", + "basaal", + "batelaan", + "batje", + "beambte", + "bedlamp", + "bedwelmd", + "befaamd", + "begierd", + "begraaf", + "behield", + "beijaard", + "bejaagd", + "bekaaid", + "beks", + "bektas", + "belaad", + "belboei", + "belderbos", + "beloerd", + "beluchten", + "bemiddeld", + "benadeeld", + "benijd", + "berechten", + "beroemd", + "besef", + "besseling", + "best", + "betichten", + "bevind", + "bevochten", + "bevraagd", + "bewust", + "bidplaats", + "biefstuk", + "biemans", + "biezen", + "bijbaan", + "bijeenkom", + "bijfiguur", + "bijkaart", + "bijlage", + "bijpaard", + "bijtgaar", + "bijweg", + "bimmel", + "binck", + "bint", + "biobak", + "biotisch", + "biseks", + "bistro", + "bitter", + "bitumen", + "bizar", + "blad", + "bleken", + "blender", + "bleu", + "blief", + "blijven", + "blozen", + "bock", + "boef", + "boei", + "boks", + "bolder", + "bolus", + "bolvormig", + "bomaanval", + "bombarde", + "bomma", + "bomtapijt", + "bookmaker", + "boos", + "borg", + "bosbes", + "boshuizen", + "bosloop", + "botanicus", + "bougie", + "bovag", + "boxspring", + "braad", + "brasem", + "brevet", + "brigade", + "brinckman", + "bruid", + "budget", + "buffel", + "buks", + "bulgaar", + "buma", + "butaan", + "butler", + "buuf", + "cactus", + "cafeetje", + "camcorder", + "cannabis", + "canyon", + "capoeira", + "capsule", + "carkit", + "casanova", + "catalaan", + "ceintuur", + "celdeling", + "celplasma", + "cement", + "censeren", + "ceramisch", + "cerberus", + "cerebraal", + "cesium", + "cirkel", + "citeer", + "civiel", + "claxon", + "clenbuterol", + "clicheren", + "clijsen", + "coalitie", + "coassistentschap", + "coaxiaal", + "codetaal", + "cofinanciering", + "cognac", + "coltrui", + "comfort", + "commandant", + "condensaat", + "confectie", + "conifeer", + "convector", + "copier", + "corfu", + "correct", + "coup", + "couvert", + "creatie", + "credit", + "crematie", + "cricket", + "croupier", + "cruciaal", + "cruijff", + "cuisine", + "culemborg", + "culinair", + "curve", + "cyrano", + "dactylus", + "dading", + "dagblind", + "dagje", + "daglicht", + "dagprijs", + "dagranden", + "dakdekker", + "dakpark", + "dakterras", + "dalgrond", + "dambord", + "damkat", + "damlengte", + "damman", + "danenberg", + "debbie", + "decibel", + "defect", + "deformeer", + "degelijk", + "degradant", + "dejonghe", + "dekken", + "deppen", + "derek", + "derf", + "derhalve", + "detineren", + "devalueer", + "diaken", + "dicht", + "dictaat", + "dief", + "digitaal", + "dijbreuk", + "dijkmans", + "dimbaar", + "dinsdag", + "diode", + "dirigeer", + "disbalans", + "dobermann", + "doenbaar", + "doerak", + "dogma", + "dokhaven", + "dokwerker", + "doling", + "dolphijn", + "dolven", + "dombo", + "dooraderd", + "dopeling", + "doping", + "draderig", + "drama", + "drenkbak", + "dreumes", + "drol", + "drug", + "duaal", + "dublin", + "duplicaat", + "durven", + "dusdanig", + "dutchbat", + "dutje", + "dutten", + "duur", + "duwwerk", + "dwaal", + "dweil", + "dwing", + "dyslexie", + "ecostroom", + "ecotaks", + "educatie", + "eeckhout", + "eede", + "eemland", + "eencellig", + "eeneiig", + "eenruiter", + "eenwinter", + "eerenberg", + "eerrover", + "eersel", + "eetmaal", + "efteling", + "egaal", + "egtberts", + "eickhoff", + "eidooier", + "eiland", + "eind", + "eisden", + "ekster", + "elburg", + "elevatie", + "elfkoppig", + "elfrink", + "elftal", + "elimineer", + "elleboog", + "elma", + "elodie", + "elsa", + "embleem", + "embolie", + "emoe", + "emonds", + "emplooi", + "enduro", + "enfin", + "engageer", + "entourage", + "entstof", + "epileer", + "episch", + "eppo", + "erasmus", + "erboven", + "erebaan", + "erelijst", + "ereronden", + "ereteken", + "erfhuis", + "erfwet", + "erger", + "erica", + "ermitage", + "erna", + "ernie", + "erts", + "ertussen", + "eruitzien", + "ervaar", + "erven", + "erwt", + "esbeek", + "escort", + "esdoorn", + "essing", + "etage", + "eter", + "ethanol", + "ethicus", + "etholoog", + "eufonisch", + "eurocent", + "evacuatie", + "exact", + "examen", + "executant", + "exen", + "exit", + "exogeen", + "exotherm", + "expeditie", + "expletief", + "expres", + "extase", + "extinctie", + "faal", + "faam", + "fabel", + "facultair", + "fakir", + "fakkel", + "faliekant", + "fallisch", + "famke", + "fanclub", + "fase", + "fatsoen", + "fauna", + "federaal", + "feedback", + "feest", + "feilbaar", + "feitelijk", + "felblauw", + "figurante", + "fiod", + "fitheid", + "fixeer", + "flap", + "fleece", + "fleur", + "flexibel", + "flits", + "flos", + "flow", + "fluweel", + "foezelen", + "fokkelman", + "fokpaard", + "fokvee", + "folder", + "follikel", + "folmer", + "folteraar", + "fooi", + "foolen", + "forfait", + "forint", + "formule", + "fornuis", + "fosfaat", + "foxtrot", + "foyer", + "fragiel", + "frater", + "freak", + "freddie", + "fregat", + "freon", + "frijnen", + "fructose", + "frunniken", + "fuiven", + "funshop", + "furieus", + "fysica", + "gadget", + "galder", + "galei", + "galg", + "galvlieg", + "galzuur", + "ganesh", + "gaswet", + "gaza", + "gazelle", + "geaaid", + "gebiecht", + "gebufferd", + "gedijd", + "geef", + "geflanst", + "gefreesd", + "gegaan", + "gegijzeld", + "gegniffel", + "gegraaid", + "gehikt", + "gehobbeld", + "gehucht", + "geiser", + "geiten", + "gekaakt", + "gekheid", + "gekijf", + "gekmakend", + "gekocht", + "gekskap", + "gekte", + "gelubberd", + "gemiddeld", + "geordend", + "gepoederd", + "gepuft", + "gerda", + "gerijpt", + "geseald", + "geshockt", + "gesierd", + "geslaagd", + "gesnaaid", + "getracht", + "getwijfel", + "geuit", + "gevecht", + "gevlagd", + "gewicht", + "gezaagd", + "gezocht", + "ghanees", + "giebelen", + "giechel", + "giepmans", + "gips", + "giraal", + "gistachtig", + "gitaar", + "glaasje", + "gletsjer", + "gleuf", + "glibberen", + "glijbaan", + "gloren", + "gluipen", + "gluren", + "gluur", + "gnoe", + "goddelijk", + "godgans", + "godschalk", + "godzalig", + "goeierd", + "gogme", + "goklustig", + "gokwereld", + "gonggrijp", + "gonje", + "goor", + "grabbel", + "graf", + "graveer", + "grif", + "grolleman", + "grom", + "groosman", + "grubben", + "gruijs", + "grut", + "guacamole", + "guido", + "guppy", + "haazen", + "hachelijk", + "haex", + "haiku", + "hakhout", + "hakken", + "hanegem", + "hans", + "hanteer", + "harrie", + "hazebroek", + "hedonist", + "heil", + "heineken", + "hekhuis", + "hekman", + "helbig", + "helga", + "helwegen", + "hengelaar", + "herkansen", + "hermafrodiet", + "hertaald", + "hiaat", + "hikspoors", + "hitachi", + "hitparade", + "hobo", + "hoeve", + "holocaust", + "hond", + "honnepon", + "hoogacht", + "hotelbed", + "hufter", + "hugo", + "huilbier", + "hulk", + "humus", + "huwbaar", + "huwelijk", + "hype", + "iconisch", + "idema", + "ideogram", + "idolaat", + "ietje", + "ijker", + "ijkheid", + "ijklijn", + "ijkmaat", + "ijkwezen", + "ijmuiden", + "ijsbox", + "ijsdag", + "ijselijk", + "ijskoud", + "ilse", + "immuun", + "impliceer", + "impuls", + "inbijten", + "inbuigen", + "indijken", + "induceer", + "indy", + "infecteer", + "inhaak", + "inkijk", + "inluiden", + "inmijnen", + "inoefenen", + "inpolder", + "inrijden", + "inslaan", + "invitatie", + "inwaaien", + "ionisch", + "isaac", + "isolatie", + "isotherm", + "isra", + "italiaan", + "ivoor", + "jacobs", + "jakob", + "jammen", + "jampot", + "jarig", + "jehova", + "jenever", + "jezus", + "joana", + "jobdienst", + "josua", + "joule", + "juich", + "jurk", + "juut", + "kaas", + "kabelaar", + "kabinet", + "kagenaar", + "kajuit", + "kalebas", + "kalm", + "kanjer", + "kapucijn", + "karregat", + "kart", + "katvanger", + "katwijk", + "kegelaar", + "keiachtig", + "keizer", + "kenletter", + "kerdijk", + "keus", + "kevlar", + "kezen", + "kickback", + "kieviet", + "kijken", + "kikvors", + "kilheid", + "kilobit", + "kilsdonk", + "kipschnitzel", + "kissebis", + "klad", + "klagelijk", + "klak", + "klapbaar", + "klaver", + "klene", + "klets", + "klijnhout", + "klit", + "klok", + "klonen", + "klotefilm", + "kluif", + "klumper", + "klus", + "knabbel", + "knagen", + "knaven", + "kneedbaar", + "knmi", + "knul", + "knus", + "kokhals", + "komiek", + "komkommer", + "kompaan", + "komrij", + "komvormig", + "koning", + "kopbal", + "kopklep", + "kopnagel", + "koppejan", + "koptekst", + "kopwand", + "koraal", + "kosmisch", + "kostbaar", + "kram", + "kraneveld", + "kras", + "kreling", + "krengen", + "kribbe", + "krik", + "kruid", + "krulbol", + "kuijper", + "kuipbank", + "kuit", + "kuiven", + "kutsmoes", + "kuub", + "kwak", + "kwatong", + "kwetsbaar", + "kwezelaar", + "kwijnen", + "kwik", + "kwinkslag", + "kwitantie", + "lading", + "lakbeits", + "lakken", + "laklaag", + "lakmoes", + "lakwijk", + "lamheid", + "lamp", + "lamsbout", + "lapmiddel", + "larve", + "laser", + "latijn", + "latuw", + "lawaai", + "laxeerpil", + "lebberen", + "ledeboer", + "leefbaar", + "leeman", + "lefdoekje", + "lefhebber", + "legboor", + "legsel", + "leguaan", + "leiplaat", + "lekdicht", + "lekrijden", + "leksteen", + "lenen", + "leraar", + "lesbienne", + "leugenaar", + "leut", + "lexicaal", + "lezing", + "lieten", + "liggeld", + "lijdzaam", + "lijk", + "lijmstang", + "lijnschip", + "likdoorn", + "likken", + "liksteen", + "limburg", + "link", + "linoleum", + "lipbloem", + "lipman", + "lispelen", + "lissabon", + "litanie", + "liturgie", + "lochem", + "loempia", + "loesje", + "logheid", + "lonen", + "lonneke", + "loom", + "loos", + "losbaar", + "loslaten", + "losplaats", + "loting", + "lotnummer", + "lots", + "louie", + "lourdes", + "louter", + "lowbudget", + "luijten", + "luikenaar", + "luilak", + "luipaard", + "luizenbos", + "lulkoek", + "lumen", + "lunzen", + "lurven", + "lutjeboer", + "luttel", + "lutz", + "luuk", + "luwte", + "luyendijk", + "lyceum", + "lynx", + "maakbaar", + "magdalena", + "malheid", + "manchet", + "manfred", + "manhaftig", + "mank", + "mantel", + "marion", + "marxist", + "masmeijer", + "massaal", + "matsen", + "matverf", + "matze", + "maude", + "mayonaise", + "mechanica", + "meifeest", + "melodie", + "meppelink", + "midvoor", + "midweeks", + "midzomer", + "miezel", + "mijnraad", + "minus", + "mirck", + "mirte", + "mispakken", + "misraden", + "miswassen", + "mitella", + "moker", + "molecule", + "mombakkes", + "moonen", + "mopperaar", + "moraal", + "morgana", + "mormel", + "mosselaar", + "motregen", + "mouw", + "mufheid", + "mutueel", + "muzelman", + "naaidoos", + "naald", + "nadeel", + "nadruk", + "nagy", + "nahon", + "naima", + "nairobi", + "napalm", + "napels", + "napijn", + "napoleon", + "narigheid", + "narratief", + "naseizoen", + "nasibal", + "navigatie", + "nawijn", + "negatief", + "nekletsel", + "nekwervel", + "neolatijn", + "neonataal", + "neptunus", + "nerd", + "nest", + "neuzelaar", + "nihiliste", + "nijenhuis", + "nijging", + "nijhoff", + "nijl", + "nijptang", + "nippel", + "nokkenas", + "noordam", + "noren", + "normaal", + "nottelman", + "notulant", + "nout", + "nuance", + "nuchter", + "nudorp", + "nulde", + "nullijn", + "nulmeting", + "nunspeet", + "nylon", + "obelisk", + "object", + "oblie", + "obsceen", + "occlusie", + "oceaan", + "ochtend", + "ockhuizen", + "oerdom", + "oergezond", + "oerlaag", + "oester", + "okhuijsen", + "olifant", + "olijfboer", + "omaans", + "ombudsman", + "omdat", + "omdijken", + "omdoen", + "omgebouwd", + "omkeer", + "omkomen", + "ommegaand", + "ommuren", + "omroep", + "omruil", + "omslaan", + "omsmeden", + "omvaar", + "onaardig", + "onedel", + "onenig", + "onheilig", + "onrecht", + "onroerend", + "ontcijfer", + "onthaal", + "ontvallen", + "ontzadeld", + "onzacht", + "onzin", + "onzuiver", + "oogappel", + "ooibos", + "ooievaar", + "ooit", + "oorarts", + "oorhanger", + "oorijzer", + "oorklep", + "oorschelp", + "oorworm", + "oorzaak", + "opdagen", + "opdien", + "opdweilen", + "opel", + "opgebaard", + "opinie", + "opjutten", + "opkijken", + "opklaar", + "opkuisen", + "opkwam", + "opnaaien", + "opossum", + "opsieren", + "opsmeer", + "optreden", + "opvijzel", + "opvlammen", + "opwind", + "oraal", + "orchidee", + "orkest", + "ossuarium", + "ostendorf", + "oublie", + "oudachtig", + "oudbakken", + "oudnoors", + "oudshoorn", + "oudtante", + "oven", + "over", + "oxidant", + "pablo", + "pacht", + "paktafel", + "pakzadel", + "paljas", + "panharing", + "papfles", + "paprika", + "parochie", + "paus", + "pauze", + "paviljoen", + "peek", + "pegel", + "peigeren", + "pekela", + "pendant", + "penibel", + "pepmiddel", + "peptalk", + "periferie", + "perron", + "pessarium", + "peter", + "petfles", + "petgat", + "peuk", + "pfeifer", + "picknick", + "pief", + "pieneman", + "pijlkruid", + "pijnacker", + "pijpelink", + "pikdonker", + "pikeer", + "pilaar", + "pionier", + "pipet", + "piscine", + "pissebed", + "pitchen", + "pixel", + "plamuren", + "plan", + "plausibel", + "plegen", + "plempen", + "pleonasme", + "plezant", + "podoloog", + "pofmouw", + "pokdalig", + "ponywagen", + "popachtig", + "popidool", + "porren", + "positie", + "potten", + "pralen", + "prezen", + "prijzen", + "privaat", + "proef", + "prooi", + "prozawerk", + "pruik", + "prul", + "publiceer", + "puck", + "puilen", + "pukkelig", + "pulveren", + "pupil", + "puppy", + "purmerend", + "pustjens", + "putemmer", + "puzzelaar", + "queenie", + "quiche", + "raam", + "raar", + "raat", + "raes", + "ralf", + "rally", + "ramona", + "ramselaar", + "ranonkel", + "rapen", + "rapunzel", + "rarekiek", + "rarigheid", + "rattenhol", + "ravage", + "reactie", + "recreant", + "redacteur", + "redster", + "reewild", + "regie", + "reijnders", + "rein", + "replica", + "revanche", + "rigide", + "rijbaan", + "rijdansen", + "rijgen", + "rijkdom", + "rijles", + "rijnwijn", + "rijpma", + "rijstafel", + "rijtaak", + "rijzwepen", + "rioleer", + "ripdeal", + "riphagen", + "riskant", + "rits", + "rivaal", + "robbedoes", + "robot", + "rockact", + "rodijk", + "rogier", + "rohypnol", + "rollaag", + "rolpaal", + "roltafel", + "roof", + "roon", + "roppen", + "rosbief", + "rosharig", + "rosielle", + "rotan", + "rotleven", + "rotten", + "rotvaart", + "royaal", + "royeer", + "rubato", + "ruby", + "ruche", + "rudge", + "ruggetje", + "rugnummer", + "rugpijn", + "rugtitel", + "rugzak", + "ruilbaar", + "ruis", + "ruit", + "rukwind", + "rulijs", + "rumoeren", + "rumsdorp", + "rumtaart", + "runnen", + "russchen", + "ruwkruid", + "saboteer", + "saksisch", + "salade", + "salpeter", + "sambabal", + "samsam", + "satelliet", + "satineer", + "saus", + "scampi", + "scarabee", + "scenario", + "schobben", + "schubben", + "scout", + "secessie", + "secondair", + "seculair", + "sediment", + "seeland", + "settelen", + "setwinst", + "sheriff", + "shiatsu", + "siciliaan", + "sidderaal", + "sigma", + "sijben", + "silvana", + "simkaart", + "sinds", + "situatie", + "sjaak", + "sjardijn", + "sjezen", + "sjor", + "skinhead", + "skylab", + "slamixen", + "sleijpen", + "slijkerig", + "slordig", + "slowaak", + "sluieren", + "smadelijk", + "smiecht", + "smoel", + "smos", + "smukken", + "snackcar", + "snavel", + "sneaker", + "sneu", + "snijdbaar", + "snit", + "snorder", + "soapbox", + "soetekouw", + "soigneren", + "sojaboon", + "solo", + "solvabel", + "somber", + "sommatie", + "soort", + "soppen", + "sopraan", + "soundbar", + "spanen", + "spawater", + "spijgat", + "spinaal", + "spionage", + "spiraal", + "spleet", + "splijt", + "spoed", + "sporen", + "spul", + "spuug", + "spuw", + "stalen", + "standaard", + "star", + "stefan", + "stencil", + "stijf", + "stil", + "stip", + "stopdas", + "stoten", + "stoven", + "straat", + "strobbe", + "strubbel", + "stucadoor", + "stuif", + "stukadoor", + "subhoofd", + "subregent", + "sudoku", + "sukade", + "sulfaat", + "surinaams", + "suus", + "syfilis", + "symboliek", + "sympathie", + "synagoge", + "synchroon", + "synergie", + "systeem", + "taanderij", + "tabak", + "tachtig", + "tackelen", + "taiwanees", + "talman", + "tamheid", + "tangaslip", + "taps", + "tarkan", + "tarwe", + "tasman", + "tatjana", + "taxameter", + "teil", + "teisman", + "telbaar", + "telco", + "telganger", + "telstar", + "tenant", + "tepel", + "terzet", + "testament", + "ticket", + "tiesinga", + "tijdelijk", + "tika", + "tiksel", + "tilleman", + "timbaal", + "tinsteen", + "tiplijn", + "tippelaar", + "tjirpen", + "toezeggen", + "tolbaas", + "tolgeld", + "tolhek", + "tolo", + "tolpoort", + "toltarief", + "tolvrij", + "tomaat", + "tondeuse", + "toog", + "tooi", + "toonbaar", + "toos", + "topclub", + "toppen", + "toptalent", + "topvrouw", + "toque", + "torment", + "tornado", + "tosti", + "totdat", + "toucheer", + "toulouse", + "tournedos", + "tout", + "trabant", + "tragedie", + "trailer", + "traject", + "traktaat", + "trauma", + "tray", + "trechter", + "tred", + "tref", + "treur", + "troebel", + "tros", + "trucage", + "truffel", + "tsaar", + "tucht", + "tuenter", + "tuitelig", + "tukje", + "tuktuk", + "tulp", + "tuma", + "tureluurs", + "twijfel", + "twitteren", + "tyfoon", + "typograaf", + "ugandees", + "uiachtig", + "uier", + "uisnipper", + "ultiem", + "unitair", + "uranium", + "urbaan", + "urendag", + "ursula", + "uurcirkel", + "uurglas", + "uzelf", + "vaat", + "vakantie", + "vakleraar", + "valbijl", + "valpartij", + "valreep", + "valuatie", + "vanmiddag", + "vanonder", + "varaan", + "varken", + "vaten", + "veenbes", + "veeteler", + "velgrem", + "vellekoop", + "velvet", + "veneberg", + "venlo", + "vent", + "venusberg", + "venw", + "veredeld", + "verf", + "verhaaf", + "vermaak", + "vernaaid", + "verraad", + "vers", + "veruit", + "verzaagd", + "vetachtig", + "vetlok", + "vetmesten", + "veto", + "vetrek", + "vetstaart", + "vetten", + "veurink", + "viaduct", + "vibrafoon", + "vicariaat", + "vieux", + "vieveen", + "vijfvoud", + "villa", + "vilt", + "vimmetje", + "vindbaar", + "vips", + "virtueel", + "visdieven", + "visee", + "visie", + "vlaag", + "vleugel", + "vmbo", + "vocht", + "voesenek", + "voicemail", + "voip", + "volg", + "vork", + "vorselaar", + "voyeur", + "vracht", + "vrekkig", + "vreten", + "vrije", + "vrozen", + "vrucht", + "vucht", + "vugt", + "vulkaan", + "vulmiddel", + "vulva", + "vuren", + "waas", + "wacht", + "wadvogel", + "wafel", + "waffel", + "walhalla", + "walnoot", + "walraven", + "wals", + "walvis", + "wandaad", + "wanen", + "wanmolen", + "want", + "warklomp", + "warm", + "wasachtig", + "wasteil", + "watt", + "webhandel", + "weblog", + "webpagina", + "webzine", + "wedereis", + "wedstrijd", + "weeda", + "weert", + "wegmaaien", + "wegscheer", + "wekelijks", + "wekken", + "wekroep", + "wektoon", + "weldaad", + "welwater", + "wendbaar", + "wenkbrauw", + "wens", + "wentelaar", + "wervel", + "wesseling", + "wetboek", + "wetmatig", + "whirlpool", + "wijbrands", + "wijdbeens", + "wijk", + "wijnbes", + "wijting", + "wild", + "wimpelen", + "wingebied", + "winplaats", + "winter", + "winzucht", + "wipstaart", + "wisgerhof", + "withaar", + "witmaker", + "wokkel", + "wolf", + "wonenden", + "woning", + "worden", + "worp", + "wortel", + "wrat", + "wrijf", + "wringen", + "yoghurt", + "ypsilon", + "zaaijer", + "zaak", + "zacharias", + "zakelijk", + "zakkam", + "zakwater", + "zalf", + "zalig", + "zaniken", + "zebracode", + "zeeblauw", + "zeef", + "zeegaand", + "zeeuw", + "zege", + "zegje", + "zeil", + "zesbaans", + "zesenhalf", + "zeskantig", + "zesmaal", + "zetbaas", + "zetpil", + "zeulen", + "ziezo", + "zigzag", + "zijaltaar", + "zijbeuk", + "zijlijn", + "zijmuur", + "zijn", + "zijwaarts", + "zijzelf", + "zilt", + "zimmerman", + "zinledig", + "zinnelijk", + "zionist", + "zitdag", + "zitruimte", + "zitzak", + "zoal", + "zodoende", + "zoekbots", + "zoem", + "zoiets", + "zojuist", + "zondaar", + "zotskap", + "zottebol", + "zucht", + "zuivel", + "zulk", + "zult", + "zuster", + "zuur", + "zweedijk", + "zwendel", + "zwepen", + "zwiep", + "zwijmel", + "zworen" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/english.dart b/cw_wownero/lib/mnemonics/english.dart new file mode 100644 index 000000000..fb464d04e --- /dev/null +++ b/cw_wownero/lib/mnemonics/english.dart @@ -0,0 +1,1630 @@ +class EnglishMnemonics { + static const words = [ + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "guru", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "menu", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" + ]; +} diff --git a/cw_wownero/lib/mnemonics/french.dart b/cw_wownero/lib/mnemonics/french.dart new file mode 100644 index 000000000..76d556f6a --- /dev/null +++ b/cw_wownero/lib/mnemonics/french.dart @@ -0,0 +1,1630 @@ +class FrenchMnemonics { + static const words = [ + "abandon", + "abattre", + "aboi", + "abolir", + "aborder", + "abri", + "absence", + "absolu", + "abuser", + "acacia", + "acajou", + "accent", + "accord", + "accrocher", + "accuser", + "acerbe", + "achat", + "acheter", + "acide", + "acier", + "acquis", + "acte", + "action", + "adage", + "adepte", + "adieu", + "admettre", + "admis", + "adorer", + "adresser", + "aduler", + "affaire", + "affirmer", + "afin", + "agacer", + "agent", + "agir", + "agiter", + "agonie", + "agrafe", + "agrume", + "aider", + "aigle", + "aigre", + "aile", + "ailleurs", + "aimant", + "aimer", + "ainsi", + "aise", + "ajouter", + "alarme", + "album", + "alcool", + "alerte", + "algue", + "alibi", + "aller", + "allumer", + "alors", + "amande", + "amener", + "amie", + "amorcer", + "amour", + "ample", + "amuser", + "ananas", + "ancien", + "anglais", + "angoisse", + "animal", + "anneau", + "annoncer", + "apercevoir", + "apparence", + "appel", + "apporter", + "apprendre", + "appuyer", + "arbre", + "arcade", + "arceau", + "arche", + "ardeur", + "argent", + "argile", + "aride", + "arme", + "armure", + "arracher", + "arriver", + "article", + "asile", + "aspect", + "assaut", + "assez", + "assister", + "assurer", + "astre", + "astuce", + "atlas", + "atroce", + "attacher", + "attente", + "attirer", + "aube", + "aucun", + "audace", + "auparavant", + "auquel", + "aurore", + "aussi", + "autant", + "auteur", + "autoroute", + "autre", + "aval", + "avant", + "avec", + "avenir", + "averse", + "aveu", + "avide", + "avion", + "avis", + "avoir", + "avouer", + "avril", + "azote", + "azur", + "badge", + "bagage", + "bague", + "bain", + "baisser", + "balai", + "balcon", + "balise", + "balle", + "bambou", + "banane", + "banc", + "bandage", + "banjo", + "banlieue", + "bannir", + "banque", + "baobab", + "barbe", + "barque", + "barrer", + "bassine", + "bataille", + "bateau", + "battre", + "baver", + "bavoir", + "bazar", + "beau", + "beige", + "berger", + "besoin", + "beurre", + "biais", + "biceps", + "bidule", + "bien", + "bijou", + "bilan", + "billet", + "blanc", + "blason", + "bleu", + "bloc", + "blond", + "bocal", + "boire", + "boiserie", + "boiter", + "bonbon", + "bondir", + "bonheur", + "bordure", + "borgne", + "borner", + "bosse", + "bouche", + "bouder", + "bouger", + "boule", + "bourse", + "bout", + "boxe", + "brader", + "braise", + "branche", + "braquer", + "bras", + "brave", + "brebis", + "brevet", + "brider", + "briller", + "brin", + "brique", + "briser", + "broche", + "broder", + "bronze", + "brosser", + "brouter", + "bruit", + "brute", + "budget", + "buffet", + "bulle", + "bureau", + "buriner", + "buste", + "buter", + "butiner", + "cabas", + "cabinet", + "cabri", + "cacao", + "cacher", + "cadeau", + "cadre", + "cage", + "caisse", + "caler", + "calme", + "camarade", + "camion", + "campagne", + "canal", + "canif", + "capable", + "capot", + "carat", + "caresser", + "carie", + "carpe", + "cartel", + "casier", + "casque", + "casserole", + "cause", + "cavale", + "cave", + "ceci", + "cela", + "celui", + "cendre", + "cent", + "cependant", + "cercle", + "cerise", + "cerner", + "certes", + "cerveau", + "cesser", + "chacun", + "chair", + "chaleur", + "chamois", + "chanson", + "chaque", + "charge", + "chasse", + "chat", + "chaud", + "chef", + "chemin", + "cheveu", + "chez", + "chicane", + "chien", + "chiffre", + "chiner", + "chiot", + "chlore", + "choc", + "choix", + "chose", + "chou", + "chute", + "cibler", + "cidre", + "ciel", + "cigale", + "cinq", + "cintre", + "cirage", + "cirque", + "ciseau", + "citation", + "citer", + "citron", + "civet", + "clairon", + "clan", + "classe", + "clavier", + "clef", + "climat", + "cloche", + "cloner", + "clore", + "clos", + "clou", + "club", + "cobra", + "cocon", + "coiffer", + "coin", + "colline", + "colon", + "combat", + "comme", + "compte", + "conclure", + "conduire", + "confier", + "connu", + "conseil", + "contre", + "convenir", + "copier", + "cordial", + "cornet", + "corps", + "cosmos", + "coton", + "couche", + "coude", + "couler", + "coupure", + "cour", + "couteau", + "couvrir", + "crabe", + "crainte", + "crampe", + "cran", + "creuser", + "crever", + "crier", + "crime", + "crin", + "crise", + "crochet", + "croix", + "cruel", + "cuisine", + "cuite", + "culot", + "culte", + "cumul", + "cure", + "curieux", + "cuve", + "dame", + "danger", + "dans", + "davantage", + "debout", + "dedans", + "dehors", + "delta", + "demain", + "demeurer", + "demi", + "dense", + "dent", + "depuis", + "dernier", + "descendre", + "dessus", + "destin", + "dette", + "deuil", + "deux", + "devant", + "devenir", + "devin", + "devoir", + "dicton", + "dieu", + "difficile", + "digestion", + "digue", + "diluer", + "dimanche", + "dinde", + "diode", + "dire", + "diriger", + "discours", + "disposer", + "distance", + "divan", + "divers", + "docile", + "docteur", + "dodu", + "dogme", + "doigt", + "dominer", + "donation", + "donjon", + "donner", + "dopage", + "dorer", + "dormir", + "doseur", + "douane", + "double", + "douche", + "douleur", + "doute", + "doux", + "douzaine", + "draguer", + "drame", + "drap", + "dresser", + "droit", + "duel", + "dune", + "duper", + "durant", + "durcir", + "durer", + "eaux", + "effacer", + "effet", + "effort", + "effrayant", + "elle", + "embrasser", + "emmener", + "emparer", + "empire", + "employer", + "emporter", + "enclos", + "encore", + "endive", + "endormir", + "endroit", + "enduit", + "enfant", + "enfermer", + "enfin", + "enfler", + "enfoncer", + "enfuir", + "engager", + "engin", + "enjeu", + "enlever", + "ennemi", + "ennui", + "ensemble", + "ensuite", + "entamer", + "entendre", + "entier", + "entourer", + "entre", + "envelopper", + "envie", + "envoyer", + "erreur", + "escalier", + "espace", + "espoir", + "esprit", + "essai", + "essor", + "essuyer", + "estimer", + "exact", + "examiner", + "excuse", + "exemple", + "exiger", + "exil", + "exister", + "exode", + "expliquer", + "exposer", + "exprimer", + "extase", + "fable", + "facette", + "facile", + "fade", + "faible", + "faim", + "faire", + "fait", + "falloir", + "famille", + "faner", + "farce", + "farine", + "fatigue", + "faucon", + "faune", + "faute", + "faux", + "faveur", + "favori", + "faxer", + "feinter", + "femme", + "fendre", + "fente", + "ferme", + "festin", + "feuille", + "feutre", + "fiable", + "fibre", + "ficher", + "fier", + "figer", + "figure", + "filet", + "fille", + "filmer", + "fils", + "filtre", + "final", + "finesse", + "finir", + "fiole", + "firme", + "fixe", + "flacon", + "flair", + "flamme", + "flan", + "flaque", + "fleur", + "flocon", + "flore", + "flot", + "flou", + "fluide", + "fluor", + "flux", + "focus", + "foin", + "foire", + "foison", + "folie", + "fonction", + "fondre", + "fonte", + "force", + "forer", + "forger", + "forme", + "fort", + "fosse", + "fouet", + "fouine", + "foule", + "four", + "foyer", + "frais", + "franc", + "frapper", + "freiner", + "frimer", + "friser", + "frite", + "froid", + "froncer", + "fruit", + "fugue", + "fuir", + "fuite", + "fumer", + "fureur", + "furieux", + "fuser", + "fusil", + "futile", + "futur", + "gagner", + "gain", + "gala", + "galet", + "galop", + "gamme", + "gant", + "garage", + "garde", + "garer", + "gauche", + "gaufre", + "gaule", + "gaver", + "gazon", + "geler", + "genou", + "genre", + "gens", + "gercer", + "germer", + "geste", + "gibier", + "gicler", + "gilet", + "girafe", + "givre", + "glace", + "glisser", + "globe", + "gloire", + "gluant", + "gober", + "golf", + "gommer", + "gorge", + "gosier", + "goutte", + "grain", + "gramme", + "grand", + "gras", + "grave", + "gredin", + "griffure", + "griller", + "gris", + "gronder", + "gros", + "grotte", + "groupe", + "grue", + "guerrier", + "guetter", + "guider", + "guise", + "habiter", + "hache", + "haie", + "haine", + "halte", + "hamac", + "hanche", + "hangar", + "hanter", + "haras", + "hareng", + "harpe", + "hasard", + "hausse", + "haut", + "havre", + "herbe", + "heure", + "hibou", + "hier", + "histoire", + "hiver", + "hochet", + "homme", + "honneur", + "honte", + "horde", + "horizon", + "hormone", + "houle", + "housse", + "hublot", + "huile", + "huit", + "humain", + "humble", + "humide", + "humour", + "hurler", + "idole", + "igloo", + "ignorer", + "illusion", + "image", + "immense", + "immobile", + "imposer", + "impression", + "incapable", + "inconnu", + "index", + "indiquer", + "infime", + "injure", + "inox", + "inspirer", + "instant", + "intention", + "intime", + "inutile", + "inventer", + "inviter", + "iode", + "iris", + "issue", + "ivre", + "jade", + "jadis", + "jamais", + "jambe", + "janvier", + "jardin", + "jauge", + "jaunisse", + "jeter", + "jeton", + "jeudi", + "jeune", + "joie", + "joindre", + "joli", + "joueur", + "journal", + "judo", + "juge", + "juillet", + "juin", + "jument", + "jungle", + "jupe", + "jupon", + "jurer", + "juron", + "jury", + "jusque", + "juste", + "kayak", + "ketchup", + "kilo", + "kiwi", + "koala", + "label", + "lacet", + "lacune", + "laine", + "laisse", + "lait", + "lame", + "lancer", + "lande", + "laque", + "lard", + "largeur", + "larme", + "larve", + "lasso", + "laver", + "lendemain", + "lentement", + "lequel", + "lettre", + "leur", + "lever", + "levure", + "liane", + "libre", + "lien", + "lier", + "lieutenant", + "ligne", + "ligoter", + "liguer", + "limace", + "limer", + "limite", + "lingot", + "lion", + "lire", + "lisser", + "litre", + "livre", + "lobe", + "local", + "logis", + "loin", + "loisir", + "long", + "loque", + "lors", + "lotus", + "louer", + "loup", + "lourd", + "louve", + "loyer", + "lubie", + "lucide", + "lueur", + "luge", + "luire", + "lundi", + "lune", + "lustre", + "lutin", + "lutte", + "luxe", + "machine", + "madame", + "magie", + "magnifique", + "magot", + "maigre", + "main", + "mairie", + "maison", + "malade", + "malheur", + "malin", + "manche", + "manger", + "manier", + "manoir", + "manquer", + "marche", + "mardi", + "marge", + "mariage", + "marquer", + "mars", + "masque", + "masse", + "matin", + "mauvais", + "meilleur", + "melon", + "membre", + "menacer", + "mener", + "mensonge", + "mentir", + "menu", + "merci", + "merlu", + "mesure", + "mettre", + "meuble", + "meunier", + "meute", + "miche", + "micro", + "midi", + "miel", + "miette", + "mieux", + "milieu", + "mille", + "mimer", + "mince", + "mineur", + "ministre", + "minute", + "mirage", + "miroir", + "miser", + "mite", + "mixte", + "mobile", + "mode", + "module", + "moins", + "mois", + "moment", + "momie", + "monde", + "monsieur", + "monter", + "moquer", + "moral", + "morceau", + "mordre", + "morose", + "morse", + "mortier", + "morue", + "motif", + "motte", + "moudre", + "moule", + "mourir", + "mousse", + "mouton", + "mouvement", + "moyen", + "muer", + "muette", + "mugir", + "muguet", + "mulot", + "multiple", + "munir", + "muret", + "muse", + "musique", + "muter", + "nacre", + "nager", + "nain", + "naissance", + "narine", + "narrer", + "naseau", + "nasse", + "nation", + "nature", + "naval", + "navet", + "naviguer", + "navrer", + "neige", + "nerf", + "nerveux", + "neuf", + "neutre", + "neuve", + "neveu", + "niche", + "nier", + "niveau", + "noble", + "noce", + "nocif", + "noir", + "nomade", + "nombre", + "nommer", + "nord", + "norme", + "notaire", + "notice", + "notre", + "nouer", + "nougat", + "nourrir", + "nous", + "nouveau", + "novice", + "noyade", + "noyer", + "nuage", + "nuance", + "nuire", + "nuit", + "nulle", + "nuque", + "oasis", + "objet", + "obliger", + "obscur", + "observer", + "obtenir", + "obus", + "occasion", + "occuper", + "ocre", + "octet", + "odeur", + "odorat", + "offense", + "officier", + "offrir", + "ogive", + "oiseau", + "olive", + "ombre", + "onctueux", + "onduler", + "ongle", + "onze", + "opter", + "option", + "orageux", + "oral", + "orange", + "orbite", + "ordinaire", + "ordre", + "oreille", + "organe", + "orgie", + "orgueil", + "orient", + "origan", + "orner", + "orteil", + "ortie", + "oser", + "osselet", + "otage", + "otarie", + "ouate", + "oublier", + "ouest", + "ours", + "outil", + "outre", + "ouvert", + "ouvrir", + "ovale", + "ozone", + "pacte", + "page", + "paille", + "pain", + "paire", + "paix", + "palace", + "palissade", + "palmier", + "palpiter", + "panda", + "panneau", + "papa", + "papier", + "paquet", + "parc", + "pardi", + "parfois", + "parler", + "parmi", + "parole", + "partir", + "parvenir", + "passer", + "pastel", + "patin", + "patron", + "paume", + "pause", + "pauvre", + "paver", + "pavot", + "payer", + "pays", + "peau", + "peigne", + "peinture", + "pelage", + "pelote", + "pencher", + "pendre", + "penser", + "pente", + "percer", + "perdu", + "perle", + "permettre", + "personne", + "perte", + "peser", + "pesticide", + "petit", + "peuple", + "peur", + "phase", + "photo", + "phrase", + "piano", + "pied", + "pierre", + "pieu", + "pile", + "pilier", + "pilote", + "pilule", + "piment", + "pincer", + "pinson", + "pinte", + "pion", + "piquer", + "pirate", + "pire", + "piste", + "piton", + "pitre", + "pivot", + "pizza", + "placer", + "plage", + "plaire", + "plan", + "plaque", + "plat", + "plein", + "pleurer", + "pliage", + "plier", + "plonger", + "plot", + "pluie", + "plume", + "plus", + "pneu", + "poche", + "podium", + "poids", + "poil", + "point", + "poire", + "poison", + "poitrine", + "poivre", + "police", + "pollen", + "pomme", + "pompier", + "poncer", + "pondre", + "pont", + "portion", + "poser", + "position", + "possible", + "poste", + "potage", + "potin", + "pouce", + "poudre", + "poulet", + "poumon", + "poupe", + "pour", + "pousser", + "poutre", + "pouvoir", + "prairie", + "premier", + "prendre", + "presque", + "preuve", + "prier", + "primeur", + "prince", + "prison", + "priver", + "prix", + "prochain", + "produire", + "profond", + "proie", + "projet", + "promener", + "prononcer", + "propre", + "prose", + "prouver", + "prune", + "public", + "puce", + "pudeur", + "puiser", + "pull", + "pulpe", + "puma", + "punir", + "purge", + "putois", + "quand", + "quartier", + "quasi", + "quatre", + "quel", + "question", + "queue", + "quiche", + "quille", + "quinze", + "quitter", + "quoi", + "rabais", + "raboter", + "race", + "racheter", + "racine", + "racler", + "raconter", + "radar", + "radio", + "rafale", + "rage", + "ragot", + "raideur", + "raie", + "rail", + "raison", + "ramasser", + "ramener", + "rampe", + "rance", + "rang", + "rapace", + "rapide", + "rapport", + "rarement", + "rasage", + "raser", + "rasoir", + "rassurer", + "rater", + "ratio", + "rature", + "ravage", + "ravir", + "rayer", + "rayon", + "rebond", + "recevoir", + "recherche", + "record", + "reculer", + "redevenir", + "refuser", + "regard", + "regretter", + "rein", + "rejeter", + "rejoindre", + "relation", + "relever", + "religion", + "remarquer", + "remettre", + "remise", + "remonter", + "remplir", + "remuer", + "rencontre", + "rendre", + "renier", + "renoncer", + "rentrer", + "renverser", + "repas", + "repli", + "reposer", + "reproche", + "requin", + "respect", + "ressembler", + "reste", + "retard", + "retenir", + "retirer", + "retour", + "retrouver", + "revenir", + "revoir", + "revue", + "rhume", + "ricaner", + "riche", + "rideau", + "ridicule", + "rien", + "rigide", + "rincer", + "rire", + "risquer", + "rituel", + "rivage", + "rive", + "robe", + "robot", + "robuste", + "rocade", + "roche", + "rodeur", + "rogner", + "roman", + "rompre", + "ronce", + "rondeur", + "ronger", + "roque", + "rose", + "rosir", + "rotation", + "rotule", + "roue", + "rouge", + "rouler", + "route", + "ruban", + "rubis", + "ruche", + "rude", + "ruelle", + "ruer", + "rugby", + "rugir", + "ruine", + "rumeur", + "rural", + "ruse", + "rustre", + "sable", + "sabot", + "sabre", + "sacre", + "sage", + "saint", + "saisir", + "salade", + "salive", + "salle", + "salon", + "salto", + "salut", + "salve", + "samba", + "sandale", + "sanguin", + "sapin", + "sarcasme", + "satisfaire", + "sauce", + "sauf", + "sauge", + "saule", + "sauna", + "sauter", + "sauver", + "savoir", + "science", + "scoop", + "score", + "second", + "secret", + "secte", + "seigneur", + "sein", + "seize", + "selle", + "selon", + "semaine", + "sembler", + "semer", + "semis", + "sensuel", + "sentir", + "sept", + "serpe", + "serrer", + "sertir", + "service", + "seuil", + "seulement", + "short", + "sien", + "sigle", + "signal", + "silence", + "silo", + "simple", + "singe", + "sinon", + "sinus", + "sioux", + "sirop", + "site", + "situation", + "skier", + "snob", + "sobre", + "social", + "socle", + "sodium", + "soigner", + "soir", + "soixante", + "soja", + "solaire", + "soldat", + "soleil", + "solide", + "solo", + "solvant", + "sombre", + "somme", + "somnoler", + "sondage", + "songeur", + "sonner", + "sorte", + "sosie", + "sottise", + "souci", + "soudain", + "souffrir", + "souhaiter", + "soulever", + "soumettre", + "soupe", + "sourd", + "soustraire", + "soutenir", + "souvent", + "soyeux", + "spectacle", + "sport", + "stade", + "stagiaire", + "stand", + "star", + "statue", + "stock", + "stop", + "store", + "style", + "suave", + "subir", + "sucre", + "suer", + "suffire", + "suie", + "suite", + "suivre", + "sujet", + "sulfite", + "supposer", + "surf", + "surprendre", + "surtout", + "surveiller", + "tabac", + "table", + "tabou", + "tache", + "tacler", + "tacot", + "tact", + "taie", + "taille", + "taire", + "talon", + "talus", + "tandis", + "tango", + "tanin", + "tant", + "taper", + "tapis", + "tard", + "tarif", + "tarot", + "tarte", + "tasse", + "taureau", + "taux", + "taverne", + "taxer", + "taxi", + "tellement", + "temple", + "tendre", + "tenir", + "tenter", + "tenu", + "terme", + "ternir", + "terre", + "test", + "texte", + "thym", + "tibia", + "tiers", + "tige", + "tipi", + "tique", + "tirer", + "tissu", + "titre", + "toast", + "toge", + "toile", + "toiser", + "toiture", + "tomber", + "tome", + "tonne", + "tonte", + "toque", + "torse", + "tortue", + "totem", + "toucher", + "toujours", + "tour", + "tousser", + "tout", + "toux", + "trace", + "train", + "trame", + "tranquille", + "travail", + "trembler", + "trente", + "tribu", + "trier", + "trio", + "tripe", + "triste", + "troc", + "trois", + "tromper", + "tronc", + "trop", + "trotter", + "trouer", + "truc", + "truite", + "tuba", + "tuer", + "tuile", + "turbo", + "tutu", + "tuyau", + "type", + "union", + "unique", + "unir", + "unisson", + "untel", + "urne", + "usage", + "user", + "usiner", + "usure", + "utile", + "vache", + "vague", + "vaincre", + "valeur", + "valoir", + "valser", + "valve", + "vampire", + "vaseux", + "vaste", + "veau", + "veille", + "veine", + "velours", + "velu", + "vendre", + "venir", + "vent", + "venue", + "verbe", + "verdict", + "version", + "vertige", + "verve", + "veste", + "veto", + "vexer", + "vice", + "victime", + "vide", + "vieil", + "vieux", + "vigie", + "vigne", + "ville", + "vingt", + "violent", + "virer", + "virus", + "visage", + "viser", + "visite", + "visuel", + "vitamine", + "vitrine", + "vivant", + "vivre", + "vocal", + "vodka", + "vogue", + "voici", + "voile", + "voir", + "voisin", + "voiture", + "volaille", + "volcan", + "voler", + "volt", + "votant", + "votre", + "vouer", + "vouloir", + "vous", + "voyage", + "voyou", + "vrac", + "vrai", + "yacht", + "yeti", + "yeux", + "yoga", + "zeste", + "zinc", + "zone", + "zoom" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/german.dart b/cw_wownero/lib/mnemonics/german.dart new file mode 100644 index 000000000..1491c9b0e --- /dev/null +++ b/cw_wownero/lib/mnemonics/german.dart @@ -0,0 +1,1630 @@ +class GermanMnemonics { + static const words = [ + "Abakus", + "Abart", + "abbilden", + "Abbruch", + "Abdrift", + "Abendrot", + "Abfahrt", + "abfeuern", + "Abflug", + "abfragen", + "Abglanz", + "abhärten", + "abheben", + "Abhilfe", + "Abitur", + "Abkehr", + "Ablauf", + "ablecken", + "Ablösung", + "Abnehmer", + "abnutzen", + "Abonnent", + "Abrasion", + "Abrede", + "abrüsten", + "Absicht", + "Absprung", + "Abstand", + "absuchen", + "Abteil", + "Abundanz", + "abwarten", + "Abwurf", + "Abzug", + "Achse", + "Achtung", + "Acker", + "Aderlass", + "Adler", + "Admiral", + "Adresse", + "Affe", + "Affront", + "Afrika", + "Aggregat", + "Agilität", + "ähneln", + "Ahnung", + "Ahorn", + "Akazie", + "Akkord", + "Akrobat", + "Aktfoto", + "Aktivist", + "Albatros", + "Alchimie", + "Alemanne", + "Alibi", + "Alkohol", + "Allee", + "Allüre", + "Almosen", + "Almweide", + "Aloe", + "Alpaka", + "Alpental", + "Alphabet", + "Alpinist", + "Alraune", + "Altbier", + "Alter", + "Altflöte", + "Altruist", + "Alublech", + "Aludose", + "Amateur", + "Amazonas", + "Ameise", + "Amnesie", + "Amok", + "Ampel", + "Amphibie", + "Ampulle", + "Amsel", + "Amulett", + "Anakonda", + "Analogie", + "Ananas", + "Anarchie", + "Anatomie", + "Anbau", + "Anbeginn", + "anbieten", + "Anblick", + "ändern", + "andocken", + "Andrang", + "anecken", + "Anflug", + "Anfrage", + "Anführer", + "Angebot", + "Angler", + "Anhalter", + "Anhöhe", + "Animator", + "Anis", + "Anker", + "ankleben", + "Ankunft", + "Anlage", + "anlocken", + "Anmut", + "Annahme", + "Anomalie", + "Anonymus", + "Anorak", + "anpeilen", + "Anrecht", + "Anruf", + "Ansage", + "Anschein", + "Ansicht", + "Ansporn", + "Anteil", + "Antlitz", + "Antrag", + "Antwort", + "Anwohner", + "Aorta", + "Apfel", + "Appetit", + "Applaus", + "Aquarium", + "Arbeit", + "Arche", + "Argument", + "Arktis", + "Armband", + "Aroma", + "Asche", + "Askese", + "Asphalt", + "Asteroid", + "Ästhetik", + "Astronom", + "Atelier", + "Athlet", + "Atlantik", + "Atmung", + "Audienz", + "aufatmen", + "Auffahrt", + "aufholen", + "aufregen", + "Aufsatz", + "Auftritt", + "Aufwand", + "Augapfel", + "Auktion", + "Ausbruch", + "Ausflug", + "Ausgabe", + "Aushilfe", + "Ausland", + "Ausnahme", + "Aussage", + "Autobahn", + "Avocado", + "Axthieb", + "Bach", + "backen", + "Badesee", + "Bahnhof", + "Balance", + "Balkon", + "Ballett", + "Balsam", + "Banane", + "Bandage", + "Bankett", + "Barbar", + "Barde", + "Barett", + "Bargeld", + "Barkasse", + "Barriere", + "Bart", + "Bass", + "Bastler", + "Batterie", + "Bauch", + "Bauer", + "Bauholz", + "Baujahr", + "Baum", + "Baustahl", + "Bauteil", + "Bauweise", + "Bazar", + "beachten", + "Beatmung", + "beben", + "Becher", + "Becken", + "bedanken", + "beeilen", + "beenden", + "Beere", + "befinden", + "Befreier", + "Begabung", + "Begierde", + "begrüßen", + "Beiboot", + "Beichte", + "Beifall", + "Beigabe", + "Beil", + "Beispiel", + "Beitrag", + "beizen", + "bekommen", + "beladen", + "Beleg", + "bellen", + "belohnen", + "Bemalung", + "Bengel", + "Benutzer", + "Benzin", + "beraten", + "Bereich", + "Bergluft", + "Bericht", + "Bescheid", + "Besitz", + "besorgen", + "Bestand", + "Besuch", + "betanken", + "beten", + "betören", + "Bett", + "Beule", + "Beute", + "Bewegung", + "bewirken", + "Bewohner", + "bezahlen", + "Bezug", + "biegen", + "Biene", + "Bierzelt", + "bieten", + "Bikini", + "Bildung", + "Billard", + "binden", + "Biobauer", + "Biologe", + "Bionik", + "Biotop", + "Birke", + "Bison", + "Bitte", + "Biwak", + "Bizeps", + "blasen", + "Blatt", + "Blauwal", + "Blende", + "Blick", + "Blitz", + "Blockade", + "Blödelei", + "Blondine", + "Blues", + "Blume", + "Blut", + "Bodensee", + "Bogen", + "Boje", + "Bollwerk", + "Bonbon", + "Bonus", + "Boot", + "Bordarzt", + "Börse", + "Böschung", + "Boudoir", + "Boxkampf", + "Boykott", + "Brahms", + "Brandung", + "Brauerei", + "Brecher", + "Breitaxt", + "Bremse", + "brennen", + "Brett", + "Brief", + "Brigade", + "Brillanz", + "bringen", + "brodeln", + "Brosche", + "Brötchen", + "Brücke", + "Brunnen", + "Brüste", + "Brutofen", + "Buch", + "Büffel", + "Bugwelle", + "Bühne", + "Buletten", + "Bullauge", + "Bumerang", + "bummeln", + "Buntglas", + "Bürde", + "Burgherr", + "Bursche", + "Busen", + "Buslinie", + "Bussard", + "Butangas", + "Butter", + "Cabrio", + "campen", + "Captain", + "Cartoon", + "Cello", + "Chalet", + "Charisma", + "Chefarzt", + "Chiffon", + "Chipsatz", + "Chirurg", + "Chor", + "Chronik", + "Chuzpe", + "Clubhaus", + "Cockpit", + "Codewort", + "Cognac", + "Coladose", + "Computer", + "Coupon", + "Cousin", + "Cracking", + "Crash", + "Curry", + "Dach", + "Dackel", + "daddeln", + "daliegen", + "Dame", + "Dammbau", + "Dämon", + "Dampflok", + "Dank", + "Darm", + "Datei", + "Datsche", + "Datteln", + "Datum", + "Dauer", + "Daunen", + "Deckel", + "Decoder", + "Defekt", + "Degen", + "Dehnung", + "Deiche", + "Dekade", + "Dekor", + "Delfin", + "Demut", + "denken", + "Deponie", + "Design", + "Desktop", + "Dessert", + "Detail", + "Detektiv", + "Dezibel", + "Diadem", + "Diagnose", + "Dialekt", + "Diamant", + "Dichter", + "Dickicht", + "Diesel", + "Diktat", + "Diplom", + "Direktor", + "Dirne", + "Diskurs", + "Distanz", + "Docht", + "Dohle", + "Dolch", + "Domäne", + "Donner", + "Dorade", + "Dorf", + "Dörrobst", + "Dorsch", + "Dossier", + "Dozent", + "Drachen", + "Draht", + "Drama", + "Drang", + "Drehbuch", + "Dreieck", + "Dressur", + "Drittel", + "Drossel", + "Druck", + "Duell", + "Duft", + "Düne", + "Dünung", + "dürfen", + "Duschbad", + "Düsenjet", + "Dynamik", + "Ebbe", + "Echolot", + "Echse", + "Eckball", + "Edding", + "Edelweiß", + "Eden", + "Edition", + "Efeu", + "Effekte", + "Egoismus", + "Ehre", + "Eiablage", + "Eiche", + "Eidechse", + "Eidotter", + "Eierkopf", + "Eigelb", + "Eiland", + "Eilbote", + "Eimer", + "einatmen", + "Einband", + "Eindruck", + "Einfall", + "Eingang", + "Einkauf", + "einladen", + "Einöde", + "Einrad", + "Eintopf", + "Einwurf", + "Einzug", + "Eisbär", + "Eisen", + "Eishöhle", + "Eismeer", + "Eiweiß", + "Ekstase", + "Elan", + "Elch", + "Elefant", + "Eleganz", + "Element", + "Elfe", + "Elite", + "Elixier", + "Ellbogen", + "Eloquenz", + "Emigrant", + "Emission", + "Emotion", + "Empathie", + "Empfang", + "Endzeit", + "Energie", + "Engpass", + "Enkel", + "Enklave", + "Ente", + "entheben", + "Entität", + "entladen", + "Entwurf", + "Episode", + "Epoche", + "erachten", + "Erbauer", + "erblühen", + "Erdbeere", + "Erde", + "Erdgas", + "Erdkunde", + "Erdnuss", + "Erdöl", + "Erdteil", + "Ereignis", + "Eremit", + "erfahren", + "Erfolg", + "erfreuen", + "erfüllen", + "Ergebnis", + "erhitzen", + "erkalten", + "erkennen", + "erleben", + "Erlösung", + "ernähren", + "erneuern", + "Ernte", + "Eroberer", + "eröffnen", + "Erosion", + "Erotik", + "Erpel", + "erraten", + "Erreger", + "erröten", + "Ersatz", + "Erstflug", + "Ertrag", + "Eruption", + "erwarten", + "erwidern", + "Erzbau", + "Erzeuger", + "erziehen", + "Esel", + "Eskimo", + "Eskorte", + "Espe", + "Espresso", + "essen", + "Etage", + "Etappe", + "Etat", + "Ethik", + "Etikett", + "Etüde", + "Eule", + "Euphorie", + "Europa", + "Everest", + "Examen", + "Exil", + "Exodus", + "Extrakt", + "Fabel", + "Fabrik", + "Fachmann", + "Fackel", + "Faden", + "Fagott", + "Fahne", + "Faible", + "Fairness", + "Fakt", + "Fakultät", + "Falke", + "Fallobst", + "Fälscher", + "Faltboot", + "Familie", + "Fanclub", + "Fanfare", + "Fangarm", + "Fantasie", + "Farbe", + "Farmhaus", + "Farn", + "Fasan", + "Faser", + "Fassung", + "fasten", + "Faulheit", + "Fauna", + "Faust", + "Favorit", + "Faxgerät", + "Fazit", + "fechten", + "Federboa", + "Fehler", + "Feier", + "Feige", + "feilen", + "Feinripp", + "Feldbett", + "Felge", + "Fellpony", + "Felswand", + "Ferien", + "Ferkel", + "Fernweh", + "Ferse", + "Fest", + "Fettnapf", + "Feuer", + "Fiasko", + "Fichte", + "Fiktion", + "Film", + "Filter", + "Filz", + "Finanzen", + "Findling", + "Finger", + "Fink", + "Finnwal", + "Fisch", + "Fitness", + "Fixpunkt", + "Fixstern", + "Fjord", + "Flachbau", + "Flagge", + "Flamenco", + "Flanke", + "Flasche", + "Flaute", + "Fleck", + "Flegel", + "flehen", + "Fleisch", + "fliegen", + "Flinte", + "Flirt", + "Flocke", + "Floh", + "Floskel", + "Floß", + "Flöte", + "Flugzeug", + "Flunder", + "Flusstal", + "Flutung", + "Fockmast", + "Fohlen", + "Föhnlage", + "Fokus", + "folgen", + "Foliant", + "Folklore", + "Fontäne", + "Förde", + "Forelle", + "Format", + "Forscher", + "Fortgang", + "Forum", + "Fotograf", + "Frachter", + "Fragment", + "Fraktion", + "fräsen", + "Frauenpo", + "Freak", + "Fregatte", + "Freiheit", + "Freude", + "Frieden", + "Frohsinn", + "Frosch", + "Frucht", + "Frühjahr", + "Fuchs", + "Fügung", + "fühlen", + "Füller", + "Fundbüro", + "Funkboje", + "Funzel", + "Furnier", + "Fürsorge", + "Fusel", + "Fußbad", + "Futteral", + "Gabelung", + "gackern", + "Gage", + "gähnen", + "Galaxie", + "Galeere", + "Galopp", + "Gameboy", + "Gamsbart", + "Gandhi", + "Gang", + "Garage", + "Gardine", + "Garküche", + "Garten", + "Gasthaus", + "Gattung", + "gaukeln", + "Gazelle", + "Gebäck", + "Gebirge", + "Gebräu", + "Geburt", + "Gedanke", + "Gedeck", + "Gedicht", + "Gefahr", + "Gefieder", + "Geflügel", + "Gefühl", + "Gegend", + "Gehirn", + "Gehöft", + "Gehweg", + "Geige", + "Geist", + "Gelage", + "Geld", + "Gelenk", + "Gelübde", + "Gemälde", + "Gemeinde", + "Gemüse", + "genesen", + "Genuss", + "Gepäck", + "Geranie", + "Gericht", + "Germane", + "Geruch", + "Gesang", + "Geschenk", + "Gesetz", + "Gesindel", + "Gesöff", + "Gespan", + "Gestade", + "Gesuch", + "Getier", + "Getränk", + "Getümmel", + "Gewand", + "Geweih", + "Gewitter", + "Gewölbe", + "Geysir", + "Giftzahn", + "Gipfel", + "Giraffe", + "Gitarre", + "glänzen", + "Glasauge", + "Glatze", + "Gleis", + "Globus", + "Glück", + "glühen", + "Glutofen", + "Goldzahn", + "Gondel", + "gönnen", + "Gottheit", + "graben", + "Grafik", + "Grashalm", + "Graugans", + "greifen", + "Grenze", + "grillen", + "Groschen", + "Grotte", + "Grube", + "Grünalge", + "Gruppe", + "gruseln", + "Gulasch", + "Gummibär", + "Gurgel", + "Gürtel", + "Güterzug", + "Haarband", + "Habicht", + "hacken", + "hadern", + "Hafen", + "Hagel", + "Hähnchen", + "Haifisch", + "Haken", + "Halbaffe", + "Halsader", + "halten", + "Halunke", + "Handbuch", + "Hanf", + "Harfe", + "Harnisch", + "härten", + "Harz", + "Hasenohr", + "Haube", + "hauchen", + "Haupt", + "Haut", + "Havarie", + "Hebamme", + "hecheln", + "Heck", + "Hedonist", + "Heiler", + "Heimat", + "Heizung", + "Hektik", + "Held", + "helfen", + "Helium", + "Hemd", + "hemmen", + "Hengst", + "Herd", + "Hering", + "Herkunft", + "Hermelin", + "Herrchen", + "Herzdame", + "Heulboje", + "Hexe", + "Hilfe", + "Himbeere", + "Himmel", + "Hingabe", + "hinhören", + "Hinweis", + "Hirsch", + "Hirte", + "Hitzkopf", + "Hobel", + "Hochform", + "Hocker", + "hoffen", + "Hofhund", + "Hofnarr", + "Höhenzug", + "Hohlraum", + "Hölle", + "Holzboot", + "Honig", + "Honorar", + "horchen", + "Hörprobe", + "Höschen", + "Hotel", + "Hubraum", + "Hufeisen", + "Hügel", + "huldigen", + "Hülle", + "Humbug", + "Hummer", + "Humor", + "Hund", + "Hunger", + "Hupe", + "Hürde", + "Hurrikan", + "Hydrant", + "Hypnose", + "Ibis", + "Idee", + "Idiot", + "Igel", + "Illusion", + "Imitat", + "impfen", + "Import", + "Inferno", + "Ingwer", + "Inhalte", + "Inland", + "Insekt", + "Ironie", + "Irrfahrt", + "Irrtum", + "Isolator", + "Istwert", + "Jacke", + "Jade", + "Jagdhund", + "Jäger", + "Jaguar", + "Jahr", + "Jähzorn", + "Jazzfest", + "Jetpilot", + "jobben", + "Jochbein", + "jodeln", + "Jodsalz", + "Jolle", + "Journal", + "Jubel", + "Junge", + "Junimond", + "Jupiter", + "Jutesack", + "Juwel", + "Kabarett", + "Kabine", + "Kabuff", + "Käfer", + "Kaffee", + "Kahlkopf", + "Kaimauer", + "Kajüte", + "Kaktus", + "Kaliber", + "Kaltluft", + "Kamel", + "kämmen", + "Kampagne", + "Kanal", + "Känguru", + "Kanister", + "Kanone", + "Kante", + "Kanu", + "kapern", + "Kapitän", + "Kapuze", + "Karneval", + "Karotte", + "Käsebrot", + "Kasper", + "Kastanie", + "Katalog", + "Kathode", + "Katze", + "kaufen", + "Kaugummi", + "Kauz", + "Kehle", + "Keilerei", + "Keksdose", + "Kellner", + "Keramik", + "Kerze", + "Kessel", + "Kette", + "keuchen", + "kichern", + "Kielboot", + "Kindheit", + "Kinnbart", + "Kinosaal", + "Kiosk", + "Kissen", + "Klammer", + "Klang", + "Klapprad", + "Klartext", + "kleben", + "Klee", + "Kleinod", + "Klima", + "Klingel", + "Klippe", + "Klischee", + "Kloster", + "Klugheit", + "Klüngel", + "kneten", + "Knie", + "Knöchel", + "knüpfen", + "Kobold", + "Kochbuch", + "Kohlrabi", + "Koje", + "Kokosöl", + "Kolibri", + "Kolumne", + "Kombüse", + "Komiker", + "kommen", + "Konto", + "Konzept", + "Kopfkino", + "Kordhose", + "Korken", + "Korsett", + "Kosename", + "Krabbe", + "Krach", + "Kraft", + "Krähe", + "Kralle", + "Krapfen", + "Krater", + "kraulen", + "Kreuz", + "Krokodil", + "Kröte", + "Kugel", + "Kuhhirt", + "Kühnheit", + "Künstler", + "Kurort", + "Kurve", + "Kurzfilm", + "kuscheln", + "küssen", + "Kutter", + "Labor", + "lachen", + "Lackaffe", + "Ladeluke", + "Lagune", + "Laib", + "Lakritze", + "Lammfell", + "Land", + "Langmut", + "Lappalie", + "Last", + "Laterne", + "Latzhose", + "Laubsäge", + "laufen", + "Laune", + "Lausbub", + "Lavasee", + "Leben", + "Leder", + "Leerlauf", + "Lehm", + "Lehrer", + "leihen", + "Lektüre", + "Lenker", + "Lerche", + "Leseecke", + "Leuchter", + "Lexikon", + "Libelle", + "Libido", + "Licht", + "Liebe", + "liefern", + "Liftboy", + "Limonade", + "Lineal", + "Linoleum", + "List", + "Liveband", + "Lobrede", + "locken", + "Löffel", + "Logbuch", + "Logik", + "Lohn", + "Loipe", + "Lokal", + "Lorbeer", + "Lösung", + "löten", + "Lottofee", + "Löwe", + "Luchs", + "Luder", + "Luftpost", + "Luke", + "Lümmel", + "Lunge", + "lutschen", + "Luxus", + "Macht", + "Magazin", + "Magier", + "Magnet", + "mähen", + "Mahlzeit", + "Mahnmal", + "Maibaum", + "Maisbrei", + "Makel", + "malen", + "Mammut", + "Maniküre", + "Mantel", + "Marathon", + "Marder", + "Marine", + "Marke", + "Marmor", + "Märzluft", + "Maske", + "Maßanzug", + "Maßkrug", + "Mastkorb", + "Material", + "Matratze", + "Mauerbau", + "Maulkorb", + "Mäuschen", + "Mäzen", + "Medium", + "Meinung", + "melden", + "Melodie", + "Mensch", + "Merkmal", + "Messe", + "Metall", + "Meteor", + "Methode", + "Metzger", + "Mieze", + "Milchkuh", + "Mimose", + "Minirock", + "Minute", + "mischen", + "Missetat", + "mitgehen", + "Mittag", + "Mixtape", + "Möbel", + "Modul", + "mögen", + "Möhre", + "Molch", + "Moment", + "Monat", + "Mondflug", + "Monitor", + "Monokini", + "Monster", + "Monument", + "Moorhuhn", + "Moos", + "Möpse", + "Moral", + "Mörtel", + "Motiv", + "Motorrad", + "Möwe", + "Mühe", + "Mulatte", + "Müller", + "Mumie", + "Mund", + "Münze", + "Muschel", + "Muster", + "Mythos", + "Nabel", + "Nachtzug", + "Nackedei", + "Nagel", + "Nähe", + "Nähnadel", + "Namen", + "Narbe", + "Narwal", + "Nasenbär", + "Natur", + "Nebel", + "necken", + "Neffe", + "Neigung", + "Nektar", + "Nenner", + "Neptun", + "Nerz", + "Nessel", + "Nestbau", + "Netz", + "Neubau", + "Neuerung", + "Neugier", + "nicken", + "Niere", + "Nilpferd", + "nisten", + "Nocke", + "Nomade", + "Nordmeer", + "Notdurft", + "Notstand", + "Notwehr", + "Nudismus", + "Nuss", + "Nutzhanf", + "Oase", + "Obdach", + "Oberarzt", + "Objekt", + "Oboe", + "Obsthain", + "Ochse", + "Odyssee", + "Ofenholz", + "öffnen", + "Ohnmacht", + "Ohrfeige", + "Ohrwurm", + "Ökologie", + "Oktave", + "Ölberg", + "Olive", + "Ölkrise", + "Omelett", + "Onkel", + "Oper", + "Optiker", + "Orange", + "Orchidee", + "ordnen", + "Orgasmus", + "Orkan", + "Ortskern", + "Ortung", + "Ostasien", + "Ozean", + "Paarlauf", + "Packeis", + "paddeln", + "Paket", + "Palast", + "Pandabär", + "Panik", + "Panorama", + "Panther", + "Papagei", + "Papier", + "Paprika", + "Paradies", + "Parka", + "Parodie", + "Partner", + "Passant", + "Patent", + "Patzer", + "Pause", + "Pavian", + "Pedal", + "Pegel", + "peilen", + "Perle", + "Person", + "Pfad", + "Pfau", + "Pferd", + "Pfleger", + "Physik", + "Pier", + "Pilotwal", + "Pinzette", + "Piste", + "Plakat", + "Plankton", + "Platin", + "Plombe", + "plündern", + "Pobacke", + "Pokal", + "polieren", + "Popmusik", + "Porträt", + "Posaune", + "Postamt", + "Pottwal", + "Pracht", + "Pranke", + "Preis", + "Primat", + "Prinzip", + "Protest", + "Proviant", + "Prüfung", + "Pubertät", + "Pudding", + "Pullover", + "Pulsader", + "Punkt", + "Pute", + "Putsch", + "Puzzle", + "Python", + "quaken", + "Qualle", + "Quark", + "Quellsee", + "Querkopf", + "Quitte", + "Quote", + "Rabauke", + "Rache", + "Radclub", + "Radhose", + "Radio", + "Radtour", + "Rahmen", + "Rampe", + "Randlage", + "Ranzen", + "Rapsöl", + "Raserei", + "rasten", + "Rasur", + "Rätsel", + "Raubtier", + "Raumzeit", + "Rausch", + "Reaktor", + "Realität", + "Rebell", + "Rede", + "Reetdach", + "Regatta", + "Regen", + "Rehkitz", + "Reifen", + "Reim", + "Reise", + "Reizung", + "Rekord", + "Relevanz", + "Rennboot", + "Respekt", + "Restmüll", + "retten", + "Reue", + "Revolte", + "Rhetorik", + "Rhythmus", + "Richtung", + "Riegel", + "Rindvieh", + "Rippchen", + "Ritter", + "Robbe", + "Roboter", + "Rockband", + "Rohdaten", + "Roller", + "Roman", + "röntgen", + "Rose", + "Rosskur", + "Rost", + "Rotahorn", + "Rotglut", + "Rotznase", + "Rubrik", + "Rückweg", + "Rufmord", + "Ruhe", + "Ruine", + "Rumpf", + "Runde", + "Rüstung", + "rütteln", + "Saaltür", + "Saatguts", + "Säbel", + "Sachbuch", + "Sack", + "Saft", + "sagen", + "Sahneeis", + "Salat", + "Salbe", + "Salz", + "Sammlung", + "Samt", + "Sandbank", + "Sanftmut", + "Sardine", + "Satire", + "Sattel", + "Satzbau", + "Sauerei", + "Saum", + "Säure", + "Schall", + "Scheitel", + "Schiff", + "Schlager", + "Schmied", + "Schnee", + "Scholle", + "Schrank", + "Schulbus", + "Schwan", + "Seeadler", + "Seefahrt", + "Seehund", + "Seeufer", + "segeln", + "Sehnerv", + "Seide", + "Seilzug", + "Senf", + "Sessel", + "Seufzer", + "Sexgott", + "Sichtung", + "Signal", + "Silber", + "singen", + "Sinn", + "Sirup", + "Sitzbank", + "Skandal", + "Skikurs", + "Skipper", + "Skizze", + "Smaragd", + "Socke", + "Sohn", + "Sommer", + "Songtext", + "Sorte", + "Spagat", + "Spannung", + "Spargel", + "Specht", + "Speiseöl", + "Spiegel", + "Sport", + "spülen", + "Stadtbus", + "Stall", + "Stärke", + "Stativ", + "staunen", + "Stern", + "Stiftung", + "Stollen", + "Strömung", + "Sturm", + "Substanz", + "Südalpen", + "Sumpf", + "surfen", + "Tabak", + "Tafel", + "Tagebau", + "takeln", + "Taktung", + "Talsohle", + "Tand", + "Tanzbär", + "Tapir", + "Tarantel", + "Tarnname", + "Tasse", + "Tatnacht", + "Tatsache", + "Tatze", + "Taube", + "tauchen", + "Taufpate", + "Taumel", + "Teelicht", + "Teich", + "teilen", + "Tempo", + "Tenor", + "Terrasse", + "Testflug", + "Theater", + "Thermik", + "ticken", + "Tiefflug", + "Tierart", + "Tigerhai", + "Tinte", + "Tischler", + "toben", + "Toleranz", + "Tölpel", + "Tonband", + "Topf", + "Topmodel", + "Torbogen", + "Torlinie", + "Torte", + "Tourist", + "Tragesel", + "trampeln", + "Trapez", + "Traum", + "treffen", + "Trennung", + "Treue", + "Trick", + "trimmen", + "Trödel", + "Trost", + "Trumpf", + "tüfteln", + "Turban", + "Turm", + "Übermut", + "Ufer", + "Uhrwerk", + "umarmen", + "Umbau", + "Umfeld", + "Umgang", + "Umsturz", + "Unart", + "Unfug", + "Unimog", + "Unruhe", + "Unwucht", + "Uranerz", + "Urlaub", + "Urmensch", + "Utopie", + "Vakuum", + "Valuta", + "Vandale", + "Vase", + "Vektor", + "Ventil", + "Verb", + "Verdeck", + "Verfall", + "Vergaser", + "verhexen", + "Verlag", + "Vers", + "Vesper", + "Vieh", + "Viereck", + "Vinyl", + "Virus", + "Vitrine", + "Vollblut", + "Vorbote", + "Vorrat", + "Vorsicht", + "Vulkan", + "Wachstum", + "Wade", + "Wagemut", + "Wahlen", + "Wahrheit", + "Wald", + "Walhai", + "Wallach", + "Walnuss", + "Walzer", + "wandeln", + "Wanze", + "wärmen", + "Warnruf", + "Wäsche", + "Wasser", + "Weberei", + "wechseln", + "Wegegeld", + "wehren", + "Weiher", + "Weinglas", + "Weißbier", + "Weitwurf", + "Welle", + "Weltall", + "Werkbank", + "Werwolf", + "Wetter", + "wiehern", + "Wildgans", + "Wind", + "Wohl", + "Wohnort", + "Wolf", + "Wollust", + "Wortlaut", + "Wrack", + "Wunder", + "Wurfaxt", + "Wurst", + "Yacht", + "Yeti", + "Zacke", + "Zahl", + "zähmen", + "Zahnfee", + "Zäpfchen", + "Zaster", + "Zaumzeug", + "Zebra", + "zeigen", + "Zeitlupe", + "Zellkern", + "Zeltdach", + "Zensor", + "Zerfall", + "Zeug", + "Ziege", + "Zielfoto", + "Zimteis", + "Zobel", + "Zollhund", + "Zombie", + "Zöpfe", + "Zucht", + "Zufahrt", + "Zugfahrt", + "Zugvogel", + "Zündung", + "Zweck", + "Zyklop" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/italian.dart b/cw_wownero/lib/mnemonics/italian.dart new file mode 100644 index 000000000..275f85bf4 --- /dev/null +++ b/cw_wownero/lib/mnemonics/italian.dart @@ -0,0 +1,1630 @@ +class ItalianMnemonics { + static const words = [ + "abbinare", + "abbonato", + "abisso", + "abitare", + "abominio", + "accadere", + "accesso", + "acciaio", + "accordo", + "accumulo", + "acido", + "acqua", + "acrobata", + "acustico", + "adattare", + "addetto", + "addio", + "addome", + "adeguato", + "aderire", + "adorare", + "adottare", + "adozione", + "adulto", + "aereo", + "aerobica", + "affare", + "affetto", + "affidare", + "affogato", + "affronto", + "africano", + "afrodite", + "agenzia", + "aggancio", + "aggeggio", + "aggiunta", + "agio", + "agire", + "agitare", + "aglio", + "agnello", + "agosto", + "aiutare", + "albero", + "albo", + "alce", + "alchimia", + "alcool", + "alfabeto", + "algebra", + "alimento", + "allarme", + "alleanza", + "allievo", + "alloggio", + "alluce", + "alpi", + "alterare", + "altro", + "aluminio", + "amante", + "amarezza", + "ambiente", + "ambrosia", + "america", + "amico", + "ammalare", + "ammirare", + "amnesia", + "amnistia", + "amore", + "ampliare", + "amputare", + "analisi", + "anamnesi", + "ananas", + "anarchia", + "anatra", + "anca", + "ancorato", + "andare", + "androide", + "aneddoto", + "anello", + "angelo", + "angolino", + "anguilla", + "anidride", + "anima", + "annegare", + "anno", + "annuncio", + "anomalia", + "antenna", + "anticipo", + "aperto", + "apostolo", + "appalto", + "appello", + "appiglio", + "applauso", + "appoggio", + "appurare", + "aprile", + "aquila", + "arabo", + "arachidi", + "aragosta", + "arancia", + "arbitrio", + "archivio", + "arco", + "argento", + "argilla", + "aria", + "ariete", + "arma", + "armonia", + "aroma", + "arrivare", + "arrosto", + "arsenale", + "arte", + "artiglio", + "asfalto", + "asfissia", + "asino", + "asparagi", + "aspirina", + "assalire", + "assegno", + "assolto", + "assurdo", + "asta", + "astratto", + "atlante", + "atletica", + "atomo", + "atropina", + "attacco", + "attesa", + "attico", + "atto", + "attrarre", + "auguri", + "aula", + "aumento", + "aurora", + "auspicio", + "autista", + "auto", + "autunno", + "avanzare", + "avarizia", + "avere", + "aviatore", + "avido", + "avorio", + "avvenire", + "avviso", + "avvocato", + "azienda", + "azione", + "azzardo", + "azzurro", + "babbuino", + "bacio", + "badante", + "baffi", + "bagaglio", + "bagliore", + "bagno", + "balcone", + "balena", + "ballare", + "balordo", + "balsamo", + "bambola", + "bancomat", + "banda", + "barato", + "barba", + "barista", + "barriera", + "basette", + "basilico", + "bassista", + "bastare", + "battello", + "bavaglio", + "beccare", + "beduino", + "bellezza", + "bene", + "benzina", + "berretto", + "bestia", + "bevitore", + "bianco", + "bibbia", + "biberon", + "bibita", + "bici", + "bidone", + "bilancia", + "biliardo", + "binario", + "binocolo", + "biologia", + "biondina", + "biopsia", + "biossido", + "birbante", + "birra", + "biscotto", + "bisogno", + "bistecca", + "bivio", + "blindare", + "bloccare", + "bocca", + "bollire", + "bombola", + "bonifico", + "borghese", + "borsa", + "bottino", + "botulino", + "braccio", + "bradipo", + "branco", + "bravo", + "bresaola", + "bretelle", + "brevetto", + "briciola", + "brigante", + "brillare", + "brindare", + "brivido", + "broccoli", + "brontolo", + "bruciare", + "brufolo", + "bucare", + "buddista", + "budino", + "bufera", + "buffo", + "bugiardo", + "buio", + "buono", + "burrone", + "bussola", + "bustina", + "buttare", + "cabernet", + "cabina", + "cacao", + "cacciare", + "cactus", + "cadavere", + "caffe", + "calamari", + "calcio", + "caldaia", + "calmare", + "calunnia", + "calvario", + "calzone", + "cambiare", + "camera", + "camion", + "cammello", + "campana", + "canarino", + "cancello", + "candore", + "cane", + "canguro", + "cannone", + "canoa", + "cantare", + "canzone", + "caos", + "capanna", + "capello", + "capire", + "capo", + "capperi", + "capra", + "capsula", + "caraffa", + "carbone", + "carciofo", + "cardigan", + "carenza", + "caricare", + "carota", + "carrello", + "carta", + "casa", + "cascare", + "caserma", + "cashmere", + "casino", + "cassetta", + "castello", + "catalogo", + "catena", + "catorcio", + "cattivo", + "causa", + "cauzione", + "cavallo", + "caverna", + "caviglia", + "cavo", + "cazzotto", + "celibato", + "cemento", + "cenare", + "centrale", + "ceramica", + "cercare", + "ceretta", + "cerniera", + "certezza", + "cervello", + "cessione", + "cestino", + "cetriolo", + "chiave", + "chiedere", + "chilo", + "chimera", + "chiodo", + "chirurgo", + "chitarra", + "chiudere", + "ciabatta", + "ciao", + "cibo", + "ciccia", + "cicerone", + "ciclone", + "cicogna", + "cielo", + "cifra", + "cigno", + "ciliegia", + "cimitero", + "cinema", + "cinque", + "cintura", + "ciondolo", + "ciotola", + "cipolla", + "cippato", + "circuito", + "cisterna", + "citofono", + "ciuccio", + "civetta", + "civico", + "clausola", + "cliente", + "clima", + "clinica", + "cobra", + "coccole", + "cocktail", + "cocomero", + "codice", + "coesione", + "cogliere", + "cognome", + "colla", + "colomba", + "colpire", + "coltello", + "comando", + "comitato", + "commedia", + "comodino", + "compagna", + "comune", + "concerto", + "condotto", + "conforto", + "congiura", + "coniglio", + "consegna", + "conto", + "convegno", + "coperta", + "copia", + "coprire", + "corazza", + "corda", + "corleone", + "cornice", + "corona", + "corpo", + "corrente", + "corsa", + "cortesia", + "corvo", + "coso", + "costume", + "cotone", + "cottura", + "cozza", + "crampo", + "cratere", + "cravatta", + "creare", + "credere", + "crema", + "crescere", + "crimine", + "criterio", + "croce", + "crollare", + "cronaca", + "crostata", + "croupier", + "cubetto", + "cucciolo", + "cucina", + "cultura", + "cuoco", + "cuore", + "cupido", + "cupola", + "cura", + "curva", + "cuscino", + "custode", + "danzare", + "data", + "decennio", + "decidere", + "decollo", + "dedicare", + "dedurre", + "definire", + "delegare", + "delfino", + "delitto", + "demone", + "dentista", + "denuncia", + "deposito", + "derivare", + "deserto", + "designer", + "destino", + "detonare", + "dettagli", + "diagnosi", + "dialogo", + "diamante", + "diario", + "diavolo", + "dicembre", + "difesa", + "digerire", + "digitare", + "diluvio", + "dinamica", + "dipinto", + "diploma", + "diramare", + "dire", + "dirigere", + "dirupo", + "discesa", + "disdetta", + "disegno", + "disporre", + "dissenso", + "distacco", + "dito", + "ditta", + "diva", + "divenire", + "dividere", + "divorare", + "docente", + "dolcetto", + "dolore", + "domatore", + "domenica", + "dominare", + "donatore", + "donna", + "dorato", + "dormire", + "dorso", + "dosaggio", + "dottore", + "dovere", + "download", + "dragone", + "dramma", + "dubbio", + "dubitare", + "duetto", + "durata", + "ebbrezza", + "eccesso", + "eccitare", + "eclissi", + "economia", + "edera", + "edificio", + "editore", + "edizione", + "educare", + "effetto", + "egitto", + "egiziano", + "elastico", + "elefante", + "eleggere", + "elemento", + "elenco", + "elezione", + "elmetto", + "elogio", + "embrione", + "emergere", + "emettere", + "eminenza", + "emisfero", + "emozione", + "empatia", + "energia", + "enfasi", + "enigma", + "entrare", + "enzima", + "epidemia", + "epilogo", + "episodio", + "epoca", + "equivoco", + "erba", + "erede", + "eroe", + "erotico", + "errore", + "eruzione", + "esaltare", + "esame", + "esaudire", + "eseguire", + "esempio", + "esigere", + "esistere", + "esito", + "esperto", + "espresso", + "essere", + "estasi", + "esterno", + "estrarre", + "eterno", + "etica", + "euforico", + "europa", + "evacuare", + "evasione", + "evento", + "evidenza", + "evitare", + "evolvere", + "fabbrica", + "facciata", + "fagiano", + "fagotto", + "falco", + "fame", + "famiglia", + "fanale", + "fango", + "fantasia", + "farfalla", + "farmacia", + "faro", + "fase", + "fastidio", + "faticare", + "fatto", + "favola", + "febbre", + "femmina", + "femore", + "fenomeno", + "fermata", + "feromoni", + "ferrari", + "fessura", + "festa", + "fiaba", + "fiamma", + "fianco", + "fiat", + "fibbia", + "fidare", + "fieno", + "figa", + "figlio", + "figura", + "filetto", + "filmato", + "filosofo", + "filtrare", + "finanza", + "finestra", + "fingere", + "finire", + "finta", + "finzione", + "fiocco", + "fioraio", + "firewall", + "firmare", + "fisico", + "fissare", + "fittizio", + "fiume", + "flacone", + "flagello", + "flirtare", + "flusso", + "focaccia", + "foglio", + "fognario", + "follia", + "fonderia", + "fontana", + "forbici", + "forcella", + "foresta", + "forgiare", + "formare", + "fornace", + "foro", + "fortuna", + "forzare", + "fosforo", + "fotoni", + "fracasso", + "fragola", + "frantumi", + "fratello", + "frazione", + "freccia", + "freddo", + "frenare", + "fresco", + "friggere", + "frittata", + "frivolo", + "frizione", + "fronte", + "frullato", + "frumento", + "frusta", + "frutto", + "fucile", + "fuggire", + "fulmine", + "fumare", + "funzione", + "fuoco", + "furbizia", + "furgone", + "furia", + "furore", + "fusibile", + "fuso", + "futuro", + "gabbiano", + "galassia", + "gallina", + "gamba", + "gancio", + "garanzia", + "garofano", + "gasolio", + "gatto", + "gazebo", + "gazzetta", + "gelato", + "gemelli", + "generare", + "genitori", + "gennaio", + "geologia", + "germania", + "gestire", + "gettare", + "ghepardo", + "ghiaccio", + "giaccone", + "giaguaro", + "giallo", + "giappone", + "giardino", + "gigante", + "gioco", + "gioiello", + "giorno", + "giovane", + "giraffa", + "giudizio", + "giurare", + "giusto", + "globo", + "gloria", + "glucosio", + "gnocca", + "gocciola", + "godere", + "gomito", + "gomma", + "gonfiare", + "gorilla", + "governo", + "gradire", + "graffiti", + "granchio", + "grappolo", + "grasso", + "grattare", + "gridare", + "grissino", + "grondaia", + "grugnito", + "gruppo", + "guadagno", + "guaio", + "guancia", + "guardare", + "gufo", + "guidare", + "guscio", + "gusto", + "icona", + "idea", + "identico", + "idolo", + "idoneo", + "idrante", + "idrogeno", + "igiene", + "ignoto", + "imbarco", + "immagine", + "immobile", + "imparare", + "impedire", + "impianto", + "importo", + "impresa", + "impulso", + "incanto", + "incendio", + "incidere", + "incontro", + "incrocia", + "incubo", + "indagare", + "indice", + "indotto", + "infanzia", + "inferno", + "infinito", + "infranto", + "ingerire", + "inglese", + "ingoiare", + "ingresso", + "iniziare", + "innesco", + "insalata", + "inserire", + "insicuro", + "insonnia", + "insulto", + "interno", + "introiti", + "invasori", + "inverno", + "invito", + "invocare", + "ipnosi", + "ipocrita", + "ipotesi", + "ironia", + "irrigare", + "iscritto", + "isola", + "ispirare", + "isterico", + "istinto", + "istruire", + "italiano", + "jazz", + "labbra", + "labrador", + "ladro", + "lago", + "lamento", + "lampone", + "lancetta", + "lanterna", + "lapide", + "larva", + "lasagne", + "lasciare", + "lastra", + "latte", + "laurea", + "lavagna", + "lavorare", + "leccare", + "legare", + "leggere", + "lenzuolo", + "leone", + "lepre", + "letargo", + "lettera", + "levare", + "levitare", + "lezione", + "liberare", + "libidine", + "libro", + "licenza", + "lievito", + "limite", + "lince", + "lingua", + "liquore", + "lire", + "listino", + "litigare", + "litro", + "locale", + "lottare", + "lucciola", + "lucidare", + "luglio", + "luna", + "macchina", + "madama", + "madre", + "maestro", + "maggio", + "magico", + "maglione", + "magnolia", + "mago", + "maialino", + "maionese", + "malattia", + "male", + "malloppo", + "mancare", + "mandorla", + "mangiare", + "manico", + "manopola", + "mansarda", + "mantello", + "manubrio", + "manzo", + "mappa", + "mare", + "margine", + "marinaio", + "marmotta", + "marocco", + "martello", + "marzo", + "maschera", + "matrice", + "maturare", + "mazzetta", + "meandri", + "medaglia", + "medico", + "medusa", + "megafono", + "melone", + "membrana", + "menta", + "mercato", + "meritare", + "merluzzo", + "mese", + "mestiere", + "metafora", + "meteo", + "metodo", + "mettere", + "miele", + "miglio", + "miliardo", + "mimetica", + "minatore", + "minuto", + "miracolo", + "mirtillo", + "missile", + "mistero", + "misura", + "mito", + "mobile", + "moda", + "moderare", + "moglie", + "molecola", + "molle", + "momento", + "moneta", + "mongolia", + "monologo", + "montagna", + "morale", + "morbillo", + "mordere", + "mosaico", + "mosca", + "mostro", + "motivare", + "moto", + "mulino", + "mulo", + "muovere", + "muraglia", + "muscolo", + "museo", + "musica", + "mutande", + "nascere", + "nastro", + "natale", + "natura", + "nave", + "navigare", + "negare", + "negozio", + "nemico", + "nero", + "nervo", + "nessuno", + "nettare", + "neutroni", + "neve", + "nevicare", + "nicotina", + "nido", + "nipote", + "nocciola", + "noleggio", + "nome", + "nonno", + "norvegia", + "notare", + "notizia", + "nove", + "nucleo", + "nuda", + "nuotare", + "nutrire", + "obbligo", + "occhio", + "occupare", + "oceano", + "odissea", + "odore", + "offerta", + "officina", + "offrire", + "oggetto", + "oggi", + "olfatto", + "olio", + "oliva", + "ombelico", + "ombrello", + "omuncolo", + "ondata", + "onore", + "opera", + "opinione", + "opuscolo", + "opzione", + "orario", + "orbita", + "orchidea", + "ordine", + "orecchio", + "orgasmo", + "orgoglio", + "origine", + "orologio", + "oroscopo", + "orso", + "oscurare", + "ospedale", + "ospite", + "ossigeno", + "ostacolo", + "ostriche", + "ottenere", + "ottimo", + "ottobre", + "ovest", + "pacco", + "pace", + "pacifico", + "padella", + "pagare", + "pagina", + "pagnotta", + "palazzo", + "palestra", + "palpebre", + "pancetta", + "panfilo", + "panino", + "pannello", + "panorama", + "papa", + "paperino", + "paradiso", + "parcella", + "parente", + "parlare", + "parodia", + "parrucca", + "partire", + "passare", + "pasta", + "patata", + "patente", + "patogeno", + "patriota", + "pausa", + "pazienza", + "peccare", + "pecora", + "pedalare", + "pelare", + "pena", + "pendenza", + "penisola", + "pennello", + "pensare", + "pentirsi", + "percorso", + "perdono", + "perfetto", + "perizoma", + "perla", + "permesso", + "persona", + "pesare", + "pesce", + "peso", + "petardo", + "petrolio", + "pezzo", + "piacere", + "pianeta", + "piastra", + "piatto", + "piazza", + "piccolo", + "piede", + "piegare", + "pietra", + "pigiama", + "pigliare", + "pigrizia", + "pilastro", + "pilota", + "pinguino", + "pioggia", + "piombo", + "pionieri", + "piovra", + "pipa", + "pirata", + "pirolisi", + "piscina", + "pisolino", + "pista", + "pitone", + "piumino", + "pizza", + "plastica", + "platino", + "poesia", + "poiana", + "polaroid", + "polenta", + "polimero", + "pollo", + "polmone", + "polpetta", + "poltrona", + "pomodoro", + "pompa", + "popolo", + "porco", + "porta", + "porzione", + "possesso", + "postino", + "potassio", + "potere", + "poverino", + "pranzo", + "prato", + "prefisso", + "prelievo", + "premio", + "prendere", + "prestare", + "pretesa", + "prezzo", + "primario", + "privacy", + "problema", + "processo", + "prodotto", + "profeta", + "progetto", + "promessa", + "pronto", + "proposta", + "proroga", + "prossimo", + "proteina", + "prova", + "prudenza", + "pubblico", + "pudore", + "pugilato", + "pulire", + "pulsante", + "puntare", + "pupazzo", + "puzzle", + "quaderno", + "qualcuno", + "quarzo", + "quercia", + "quintale", + "rabbia", + "racconto", + "radice", + "raffica", + "ragazza", + "ragione", + "rammento", + "ramo", + "rana", + "randagio", + "rapace", + "rapinare", + "rapporto", + "rasatura", + "ravioli", + "reagire", + "realista", + "reattore", + "reazione", + "recitare", + "recluso", + "record", + "recupero", + "redigere", + "regalare", + "regina", + "regola", + "relatore", + "reliquia", + "remare", + "rendere", + "reparto", + "resina", + "resto", + "rete", + "retorica", + "rettile", + "revocare", + "riaprire", + "ribadire", + "ribelle", + "ricambio", + "ricetta", + "richiamo", + "ricordo", + "ridurre", + "riempire", + "riferire", + "riflesso", + "righello", + "rilancio", + "rilevare", + "rilievo", + "rimanere", + "rimborso", + "rinforzo", + "rinuncia", + "riparo", + "ripetere", + "riposare", + "ripulire", + "risalita", + "riscatto", + "riserva", + "riso", + "rispetto", + "ritaglio", + "ritmo", + "ritorno", + "ritratto", + "rituale", + "riunione", + "riuscire", + "riva", + "robotica", + "rondine", + "rosa", + "rospo", + "rosso", + "rotonda", + "rotta", + "roulotte", + "rubare", + "rubrica", + "ruffiano", + "rumore", + "ruota", + "ruscello", + "sabbia", + "sacco", + "saggio", + "sale", + "salire", + "salmone", + "salto", + "salutare", + "salvia", + "sangue", + "sanzioni", + "sapere", + "sapienza", + "sarcasmo", + "sardine", + "sartoria", + "sbalzo", + "sbarcare", + "sberla", + "sborsare", + "scadenza", + "scafo", + "scala", + "scambio", + "scappare", + "scarpa", + "scatola", + "scelta", + "scena", + "sceriffo", + "scheggia", + "schiuma", + "sciarpa", + "scienza", + "scimmia", + "sciopero", + "scivolo", + "sclerare", + "scolpire", + "sconto", + "scopa", + "scordare", + "scossa", + "scrivere", + "scrupolo", + "scuderia", + "scultore", + "scuola", + "scusare", + "sdraiare", + "secolo", + "sedativo", + "sedere", + "sedia", + "segare", + "segreto", + "seguire", + "semaforo", + "seme", + "senape", + "seno", + "sentiero", + "separare", + "sepolcro", + "sequenza", + "serata", + "serpente", + "servizio", + "sesso", + "seta", + "settore", + "sfamare", + "sfera", + "sfidare", + "sfiorare", + "sfogare", + "sgabello", + "sicuro", + "siepe", + "sigaro", + "silenzio", + "silicone", + "simbiosi", + "simpatia", + "simulare", + "sinapsi", + "sindrome", + "sinergia", + "sinonimo", + "sintonia", + "sirena", + "siringa", + "sistema", + "sito", + "smalto", + "smentire", + "smontare", + "soccorso", + "socio", + "soffitto", + "software", + "soggetto", + "sogliola", + "sognare", + "soldi", + "sole", + "sollievo", + "solo", + "sommario", + "sondare", + "sonno", + "sorpresa", + "sorriso", + "sospiro", + "sostegno", + "sovrano", + "spaccare", + "spada", + "spagnolo", + "spalla", + "sparire", + "spavento", + "spazio", + "specchio", + "spedire", + "spegnere", + "spendere", + "speranza", + "spessore", + "spezzare", + "spiaggia", + "spiccare", + "spiegare", + "spiffero", + "spingere", + "sponda", + "sporcare", + "spostare", + "spremuta", + "spugna", + "spumante", + "spuntare", + "squadra", + "squillo", + "staccare", + "stadio", + "stagione", + "stallone", + "stampa", + "stancare", + "starnuto", + "statura", + "stella", + "stendere", + "sterzo", + "stilista", + "stimolo", + "stinco", + "stiva", + "stoffa", + "storia", + "strada", + "stregone", + "striscia", + "studiare", + "stufa", + "stupendo", + "subire", + "successo", + "sudare", + "suono", + "superare", + "supporto", + "surfista", + "sussurro", + "svelto", + "svenire", + "sviluppo", + "svolta", + "svuotare", + "tabacco", + "tabella", + "tabu", + "tacchino", + "tacere", + "taglio", + "talento", + "tangente", + "tappeto", + "tartufo", + "tassello", + "tastiera", + "tavolo", + "tazza", + "teatro", + "tedesco", + "telaio", + "telefono", + "tema", + "temere", + "tempo", + "tendenza", + "tenebre", + "tensione", + "tentare", + "teologia", + "teorema", + "termica", + "terrazzo", + "teschio", + "tesi", + "tesoro", + "tessera", + "testa", + "thriller", + "tifoso", + "tigre", + "timbrare", + "timido", + "tinta", + "tirare", + "tisana", + "titano", + "titolo", + "toccare", + "togliere", + "topolino", + "torcia", + "torrente", + "tovaglia", + "traffico", + "tragitto", + "training", + "tramonto", + "transito", + "trapezio", + "trasloco", + "trattore", + "trazione", + "treccia", + "tregua", + "treno", + "triciclo", + "tridente", + "trilogia", + "tromba", + "troncare", + "trota", + "trovare", + "trucco", + "tubo", + "tulipano", + "tumulto", + "tunisia", + "tuono", + "turista", + "tuta", + "tutelare", + "tutore", + "ubriaco", + "uccello", + "udienza", + "udito", + "uffa", + "umanoide", + "umore", + "unghia", + "unguento", + "unicorno", + "unione", + "universo", + "uomo", + "uragano", + "uranio", + "urlare", + "uscire", + "utente", + "utilizzo", + "vacanza", + "vacca", + "vaglio", + "vagonata", + "valle", + "valore", + "valutare", + "valvola", + "vampiro", + "vaniglia", + "vanto", + "vapore", + "variante", + "vasca", + "vaselina", + "vassoio", + "vedere", + "vegetale", + "veglia", + "veicolo", + "vela", + "veleno", + "velivolo", + "velluto", + "vendere", + "venerare", + "venire", + "vento", + "veranda", + "verbo", + "verdura", + "vergine", + "verifica", + "vernice", + "vero", + "verruca", + "versare", + "vertebra", + "vescica", + "vespaio", + "vestito", + "vesuvio", + "veterano", + "vetro", + "vetta", + "viadotto", + "viaggio", + "vibrare", + "vicenda", + "vichingo", + "vietare", + "vigilare", + "vigneto", + "villa", + "vincere", + "violino", + "vipera", + "virgola", + "virtuoso", + "visita", + "vita", + "vitello", + "vittima", + "vivavoce", + "vivere", + "viziato", + "voglia", + "volare", + "volpe", + "volto", + "volume", + "vongole", + "voragine", + "vortice", + "votare", + "vulcano", + "vuotare", + "zabaione", + "zaffiro", + "zainetto", + "zampa", + "zanzara", + "zattera", + "zavorra", + "zenzero", + "zero", + "zingaro", + "zittire", + "zoccolo", + "zolfo", + "zombie", + "zucchero" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/japanese.dart b/cw_wownero/lib/mnemonics/japanese.dart new file mode 100644 index 000000000..5d17fdb14 --- /dev/null +++ b/cw_wownero/lib/mnemonics/japanese.dart @@ -0,0 +1,1630 @@ +class JapaneseMnemonics { + static const words = [ + "あいこくしん", + "あいさつ", + "あいだ", + "あおぞら", + "あかちゃん", + "あきる", + "あけがた", + "あける", + "あこがれる", + "あさい", + "あさひ", + "あしあと", + "あじわう", + "あずかる", + "あずき", + "あそぶ", + "あたえる", + "あたためる", + "あたりまえ", + "あたる", + "あつい", + "あつかう", + "あっしゅく", + "あつまり", + "あつめる", + "あてな", + "あてはまる", + "あひる", + "あぶら", + "あぶる", + "あふれる", + "あまい", + "あまど", + "あまやかす", + "あまり", + "あみもの", + "あめりか", + "あやまる", + "あゆむ", + "あらいぐま", + "あらし", + "あらすじ", + "あらためる", + "あらゆる", + "あらわす", + "ありがとう", + "あわせる", + "あわてる", + "あんい", + "あんがい", + "あんこ", + "あんぜん", + "あんてい", + "あんない", + "あんまり", + "いいだす", + "いおん", + "いがい", + "いがく", + "いきおい", + "いきなり", + "いきもの", + "いきる", + "いくじ", + "いくぶん", + "いけばな", + "いけん", + "いこう", + "いこく", + "いこつ", + "いさましい", + "いさん", + "いしき", + "いじゅう", + "いじょう", + "いじわる", + "いずみ", + "いずれ", + "いせい", + "いせえび", + "いせかい", + "いせき", + "いぜん", + "いそうろう", + "いそがしい", + "いだい", + "いだく", + "いたずら", + "いたみ", + "いたりあ", + "いちおう", + "いちじ", + "いちど", + "いちば", + "いちぶ", + "いちりゅう", + "いつか", + "いっしゅん", + "いっせい", + "いっそう", + "いったん", + "いっち", + "いってい", + "いっぽう", + "いてざ", + "いてん", + "いどう", + "いとこ", + "いない", + "いなか", + "いねむり", + "いのち", + "いのる", + "いはつ", + "いばる", + "いはん", + "いびき", + "いひん", + "いふく", + "いへん", + "いほう", + "いみん", + "いもうと", + "いもたれ", + "いもり", + "いやがる", + "いやす", + "いよかん", + "いよく", + "いらい", + "いらすと", + "いりぐち", + "いりょう", + "いれい", + "いれもの", + "いれる", + "いろえんぴつ", + "いわい", + "いわう", + "いわかん", + "いわば", + "いわゆる", + "いんげんまめ", + "いんさつ", + "いんしょう", + "いんよう", + "うえき", + "うえる", + "うおざ", + "うがい", + "うかぶ", + "うかべる", + "うきわ", + "うくらいな", + "うくれれ", + "うけたまわる", + "うけつけ", + "うけとる", + "うけもつ", + "うける", + "うごかす", + "うごく", + "うこん", + "うさぎ", + "うしなう", + "うしろがみ", + "うすい", + "うすぎ", + "うすぐらい", + "うすめる", + "うせつ", + "うちあわせ", + "うちがわ", + "うちき", + "うちゅう", + "うっかり", + "うつくしい", + "うったえる", + "うつる", + "うどん", + "うなぎ", + "うなじ", + "うなずく", + "うなる", + "うねる", + "うのう", + "うぶげ", + "うぶごえ", + "うまれる", + "うめる", + "うもう", + "うやまう", + "うよく", + "うらがえす", + "うらぐち", + "うらない", + "うりあげ", + "うりきれ", + "うるさい", + "うれしい", + "うれゆき", + "うれる", + "うろこ", + "うわき", + "うわさ", + "うんこう", + "うんちん", + "うんてん", + "うんどう", + "えいえん", + "えいが", + "えいきょう", + "えいご", + "えいせい", + "えいぶん", + "えいよう", + "えいわ", + "えおり", + "えがお", + "えがく", + "えきたい", + "えくせる", + "えしゃく", + "えすて", + "えつらん", + "えのぐ", + "えほうまき", + "えほん", + "えまき", + "えもじ", + "えもの", + "えらい", + "えらぶ", + "えりあ", + "えんえん", + "えんかい", + "えんぎ", + "えんげき", + "えんしゅう", + "えんぜつ", + "えんそく", + "えんちょう", + "えんとつ", + "おいかける", + "おいこす", + "おいしい", + "おいつく", + "おうえん", + "おうさま", + "おうじ", + "おうせつ", + "おうたい", + "おうふく", + "おうべい", + "おうよう", + "おえる", + "おおい", + "おおう", + "おおどおり", + "おおや", + "おおよそ", + "おかえり", + "おかず", + "おがむ", + "おかわり", + "おぎなう", + "おきる", + "おくさま", + "おくじょう", + "おくりがな", + "おくる", + "おくれる", + "おこす", + "おこなう", + "おこる", + "おさえる", + "おさない", + "おさめる", + "おしいれ", + "おしえる", + "おじぎ", + "おじさん", + "おしゃれ", + "おそらく", + "おそわる", + "おたがい", + "おたく", + "おだやか", + "おちつく", + "おっと", + "おつり", + "おでかけ", + "おとしもの", + "おとなしい", + "おどり", + "おどろかす", + "おばさん", + "おまいり", + "おめでとう", + "おもいで", + "おもう", + "おもたい", + "おもちゃ", + "おやつ", + "おやゆび", + "およぼす", + "おらんだ", + "おろす", + "おんがく", + "おんけい", + "おんしゃ", + "おんせん", + "おんだん", + "おんちゅう", + "おんどけい", + "かあつ", + "かいが", + "がいき", + "がいけん", + "がいこう", + "かいさつ", + "かいしゃ", + "かいすいよく", + "かいぜん", + "かいぞうど", + "かいつう", + "かいてん", + "かいとう", + "かいふく", + "がいへき", + "かいほう", + "かいよう", + "がいらい", + "かいわ", + "かえる", + "かおり", + "かかえる", + "かがく", + "かがし", + "かがみ", + "かくご", + "かくとく", + "かざる", + "がぞう", + "かたい", + "かたち", + "がちょう", + "がっきゅう", + "がっこう", + "がっさん", + "がっしょう", + "かなざわし", + "かのう", + "がはく", + "かぶか", + "かほう", + "かほご", + "かまう", + "かまぼこ", + "かめれおん", + "かゆい", + "かようび", + "からい", + "かるい", + "かろう", + "かわく", + "かわら", + "がんか", + "かんけい", + "かんこう", + "かんしゃ", + "かんそう", + "かんたん", + "かんち", + "がんばる", + "きあい", + "きあつ", + "きいろ", + "ぎいん", + "きうい", + "きうん", + "きえる", + "きおう", + "きおく", + "きおち", + "きおん", + "きかい", + "きかく", + "きかんしゃ", + "ききて", + "きくばり", + "きくらげ", + "きけんせい", + "きこう", + "きこえる", + "きこく", + "きさい", + "きさく", + "きさま", + "きさらぎ", + "ぎじかがく", + "ぎしき", + "ぎじたいけん", + "ぎじにってい", + "ぎじゅつしゃ", + "きすう", + "きせい", + "きせき", + "きせつ", + "きそう", + "きぞく", + "きぞん", + "きたえる", + "きちょう", + "きつえん", + "ぎっちり", + "きつつき", + "きつね", + "きてい", + "きどう", + "きどく", + "きない", + "きなが", + "きなこ", + "きぬごし", + "きねん", + "きのう", + "きのした", + "きはく", + "きびしい", + "きひん", + "きふく", + "きぶん", + "きぼう", + "きほん", + "きまる", + "きみつ", + "きむずかしい", + "きめる", + "きもだめし", + "きもち", + "きもの", + "きゃく", + "きやく", + "ぎゅうにく", + "きよう", + "きょうりゅう", + "きらい", + "きらく", + "きりん", + "きれい", + "きれつ", + "きろく", + "ぎろん", + "きわめる", + "ぎんいろ", + "きんかくじ", + "きんじょ", + "きんようび", + "ぐあい", + "くいず", + "くうかん", + "くうき", + "くうぐん", + "くうこう", + "ぐうせい", + "くうそう", + "ぐうたら", + "くうふく", + "くうぼ", + "くかん", + "くきょう", + "くげん", + "ぐこう", + "くさい", + "くさき", + "くさばな", + "くさる", + "くしゃみ", + "くしょう", + "くすのき", + "くすりゆび", + "くせげ", + "くせん", + "ぐたいてき", + "くださる", + "くたびれる", + "くちこみ", + "くちさき", + "くつした", + "ぐっすり", + "くつろぐ", + "くとうてん", + "くどく", + "くなん", + "くねくね", + "くのう", + "くふう", + "くみあわせ", + "くみたてる", + "くめる", + "くやくしょ", + "くらす", + "くらべる", + "くるま", + "くれる", + "くろう", + "くわしい", + "ぐんかん", + "ぐんしょく", + "ぐんたい", + "ぐんて", + "けあな", + "けいかく", + "けいけん", + "けいこ", + "けいさつ", + "げいじゅつ", + "けいたい", + "げいのうじん", + "けいれき", + "けいろ", + "けおとす", + "けおりもの", + "げきか", + "げきげん", + "げきだん", + "げきちん", + "げきとつ", + "げきは", + "げきやく", + "げこう", + "げこくじょう", + "げざい", + "けさき", + "げざん", + "けしき", + "けしごむ", + "けしょう", + "げすと", + "けたば", + "けちゃっぷ", + "けちらす", + "けつあつ", + "けつい", + "けつえき", + "けっこん", + "けつじょ", + "けっせき", + "けってい", + "けつまつ", + "げつようび", + "げつれい", + "けつろん", + "げどく", + "けとばす", + "けとる", + "けなげ", + "けなす", + "けなみ", + "けぬき", + "げねつ", + "けねん", + "けはい", + "げひん", + "けぶかい", + "げぼく", + "けまり", + "けみかる", + "けむし", + "けむり", + "けもの", + "けらい", + "けろけろ", + "けわしい", + "けんい", + "けんえつ", + "けんお", + "けんか", + "げんき", + "けんげん", + "けんこう", + "けんさく", + "けんしゅう", + "けんすう", + "げんそう", + "けんちく", + "けんてい", + "けんとう", + "けんない", + "けんにん", + "げんぶつ", + "けんま", + "けんみん", + "けんめい", + "けんらん", + "けんり", + "こあくま", + "こいぬ", + "こいびと", + "ごうい", + "こうえん", + "こうおん", + "こうかん", + "ごうきゅう", + "ごうけい", + "こうこう", + "こうさい", + "こうじ", + "こうすい", + "ごうせい", + "こうそく", + "こうたい", + "こうちゃ", + "こうつう", + "こうてい", + "こうどう", + "こうない", + "こうはい", + "ごうほう", + "ごうまん", + "こうもく", + "こうりつ", + "こえる", + "こおり", + "ごかい", + "ごがつ", + "ごかん", + "こくご", + "こくさい", + "こくとう", + "こくない", + "こくはく", + "こぐま", + "こけい", + "こける", + "ここのか", + "こころ", + "こさめ", + "こしつ", + "こすう", + "こせい", + "こせき", + "こぜん", + "こそだて", + "こたい", + "こたえる", + "こたつ", + "こちょう", + "こっか", + "こつこつ", + "こつばん", + "こつぶ", + "こてい", + "こてん", + "ことがら", + "ことし", + "ことば", + "ことり", + "こなごな", + "こねこね", + "このまま", + "このみ", + "このよ", + "ごはん", + "こひつじ", + "こふう", + "こふん", + "こぼれる", + "ごまあぶら", + "こまかい", + "ごますり", + "こまつな", + "こまる", + "こむぎこ", + "こもじ", + "こもち", + "こもの", + "こもん", + "こやく", + "こやま", + "こゆう", + "こゆび", + "こよい", + "こよう", + "こりる", + "これくしょん", + "ころっけ", + "こわもて", + "こわれる", + "こんいん", + "こんかい", + "こんき", + "こんしゅう", + "こんすい", + "こんだて", + "こんとん", + "こんなん", + "こんびに", + "こんぽん", + "こんまけ", + "こんや", + "こんれい", + "こんわく", + "ざいえき", + "さいかい", + "さいきん", + "ざいげん", + "ざいこ", + "さいしょ", + "さいせい", + "ざいたく", + "ざいちゅう", + "さいてき", + "ざいりょう", + "さうな", + "さかいし", + "さがす", + "さかな", + "さかみち", + "さがる", + "さぎょう", + "さくし", + "さくひん", + "さくら", + "さこく", + "さこつ", + "さずかる", + "ざせき", + "さたん", + "さつえい", + "ざつおん", + "ざっか", + "ざつがく", + "さっきょく", + "ざっし", + "さつじん", + "ざっそう", + "さつたば", + "さつまいも", + "さてい", + "さといも", + "さとう", + "さとおや", + "さとし", + "さとる", + "さのう", + "さばく", + "さびしい", + "さべつ", + "さほう", + "さほど", + "さます", + "さみしい", + "さみだれ", + "さむけ", + "さめる", + "さやえんどう", + "さゆう", + "さよう", + "さよく", + "さらだ", + "ざるそば", + "さわやか", + "さわる", + "さんいん", + "さんか", + "さんきゃく", + "さんこう", + "さんさい", + "ざんしょ", + "さんすう", + "さんせい", + "さんそ", + "さんち", + "さんま", + "さんみ", + "さんらん", + "しあい", + "しあげ", + "しあさって", + "しあわせ", + "しいく", + "しいん", + "しうち", + "しえい", + "しおけ", + "しかい", + "しかく", + "じかん", + "しごと", + "しすう", + "じだい", + "したうけ", + "したぎ", + "したて", + "したみ", + "しちょう", + "しちりん", + "しっかり", + "しつじ", + "しつもん", + "してい", + "してき", + "してつ", + "じてん", + "じどう", + "しなぎれ", + "しなもの", + "しなん", + "しねま", + "しねん", + "しのぐ", + "しのぶ", + "しはい", + "しばかり", + "しはつ", + "しはらい", + "しはん", + "しひょう", + "しふく", + "じぶん", + "しへい", + "しほう", + "しほん", + "しまう", + "しまる", + "しみん", + "しむける", + "じむしょ", + "しめい", + "しめる", + "しもん", + "しゃいん", + "しゃうん", + "しゃおん", + "じゃがいも", + "しやくしょ", + "しゃくほう", + "しゃけん", + "しゃこ", + "しゃざい", + "しゃしん", + "しゃせん", + "しゃそう", + "しゃたい", + "しゃちょう", + "しゃっきん", + "じゃま", + "しゃりん", + "しゃれい", + "じゆう", + "じゅうしょ", + "しゅくはく", + "じゅしん", + "しゅっせき", + "しゅみ", + "しゅらば", + "じゅんばん", + "しょうかい", + "しょくたく", + "しょっけん", + "しょどう", + "しょもつ", + "しらせる", + "しらべる", + "しんか", + "しんこう", + "じんじゃ", + "しんせいじ", + "しんちく", + "しんりん", + "すあげ", + "すあし", + "すあな", + "ずあん", + "すいえい", + "すいか", + "すいとう", + "ずいぶん", + "すいようび", + "すうがく", + "すうじつ", + "すうせん", + "すおどり", + "すきま", + "すくう", + "すくない", + "すける", + "すごい", + "すこし", + "ずさん", + "すずしい", + "すすむ", + "すすめる", + "すっかり", + "ずっしり", + "ずっと", + "すてき", + "すてる", + "すねる", + "すのこ", + "すはだ", + "すばらしい", + "ずひょう", + "ずぶぬれ", + "すぶり", + "すふれ", + "すべて", + "すべる", + "ずほう", + "すぼん", + "すまい", + "すめし", + "すもう", + "すやき", + "すらすら", + "するめ", + "すれちがう", + "すろっと", + "すわる", + "すんぜん", + "すんぽう", + "せあぶら", + "せいかつ", + "せいげん", + "せいじ", + "せいよう", + "せおう", + "せかいかん", + "せきにん", + "せきむ", + "せきゆ", + "せきらんうん", + "せけん", + "せこう", + "せすじ", + "せたい", + "せたけ", + "せっかく", + "せっきゃく", + "ぜっく", + "せっけん", + "せっこつ", + "せっさたくま", + "せつぞく", + "せつだん", + "せつでん", + "せっぱん", + "せつび", + "せつぶん", + "せつめい", + "せつりつ", + "せなか", + "せのび", + "せはば", + "せびろ", + "せぼね", + "せまい", + "せまる", + "せめる", + "せもたれ", + "せりふ", + "ぜんあく", + "せんい", + "せんえい", + "せんか", + "せんきょ", + "せんく", + "せんげん", + "ぜんご", + "せんさい", + "せんしゅ", + "せんすい", + "せんせい", + "せんぞ", + "せんたく", + "せんちょう", + "せんてい", + "せんとう", + "せんぬき", + "せんねん", + "せんぱい", + "ぜんぶ", + "ぜんぽう", + "せんむ", + "せんめんじょ", + "せんもん", + "せんやく", + "せんゆう", + "せんよう", + "ぜんら", + "ぜんりゃく", + "せんれい", + "せんろ", + "そあく", + "そいとげる", + "そいね", + "そうがんきょう", + "そうき", + "そうご", + "そうしん", + "そうだん", + "そうなん", + "そうび", + "そうめん", + "そうり", + "そえもの", + "そえん", + "そがい", + "そげき", + "そこう", + "そこそこ", + "そざい", + "そしな", + "そせい", + "そせん", + "そそぐ", + "そだてる", + "そつう", + "そつえん", + "そっかん", + "そつぎょう", + "そっけつ", + "そっこう", + "そっせん", + "そっと", + "そとがわ", + "そとづら", + "そなえる", + "そなた", + "そふぼ", + "そぼく", + "そぼろ", + "そまつ", + "そまる", + "そむく", + "そむりえ", + "そめる", + "そもそも", + "そよかぜ", + "そらまめ", + "そろう", + "そんかい", + "そんけい", + "そんざい", + "そんしつ", + "そんぞく", + "そんちょう", + "ぞんび", + "ぞんぶん", + "そんみん", + "たあい", + "たいいん", + "たいうん", + "たいえき", + "たいおう", + "だいがく", + "たいき", + "たいぐう", + "たいけん", + "たいこ", + "たいざい", + "だいじょうぶ", + "だいすき", + "たいせつ", + "たいそう", + "だいたい", + "たいちょう", + "たいてい", + "だいどころ", + "たいない", + "たいねつ", + "たいのう", + "たいはん", + "だいひょう", + "たいふう", + "たいへん", + "たいほ", + "たいまつばな", + "たいみんぐ", + "たいむ", + "たいめん", + "たいやき", + "たいよう", + "たいら", + "たいりょく", + "たいる", + "たいわん", + "たうえ", + "たえる", + "たおす", + "たおる", + "たおれる", + "たかい", + "たかね", + "たきび", + "たくさん", + "たこく", + "たこやき", + "たさい", + "たしざん", + "だじゃれ", + "たすける", + "たずさわる", + "たそがれ", + "たたかう", + "たたく", + "ただしい", + "たたみ", + "たちばな", + "だっかい", + "だっきゃく", + "だっこ", + "だっしゅつ", + "だったい", + "たてる", + "たとえる", + "たなばた", + "たにん", + "たぬき", + "たのしみ", + "たはつ", + "たぶん", + "たべる", + "たぼう", + "たまご", + "たまる", + "だむる", + "ためいき", + "ためす", + "ためる", + "たもつ", + "たやすい", + "たよる", + "たらす", + "たりきほんがん", + "たりょう", + "たりる", + "たると", + "たれる", + "たれんと", + "たろっと", + "たわむれる", + "だんあつ", + "たんい", + "たんおん", + "たんか", + "たんき", + "たんけん", + "たんご", + "たんさん", + "たんじょうび", + "だんせい", + "たんそく", + "たんたい", + "だんち", + "たんてい", + "たんとう", + "だんな", + "たんにん", + "だんねつ", + "たんのう", + "たんぴん", + "だんぼう", + "たんまつ", + "たんめい", + "だんれつ", + "だんろ", + "だんわ", + "ちあい", + "ちあん", + "ちいき", + "ちいさい", + "ちえん", + "ちかい", + "ちから", + "ちきゅう", + "ちきん", + "ちけいず", + "ちけん", + "ちこく", + "ちさい", + "ちしき", + "ちしりょう", + "ちせい", + "ちそう", + "ちたい", + "ちたん", + "ちちおや", + "ちつじょ", + "ちてき", + "ちてん", + "ちぬき", + "ちぬり", + "ちのう", + "ちひょう", + "ちへいせん", + "ちほう", + "ちまた", + "ちみつ", + "ちみどろ", + "ちめいど", + "ちゃんこなべ", + "ちゅうい", + "ちゆりょく", + "ちょうし", + "ちょさくけん", + "ちらし", + "ちらみ", + "ちりがみ", + "ちりょう", + "ちるど", + "ちわわ", + "ちんたい", + "ちんもく", + "ついか", + "ついたち", + "つうか", + "つうじょう", + "つうはん", + "つうわ", + "つかう", + "つかれる", + "つくね", + "つくる", + "つけね", + "つける", + "つごう", + "つたえる", + "つづく", + "つつじ", + "つつむ", + "つとめる", + "つながる", + "つなみ", + "つねづね", + "つのる", + "つぶす", + "つまらない", + "つまる", + "つみき", + "つめたい", + "つもり", + "つもる", + "つよい", + "つるぼ", + "つるみく", + "つわもの", + "つわり", + "てあし", + "てあて", + "てあみ", + "ていおん", + "ていか", + "ていき", + "ていけい", + "ていこく", + "ていさつ", + "ていし", + "ていせい", + "ていたい", + "ていど", + "ていねい", + "ていひょう", + "ていへん", + "ていぼう", + "てうち", + "ておくれ", + "てきとう", + "てくび", + "でこぼこ", + "てさぎょう", + "てさげ", + "てすり", + "てそう", + "てちがい", + "てちょう", + "てつがく", + "てつづき", + "でっぱ", + "てつぼう", + "てつや", + "でぬかえ", + "てぬき", + "てぬぐい", + "てのひら", + "てはい", + "てぶくろ", + "てふだ", + "てほどき", + "てほん", + "てまえ", + "てまきずし", + "てみじか", + "てみやげ", + "てらす", + "てれび", + "てわけ", + "てわたし", + "でんあつ", + "てんいん", + "てんかい", + "てんき", + "てんぐ", + "てんけん", + "てんごく", + "てんさい", + "てんし", + "てんすう", + "でんち", + "てんてき", + "てんとう", + "てんない", + "てんぷら", + "てんぼうだい", + "てんめつ", + "てんらんかい", + "でんりょく", + "でんわ", + "どあい", + "といれ", + "どうかん", + "とうきゅう", + "どうぐ", + "とうし", + "とうむぎ", + "とおい", + "とおか", + "とおく", + "とおす", + "とおる", + "とかい", + "とかす", + "ときおり", + "ときどき", + "とくい", + "とくしゅう", + "とくてん", + "とくに", + "とくべつ", + "とけい", + "とける", + "とこや", + "とさか", + "としょかん", + "とそう", + "とたん", + "とちゅう", + "とっきゅう", + "とっくん", + "とつぜん", + "とつにゅう", + "とどける", + "ととのえる", + "とない", + "となえる", + "となり", + "とのさま", + "とばす", + "どぶがわ", + "とほう", + "とまる", + "とめる", + "ともだち", + "ともる", + "どようび", + "とらえる", + "とんかつ", + "どんぶり", + "ないかく", + "ないこう", + "ないしょ", + "ないす", + "ないせん", + "ないそう", + "なおす", + "ながい", + "なくす", + "なげる", + "なこうど", + "なさけ", + "なたでここ", + "なっとう", + "なつやすみ", + "ななおし", + "なにごと", + "なにもの", + "なにわ", + "なのか", + "なふだ", + "なまいき", + "なまえ", + "なまみ", + "なみだ", + "なめらか", + "なめる", + "なやむ", + "ならう", + "ならび", + "ならぶ", + "なれる", + "なわとび", + "なわばり", + "にあう", + "にいがた", + "にうけ", + "におい", + "にかい", + "にがて", + "にきび", + "にくしみ", + "にくまん", + "にげる", + "にさんかたんそ", + "にしき", + "にせもの", + "にちじょう", + "にちようび", + "にっか", + "にっき", + "にっけい", + "にっこう", + "にっさん", + "にっしょく", + "にっすう", + "にっせき", + "にってい", + "になう", + "にほん", + "にまめ", + "にもつ", + "にやり", + "にゅういん", + "にりんしゃ", + "にわとり", + "にんい", + "にんか", + "にんき", + "にんげん", + "にんしき", + "にんずう", + "にんそう", + "にんたい", + "にんち", + "にんてい", + "にんにく", + "にんぷ", + "にんまり", + "にんむ", + "にんめい", + "にんよう", + "ぬいくぎ", + "ぬかす", + "ぬぐいとる", + "ぬぐう", + "ぬくもり", + "ぬすむ", + "ぬまえび", + "ぬめり", + "ぬらす", + "ぬんちゃく", + "ねあげ", + "ねいき", + "ねいる", + "ねいろ", + "ねぐせ", + "ねくたい", + "ねくら", + "ねこぜ", + "ねこむ", + "ねさげ", + "ねすごす", + "ねそべる", + "ねだん", + "ねつい", + "ねっしん", + "ねつぞう", + "ねったいぎょ", + "ねぶそく", + "ねふだ", + "ねぼう", + "ねほりはほり", + "ねまき", + "ねまわし", + "ねみみ", + "ねむい", + "ねむたい", + "ねもと", + "ねらう", + "ねわざ", + "ねんいり", + "ねんおし", + "ねんかん", + "ねんきん", + "ねんぐ", + "ねんざ", + "ねんし", + "ねんちゃく", + "ねんど", + "ねんぴ", + "ねんぶつ", + "ねんまつ", + "ねんりょう", + "ねんれい", + "のいず", + "のおづま", + "のがす", + "のきなみ", + "のこぎり", + "のこす", + "のこる", + "のせる", + "のぞく", + "のぞむ", + "のたまう", + "のちほど", + "のっく", + "のばす", + "のはら", + "のべる", + "のぼる", + "のみもの", + "のやま", + "のらいぬ", + "のらねこ", + "のりもの", + "のりゆき", + "のれん", + "のんき", + "ばあい", + "はあく", + "ばあさん", + "ばいか", + "ばいく", + "はいけん", + "はいご", + "はいしん", + "はいすい", + "はいせん", + "はいそう", + "はいち", + "ばいばい", + "はいれつ", + "はえる", + "はおる", + "はかい", + "ばかり", + "はかる", + "はくしゅ", + "はけん", + "はこぶ", + "はさみ", + "はさん", + "はしご", + "ばしょ", + "はしる", + "はせる", + "ぱそこん", + "はそん", + "はたん", + "はちみつ", + "はつおん", + "はっかく", + "はづき", + "はっきり", + "はっくつ", + "はっけん", + "はっこう", + "はっさん", + "はっしん", + "はったつ", + "はっちゅう", + "はってん", + "はっぴょう", + "はっぽう", + "はなす", + "はなび", + "はにかむ", + "はぶらし", + "はみがき", + "はむかう", + "はめつ", + "はやい", + "はやし", + "はらう", + "はろうぃん", + "はわい", + "はんい", + "はんえい", + "はんおん", + "はんかく", + "はんきょう", + "ばんぐみ", + "はんこ", + "はんしゃ", + "はんすう", + "はんだん", + "ぱんち", + "ぱんつ", + "はんてい", + "はんとし", + "はんのう", + "はんぱ", + "はんぶん", + "はんぺん", + "はんぼうき", + "はんめい", + "はんらん", + "はんろん", + "ひいき", + "ひうん", + "ひえる", + "ひかく", + "ひかり", + "ひかる", + "ひかん", + "ひくい", + "ひけつ", + "ひこうき", + "ひこく", + "ひさい", + "ひさしぶり", + "ひさん", + "びじゅつかん", + "ひしょ" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/portuguese.dart b/cw_wownero/lib/mnemonics/portuguese.dart new file mode 100644 index 000000000..bdd63d3b2 --- /dev/null +++ b/cw_wownero/lib/mnemonics/portuguese.dart @@ -0,0 +1,1630 @@ +class PortugueseMnemonics { + static const words = [ + "abaular", + "abdominal", + "abeto", + "abissinio", + "abjeto", + "ablucao", + "abnegar", + "abotoar", + "abrutalhar", + "absurdo", + "abutre", + "acautelar", + "accessorios", + "acetona", + "achocolatado", + "acirrar", + "acne", + "acovardar", + "acrostico", + "actinomicete", + "acustico", + "adaptavel", + "adeus", + "adivinho", + "adjunto", + "admoestar", + "adnominal", + "adotivo", + "adquirir", + "adriatico", + "adsorcao", + "adutora", + "advogar", + "aerossol", + "afazeres", + "afetuoso", + "afixo", + "afluir", + "afortunar", + "afrouxar", + "aftosa", + "afunilar", + "agentes", + "agito", + "aglutinar", + "aiatola", + "aimore", + "aino", + "aipo", + "airoso", + "ajeitar", + "ajoelhar", + "ajudante", + "ajuste", + "alazao", + "albumina", + "alcunha", + "alegria", + "alexandre", + "alforriar", + "alguns", + "alhures", + "alivio", + "almoxarife", + "alotropico", + "alpiste", + "alquimista", + "alsaciano", + "altura", + "aluviao", + "alvura", + "amazonico", + "ambulatorio", + "ametodico", + "amizades", + "amniotico", + "amovivel", + "amurada", + "anatomico", + "ancorar", + "anexo", + "anfora", + "aniversario", + "anjo", + "anotar", + "ansioso", + "anturio", + "anuviar", + "anverso", + "anzol", + "aonde", + "apaziguar", + "apito", + "aplicavel", + "apoteotico", + "aprimorar", + "aprumo", + "apto", + "apuros", + "aquoso", + "arauto", + "arbusto", + "arduo", + "aresta", + "arfar", + "arguto", + "aritmetico", + "arlequim", + "armisticio", + "aromatizar", + "arpoar", + "arquivo", + "arrumar", + "arsenio", + "arturiano", + "aruaque", + "arvores", + "asbesto", + "ascorbico", + "aspirina", + "asqueroso", + "assustar", + "astuto", + "atazanar", + "ativo", + "atletismo", + "atmosferico", + "atormentar", + "atroz", + "aturdir", + "audivel", + "auferir", + "augusto", + "aula", + "aumento", + "aurora", + "autuar", + "avatar", + "avexar", + "avizinhar", + "avolumar", + "avulso", + "axiomatico", + "azerbaijano", + "azimute", + "azoto", + "azulejo", + "bacteriologista", + "badulaque", + "baforada", + "baixote", + "bajular", + "balzaquiana", + "bambuzal", + "banzo", + "baoba", + "baqueta", + "barulho", + "bastonete", + "batuta", + "bauxita", + "bavaro", + "bazuca", + "bcrepuscular", + "beato", + "beduino", + "begonia", + "behaviorista", + "beisebol", + "belzebu", + "bemol", + "benzido", + "beocio", + "bequer", + "berro", + "besuntar", + "betume", + "bexiga", + "bezerro", + "biatlon", + "biboca", + "bicuspide", + "bidirecional", + "bienio", + "bifurcar", + "bigorna", + "bijuteria", + "bimotor", + "binormal", + "bioxido", + "bipolarizacao", + "biquini", + "birutice", + "bisturi", + "bituca", + "biunivoco", + "bivalve", + "bizarro", + "blasfemo", + "blenorreia", + "blindar", + "bloqueio", + "blusao", + "boazuda", + "bofete", + "bojudo", + "bolso", + "bombordo", + "bonzo", + "botina", + "boquiaberto", + "bostoniano", + "botulismo", + "bourbon", + "bovino", + "boximane", + "bravura", + "brevidade", + "britar", + "broxar", + "bruno", + "bruxuleio", + "bubonico", + "bucolico", + "buda", + "budista", + "bueiro", + "buffer", + "bugre", + "bujao", + "bumerangue", + "burundines", + "busto", + "butique", + "buzios", + "caatinga", + "cabuqui", + "cacunda", + "cafuzo", + "cajueiro", + "camurca", + "canudo", + "caquizeiro", + "carvoeiro", + "casulo", + "catuaba", + "cauterizar", + "cebolinha", + "cedula", + "ceifeiro", + "celulose", + "cerzir", + "cesto", + "cetro", + "ceus", + "cevar", + "chavena", + "cheroqui", + "chita", + "chovido", + "chuvoso", + "ciatico", + "cibernetico", + "cicuta", + "cidreira", + "cientistas", + "cifrar", + "cigarro", + "cilio", + "cimo", + "cinzento", + "cioso", + "cipriota", + "cirurgico", + "cisto", + "citrico", + "ciumento", + "civismo", + "clavicula", + "clero", + "clitoris", + "cluster", + "coaxial", + "cobrir", + "cocota", + "codorniz", + "coexistir", + "cogumelo", + "coito", + "colusao", + "compaixao", + "comutativo", + "contentamento", + "convulsivo", + "coordenativa", + "coquetel", + "correto", + "corvo", + "costureiro", + "cotovia", + "covil", + "cozinheiro", + "cretino", + "cristo", + "crivo", + "crotalo", + "cruzes", + "cubo", + "cucuia", + "cueiro", + "cuidar", + "cujo", + "cultural", + "cunilingua", + "cupula", + "curvo", + "custoso", + "cutucar", + "czarismo", + "dablio", + "dacota", + "dados", + "daguerreotipo", + "daiquiri", + "daltonismo", + "damista", + "dantesco", + "daquilo", + "darwinista", + "dasein", + "dativo", + "deao", + "debutantes", + "decurso", + "deduzir", + "defunto", + "degustar", + "dejeto", + "deltoide", + "demover", + "denunciar", + "deputado", + "deque", + "dervixe", + "desvirtuar", + "deturpar", + "deuteronomio", + "devoto", + "dextrose", + "dezoito", + "diatribe", + "dicotomico", + "didatico", + "dietista", + "difuso", + "digressao", + "diluvio", + "diminuto", + "dinheiro", + "dinossauro", + "dioxido", + "diplomatico", + "dique", + "dirimivel", + "disturbio", + "diurno", + "divulgar", + "dizivel", + "doar", + "dobro", + "docura", + "dodoi", + "doer", + "dogue", + "doloso", + "domo", + "donzela", + "doping", + "dorsal", + "dossie", + "dote", + "doutro", + "doze", + "dravidico", + "dreno", + "driver", + "dropes", + "druso", + "dubnio", + "ducto", + "dueto", + "dulija", + "dundum", + "duodeno", + "duquesa", + "durou", + "duvidoso", + "duzia", + "ebano", + "ebrio", + "eburneo", + "echarpe", + "eclusa", + "ecossistema", + "ectoplasma", + "ecumenismo", + "eczema", + "eden", + "editorial", + "edredom", + "edulcorar", + "efetuar", + "efigie", + "efluvio", + "egiptologo", + "egresso", + "egua", + "einsteiniano", + "eira", + "eivar", + "eixos", + "ejetar", + "elastomero", + "eldorado", + "elixir", + "elmo", + "eloquente", + "elucidativo", + "emaranhar", + "embutir", + "emerito", + "emfa", + "emitir", + "emotivo", + "empuxo", + "emulsao", + "enamorar", + "encurvar", + "enduro", + "enevoar", + "enfurnar", + "enguico", + "enho", + "enigmista", + "enlutar", + "enormidade", + "enpreendimento", + "enquanto", + "enriquecer", + "enrugar", + "entusiastico", + "enunciar", + "envolvimento", + "enxuto", + "enzimatico", + "eolico", + "epiteto", + "epoxi", + "epura", + "equivoco", + "erario", + "erbio", + "ereto", + "erguido", + "erisipela", + "ermo", + "erotizar", + "erros", + "erupcao", + "ervilha", + "esburacar", + "escutar", + "esfuziante", + "esguio", + "esloveno", + "esmurrar", + "esoterismo", + "esperanca", + "espirito", + "espurio", + "essencialmente", + "esturricar", + "esvoacar", + "etario", + "eterno", + "etiquetar", + "etnologo", + "etos", + "etrusco", + "euclidiano", + "euforico", + "eugenico", + "eunuco", + "europio", + "eustaquio", + "eutanasia", + "evasivo", + "eventualidade", + "evitavel", + "evoluir", + "exaustor", + "excursionista", + "exercito", + "exfoliado", + "exito", + "exotico", + "expurgo", + "exsudar", + "extrusora", + "exumar", + "fabuloso", + "facultativo", + "fado", + "fagulha", + "faixas", + "fajuto", + "faltoso", + "famoso", + "fanzine", + "fapesp", + "faquir", + "fartura", + "fastio", + "faturista", + "fausto", + "favorito", + "faxineira", + "fazer", + "fealdade", + "febril", + "fecundo", + "fedorento", + "feerico", + "feixe", + "felicidade", + "felpudo", + "feltro", + "femur", + "fenotipo", + "fervura", + "festivo", + "feto", + "feudo", + "fevereiro", + "fezinha", + "fiasco", + "fibra", + "ficticio", + "fiduciario", + "fiesp", + "fifa", + "figurino", + "fijiano", + "filtro", + "finura", + "fiorde", + "fiquei", + "firula", + "fissurar", + "fitoteca", + "fivela", + "fixo", + "flavio", + "flexor", + "flibusteiro", + "flotilha", + "fluxograma", + "fobos", + "foco", + "fofura", + "foguista", + "foie", + "foliculo", + "fominha", + "fonte", + "forum", + "fosso", + "fotossintese", + "foxtrote", + "fraudulento", + "frevo", + "frivolo", + "frouxo", + "frutose", + "fuba", + "fucsia", + "fugitivo", + "fuinha", + "fujao", + "fulustreco", + "fumo", + "funileiro", + "furunculo", + "fustigar", + "futurologo", + "fuxico", + "fuzue", + "gabriel", + "gado", + "gaelico", + "gafieira", + "gaguejo", + "gaivota", + "gajo", + "galvanoplastico", + "gamo", + "ganso", + "garrucha", + "gastronomo", + "gatuno", + "gaussiano", + "gaviao", + "gaxeta", + "gazeteiro", + "gear", + "geiser", + "geminiano", + "generoso", + "genuino", + "geossinclinal", + "gerundio", + "gestual", + "getulista", + "gibi", + "gigolo", + "gilete", + "ginseng", + "giroscopio", + "glaucio", + "glacial", + "gleba", + "glifo", + "glote", + "glutonia", + "gnostico", + "goela", + "gogo", + "goitaca", + "golpista", + "gomo", + "gonzo", + "gorro", + "gostou", + "goticula", + "gourmet", + "governo", + "gozo", + "graxo", + "grevista", + "grito", + "grotesco", + "gruta", + "guaxinim", + "gude", + "gueto", + "guizo", + "guloso", + "gume", + "guru", + "gustativo", + "grelhado", + "gutural", + "habitue", + "haitiano", + "halterofilista", + "hamburguer", + "hanseniase", + "happening", + "harpista", + "hastear", + "haveres", + "hebreu", + "hectometro", + "hedonista", + "hegira", + "helena", + "helminto", + "hemorroidas", + "henrique", + "heptassilabo", + "hertziano", + "hesitar", + "heterossexual", + "heuristico", + "hexagono", + "hiato", + "hibrido", + "hidrostatico", + "hieroglifo", + "hifenizar", + "higienizar", + "hilario", + "himen", + "hino", + "hippie", + "hirsuto", + "historiografia", + "hitlerista", + "hodometro", + "hoje", + "holograma", + "homus", + "honroso", + "hoquei", + "horto", + "hostilizar", + "hotentote", + "huguenote", + "humilde", + "huno", + "hurra", + "hutu", + "iaia", + "ialorixa", + "iambico", + "iansa", + "iaque", + "iara", + "iatista", + "iberico", + "ibis", + "icar", + "iceberg", + "icosagono", + "idade", + "ideologo", + "idiotice", + "idoso", + "iemenita", + "iene", + "igarape", + "iglu", + "ignorar", + "igreja", + "iguaria", + "iidiche", + "ilativo", + "iletrado", + "ilharga", + "ilimitado", + "ilogismo", + "ilustrissimo", + "imaturo", + "imbuzeiro", + "imerso", + "imitavel", + "imovel", + "imputar", + "imutavel", + "inaveriguavel", + "incutir", + "induzir", + "inextricavel", + "infusao", + "ingua", + "inhame", + "iniquo", + "injusto", + "inning", + "inoxidavel", + "inquisitorial", + "insustentavel", + "intumescimento", + "inutilizavel", + "invulneravel", + "inzoneiro", + "iodo", + "iogurte", + "ioio", + "ionosfera", + "ioruba", + "iota", + "ipsilon", + "irascivel", + "iris", + "irlandes", + "irmaos", + "iroques", + "irrupcao", + "isca", + "isento", + "islandes", + "isotopo", + "isqueiro", + "israelita", + "isso", + "isto", + "iterbio", + "itinerario", + "itrio", + "iuane", + "iugoslavo", + "jabuticabeira", + "jacutinga", + "jade", + "jagunco", + "jainista", + "jaleco", + "jambo", + "jantarada", + "japones", + "jaqueta", + "jarro", + "jasmim", + "jato", + "jaula", + "javel", + "jazz", + "jegue", + "jeitoso", + "jejum", + "jenipapo", + "jeova", + "jequitiba", + "jersei", + "jesus", + "jetom", + "jiboia", + "jihad", + "jilo", + "jingle", + "jipe", + "jocoso", + "joelho", + "joguete", + "joio", + "jojoba", + "jorro", + "jota", + "joule", + "joviano", + "jubiloso", + "judoca", + "jugular", + "juizo", + "jujuba", + "juliano", + "jumento", + "junto", + "jururu", + "justo", + "juta", + "juventude", + "labutar", + "laguna", + "laico", + "lajota", + "lanterninha", + "lapso", + "laquear", + "lastro", + "lauto", + "lavrar", + "laxativo", + "lazer", + "leasing", + "lebre", + "lecionar", + "ledo", + "leguminoso", + "leitura", + "lele", + "lemure", + "lento", + "leonardo", + "leopardo", + "lepton", + "leque", + "leste", + "letreiro", + "leucocito", + "levitico", + "lexicologo", + "lhama", + "lhufas", + "liame", + "licoroso", + "lidocaina", + "liliputiano", + "limusine", + "linotipo", + "lipoproteina", + "liquidos", + "lirismo", + "lisura", + "liturgico", + "livros", + "lixo", + "lobulo", + "locutor", + "lodo", + "logro", + "lojista", + "lombriga", + "lontra", + "loop", + "loquaz", + "lorota", + "losango", + "lotus", + "louvor", + "luar", + "lubrificavel", + "lucros", + "lugubre", + "luis", + "luminoso", + "luneta", + "lustroso", + "luto", + "luvas", + "luxuriante", + "luzeiro", + "maduro", + "maestro", + "mafioso", + "magro", + "maiuscula", + "majoritario", + "malvisto", + "mamute", + "manutencao", + "mapoteca", + "maquinista", + "marzipa", + "masturbar", + "matuto", + "mausoleu", + "mavioso", + "maxixe", + "mazurca", + "meandro", + "mecha", + "medusa", + "mefistofelico", + "megera", + "meirinho", + "melro", + "memorizar", + "menu", + "mequetrefe", + "mertiolate", + "mestria", + "metroviario", + "mexilhao", + "mezanino", + "miau", + "microssegundo", + "midia", + "migratorio", + "mimosa", + "minuto", + "miosotis", + "mirtilo", + "misturar", + "mitzvah", + "miudos", + "mixuruca", + "mnemonico", + "moagem", + "mobilizar", + "modulo", + "moer", + "mofo", + "mogno", + "moita", + "molusco", + "monumento", + "moqueca", + "morubixaba", + "mostruario", + "motriz", + "mouse", + "movivel", + "mozarela", + "muarra", + "muculmano", + "mudo", + "mugir", + "muitos", + "mumunha", + "munir", + "muon", + "muquira", + "murros", + "musselina", + "nacoes", + "nado", + "naftalina", + "nago", + "naipe", + "naja", + "nalgum", + "namoro", + "nanquim", + "napolitano", + "naquilo", + "nascimento", + "nautilo", + "navios", + "nazista", + "nebuloso", + "nectarina", + "nefrologo", + "negus", + "nelore", + "nenufar", + "nepotismo", + "nervura", + "neste", + "netuno", + "neutron", + "nevoeiro", + "newtoniano", + "nexo", + "nhenhenhem", + "nhoque", + "nigeriano", + "niilista", + "ninho", + "niobio", + "niponico", + "niquelar", + "nirvana", + "nisto", + "nitroglicerina", + "nivoso", + "nobreza", + "nocivo", + "noel", + "nogueira", + "noivo", + "nojo", + "nominativo", + "nonuplo", + "noruegues", + "nostalgico", + "noturno", + "nouveau", + "nuanca", + "nublar", + "nucleotideo", + "nudista", + "nulo", + "numismatico", + "nunquinha", + "nupcias", + "nutritivo", + "nuvens", + "oasis", + "obcecar", + "obeso", + "obituario", + "objetos", + "oblongo", + "obnoxio", + "obrigatorio", + "obstruir", + "obtuso", + "obus", + "obvio", + "ocaso", + "occipital", + "oceanografo", + "ocioso", + "oclusivo", + "ocorrer", + "ocre", + "octogono", + "odalisca", + "odisseia", + "odorifico", + "oersted", + "oeste", + "ofertar", + "ofidio", + "oftalmologo", + "ogiva", + "ogum", + "oigale", + "oitavo", + "oitocentos", + "ojeriza", + "olaria", + "oleoso", + "olfato", + "olhos", + "oliveira", + "olmo", + "olor", + "olvidavel", + "ombudsman", + "omeleteira", + "omitir", + "omoplata", + "onanismo", + "ondular", + "oneroso", + "onomatopeico", + "ontologico", + "onus", + "onze", + "opalescente", + "opcional", + "operistico", + "opio", + "oposto", + "oprobrio", + "optometrista", + "opusculo", + "oratorio", + "orbital", + "orcar", + "orfao", + "orixa", + "orla", + "ornitologo", + "orquidea", + "ortorrombico", + "orvalho", + "osculo", + "osmotico", + "ossudo", + "ostrogodo", + "otario", + "otite", + "ouro", + "ousar", + "outubro", + "ouvir", + "ovario", + "overnight", + "oviparo", + "ovni", + "ovoviviparo", + "ovulo", + "oxala", + "oxente", + "oxiuro", + "oxossi", + "ozonizar", + "paciente", + "pactuar", + "padronizar", + "paete", + "pagodeiro", + "paixao", + "pajem", + "paludismo", + "pampas", + "panturrilha", + "papudo", + "paquistanes", + "pastoso", + "patua", + "paulo", + "pauzinhos", + "pavoroso", + "paxa", + "pazes", + "peao", + "pecuniario", + "pedunculo", + "pegaso", + "peixinho", + "pejorativo", + "pelvis", + "penuria", + "pequno", + "petunia", + "pezada", + "piauiense", + "pictorico", + "pierro", + "pigmeu", + "pijama", + "pilulas", + "pimpolho", + "pintura", + "piorar", + "pipocar", + "piqueteiro", + "pirulito", + "pistoleiro", + "pituitaria", + "pivotar", + "pixote", + "pizzaria", + "plistoceno", + "plotar", + "pluviometrico", + "pneumonico", + "poco", + "podridao", + "poetisa", + "pogrom", + "pois", + "polvorosa", + "pomposo", + "ponderado", + "pontudo", + "populoso", + "poquer", + "porvir", + "posudo", + "potro", + "pouso", + "povoar", + "prazo", + "prezar", + "privilegios", + "proximo", + "prussiano", + "pseudopode", + "psoriase", + "pterossauros", + "ptialina", + "ptolemaico", + "pudor", + "pueril", + "pufe", + "pugilista", + "puir", + "pujante", + "pulverizar", + "pumba", + "punk", + "purulento", + "pustula", + "putsch", + "puxe", + "quatrocentos", + "quetzal", + "quixotesco", + "quotizavel", + "rabujice", + "racista", + "radonio", + "rafia", + "ragu", + "rajado", + "ralo", + "rampeiro", + "ranzinza", + "raptor", + "raquitismo", + "raro", + "rasurar", + "ratoeira", + "ravioli", + "razoavel", + "reavivar", + "rebuscar", + "recusavel", + "reduzivel", + "reexposicao", + "refutavel", + "regurgitar", + "reivindicavel", + "rejuvenescimento", + "relva", + "remuneravel", + "renunciar", + "reorientar", + "repuxo", + "requisito", + "resumo", + "returno", + "reutilizar", + "revolvido", + "rezonear", + "riacho", + "ribossomo", + "ricota", + "ridiculo", + "rifle", + "rigoroso", + "rijo", + "rimel", + "rins", + "rios", + "riqueza", + "respeito", + "rissole", + "ritualistico", + "rivalizar", + "rixa", + "robusto", + "rococo", + "rodoviario", + "roer", + "rogo", + "rojao", + "rolo", + "rompimento", + "ronronar", + "roqueiro", + "rorqual", + "rosto", + "rotundo", + "rouxinol", + "roxo", + "royal", + "ruas", + "rucula", + "rudimentos", + "ruela", + "rufo", + "rugoso", + "ruivo", + "rule", + "rumoroso", + "runico", + "ruptura", + "rural", + "rustico", + "rutilar", + "saariano", + "sabujo", + "sacudir", + "sadomasoquista", + "safra", + "sagui", + "sais", + "samurai", + "santuario", + "sapo", + "saquear", + "sartriano", + "saturno", + "saude", + "sauva", + "saveiro", + "saxofonista", + "sazonal", + "scherzo", + "script", + "seara", + "seborreia", + "secura", + "seduzir", + "sefardim", + "seguro", + "seja", + "selvas", + "sempre", + "senzala", + "sepultura", + "sequoia", + "sestercio", + "setuplo", + "seus", + "seviciar", + "sezonismo", + "shalom", + "siames", + "sibilante", + "sicrano", + "sidra", + "sifilitico", + "signos", + "silvo", + "simultaneo", + "sinusite", + "sionista", + "sirio", + "sisudo", + "situar", + "sivan", + "slide", + "slogan", + "soar", + "sobrio", + "socratico", + "sodomizar", + "soerguer", + "software", + "sogro", + "soja", + "solver", + "somente", + "sonso", + "sopro", + "soquete", + "sorveteiro", + "sossego", + "soturno", + "sousafone", + "sovinice", + "sozinho", + "suavizar", + "subverter", + "sucursal", + "sudoriparo", + "sufragio", + "sugestoes", + "suite", + "sujo", + "sultao", + "sumula", + "suntuoso", + "suor", + "supurar", + "suruba", + "susto", + "suturar", + "suvenir", + "tabuleta", + "taco", + "tadjique", + "tafeta", + "tagarelice", + "taitiano", + "talvez", + "tampouco", + "tanzaniano", + "taoista", + "tapume", + "taquion", + "tarugo", + "tascar", + "tatuar", + "tautologico", + "tavola", + "taxionomista", + "tchecoslovaco", + "teatrologo", + "tectonismo", + "tedioso", + "teflon", + "tegumento", + "teixo", + "telurio", + "temporas", + "tenue", + "teosofico", + "tepido", + "tequila", + "terrorista", + "testosterona", + "tetrico", + "teutonico", + "teve", + "texugo", + "tiara", + "tibia", + "tiete", + "tifoide", + "tigresa", + "tijolo", + "tilintar", + "timpano", + "tintureiro", + "tiquete", + "tiroteio", + "tisico", + "titulos", + "tive", + "toar", + "toboga", + "tofu", + "togoles", + "toicinho", + "tolueno", + "tomografo", + "tontura", + "toponimo", + "toquio", + "torvelinho", + "tostar", + "toto", + "touro", + "toxina", + "trazer", + "trezentos", + "trivialidade", + "trovoar", + "truta", + "tuaregue", + "tubular", + "tucano", + "tudo", + "tufo", + "tuiste", + "tulipa", + "tumultuoso", + "tunisino", + "tupiniquim", + "turvo", + "tutu", + "ucraniano", + "udenista", + "ufanista", + "ufologo", + "ugaritico", + "uiste", + "uivo", + "ulceroso", + "ulema", + "ultravioleta", + "umbilical", + "umero", + "umido", + "umlaut", + "unanimidade", + "unesco", + "ungulado", + "unheiro", + "univoco", + "untuoso", + "urano", + "urbano", + "urdir", + "uretra", + "urgente", + "urinol", + "urna", + "urologo", + "urro", + "ursulina", + "urtiga", + "urupe", + "usavel", + "usbeque", + "usei", + "usineiro", + "usurpar", + "utero", + "utilizar", + "utopico", + "uvular", + "uxoricidio", + "vacuo", + "vadio", + "vaguear", + "vaivem", + "valvula", + "vampiro", + "vantajoso", + "vaporoso", + "vaquinha", + "varziano", + "vasto", + "vaticinio", + "vaudeville", + "vazio", + "veado", + "vedico", + "veemente", + "vegetativo", + "veio", + "veja", + "veludo", + "venusiano", + "verdade", + "verve", + "vestuario", + "vetusto", + "vexatorio", + "vezes", + "viavel", + "vibratorio", + "victor", + "vicunha", + "vidros", + "vietnamita", + "vigoroso", + "vilipendiar", + "vime", + "vintem", + "violoncelo", + "viquingue", + "virus", + "visualizar", + "vituperio", + "viuvo", + "vivo", + "vizir", + "voar", + "vociferar", + "vodu", + "vogar", + "voile", + "volver", + "vomito", + "vontade", + "vortice", + "vosso", + "voto", + "vovozinha", + "voyeuse", + "vozes", + "vulva", + "vupt", + "western", + "xadrez", + "xale", + "xampu", + "xango", + "xarope", + "xaual", + "xavante", + "xaxim", + "xenonio", + "xepa", + "xerox", + "xicara", + "xifopago", + "xiita", + "xilogravura", + "xinxim", + "xistoso", + "xixi", + "xodo", + "xogum", + "xucro", + "zabumba", + "zagueiro", + "zambiano", + "zanzar", + "zarpar", + "zebu", + "zefiro", + "zeloso", + "zenite", + "zumbi" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/russian.dart b/cw_wownero/lib/mnemonics/russian.dart new file mode 100644 index 000000000..f10af0ff6 --- /dev/null +++ b/cw_wownero/lib/mnemonics/russian.dart @@ -0,0 +1,1630 @@ +class RussianMnemonics { + static const words = [ + "абажур", + "абзац", + "абонент", + "абрикос", + "абсурд", + "авангард", + "август", + "авиация", + "авоська", + "автор", + "агат", + "агент", + "агитатор", + "агнец", + "агония", + "агрегат", + "адвокат", + "адмирал", + "адрес", + "ажиотаж", + "азарт", + "азбука", + "азот", + "аист", + "айсберг", + "академия", + "аквариум", + "аккорд", + "акробат", + "аксиома", + "актер", + "акула", + "акция", + "алгоритм", + "алебарда", + "аллея", + "алмаз", + "алтарь", + "алфавит", + "алхимик", + "алый", + "альбом", + "алюминий", + "амбар", + "аметист", + "амнезия", + "ампула", + "амфора", + "анализ", + "ангел", + "анекдот", + "анимация", + "анкета", + "аномалия", + "ансамбль", + "антенна", + "апатия", + "апельсин", + "апофеоз", + "аппарат", + "апрель", + "аптека", + "арабский", + "арбуз", + "аргумент", + "арест", + "ария", + "арка", + "армия", + "аромат", + "арсенал", + "артист", + "архив", + "аршин", + "асбест", + "аскетизм", + "аспект", + "ассорти", + "астроном", + "асфальт", + "атака", + "ателье", + "атлас", + "атом", + "атрибут", + "аудитор", + "аукцион", + "аура", + "афера", + "афиша", + "ахинея", + "ацетон", + "аэропорт", + "бабушка", + "багаж", + "бадья", + "база", + "баклажан", + "балкон", + "бампер", + "банк", + "барон", + "бассейн", + "батарея", + "бахрома", + "башня", + "баян", + "бегство", + "бедро", + "бездна", + "бекон", + "белый", + "бензин", + "берег", + "беседа", + "бетонный", + "биатлон", + "библия", + "бивень", + "бигуди", + "бидон", + "бизнес", + "бикини", + "билет", + "бинокль", + "биология", + "биржа", + "бисер", + "битва", + "бицепс", + "благо", + "бледный", + "близкий", + "блок", + "блуждать", + "блюдо", + "бляха", + "бобер", + "богатый", + "бодрый", + "боевой", + "бокал", + "большой", + "борьба", + "босой", + "ботинок", + "боцман", + "бочка", + "боярин", + "брать", + "бревно", + "бригада", + "бросать", + "брызги", + "брюки", + "бублик", + "бугор", + "будущее", + "буква", + "бульвар", + "бумага", + "бунт", + "бурный", + "бусы", + "бутылка", + "буфет", + "бухта", + "бушлат", + "бывалый", + "быль", + "быстрый", + "быть", + "бюджет", + "бюро", + "бюст", + "вагон", + "важный", + "ваза", + "вакцина", + "валюта", + "вампир", + "ванная", + "вариант", + "вассал", + "вата", + "вафля", + "вахта", + "вдова", + "вдыхать", + "ведущий", + "веер", + "вежливый", + "везти", + "веко", + "великий", + "вена", + "верить", + "веселый", + "ветер", + "вечер", + "вешать", + "вещь", + "веяние", + "взаимный", + "взбучка", + "взвод", + "взгляд", + "вздыхать", + "взлетать", + "взмах", + "взнос", + "взор", + "взрыв", + "взывать", + "взятка", + "вибрация", + "визит", + "вилка", + "вино", + "вирус", + "висеть", + "витрина", + "вихрь", + "вишневый", + "включать", + "вкус", + "власть", + "влечь", + "влияние", + "влюблять", + "внешний", + "внимание", + "внук", + "внятный", + "вода", + "воевать", + "вождь", + "воздух", + "войти", + "вокзал", + "волос", + "вопрос", + "ворота", + "восток", + "впадать", + "впускать", + "врач", + "время", + "вручать", + "всадник", + "всеобщий", + "вспышка", + "встреча", + "вторник", + "вулкан", + "вурдалак", + "входить", + "въезд", + "выбор", + "вывод", + "выгодный", + "выделять", + "выезжать", + "выживать", + "вызывать", + "выигрыш", + "вылезать", + "выносить", + "выпивать", + "высокий", + "выходить", + "вычет", + "вышка", + "выяснять", + "вязать", + "вялый", + "гавань", + "гадать", + "газета", + "гаишник", + "галстук", + "гамма", + "гарантия", + "гастроли", + "гвардия", + "гвоздь", + "гектар", + "гель", + "генерал", + "геолог", + "герой", + "гешефт", + "гибель", + "гигант", + "гильза", + "гимн", + "гипотеза", + "гитара", + "глаз", + "глина", + "глоток", + "глубокий", + "глыба", + "глядеть", + "гнать", + "гнев", + "гнить", + "гном", + "гнуть", + "говорить", + "годовой", + "голова", + "гонка", + "город", + "гость", + "готовый", + "граница", + "грех", + "гриб", + "громкий", + "группа", + "грызть", + "грязный", + "губа", + "гудеть", + "гулять", + "гуманный", + "густой", + "гуща", + "давать", + "далекий", + "дама", + "данные", + "дарить", + "дать", + "дача", + "дверь", + "движение", + "двор", + "дебют", + "девушка", + "дедушка", + "дежурный", + "дезертир", + "действие", + "декабрь", + "дело", + "демократ", + "день", + "депутат", + "держать", + "десяток", + "детский", + "дефицит", + "дешевый", + "деятель", + "джаз", + "джинсы", + "джунгли", + "диалог", + "диван", + "диета", + "дизайн", + "дикий", + "динамика", + "диплом", + "директор", + "диск", + "дитя", + "дичь", + "длинный", + "дневник", + "добрый", + "доверие", + "договор", + "дождь", + "доза", + "документ", + "должен", + "домашний", + "допрос", + "дорога", + "доход", + "доцент", + "дочь", + "дощатый", + "драка", + "древний", + "дрожать", + "друг", + "дрянь", + "дубовый", + "дуга", + "дудка", + "дукат", + "дуло", + "думать", + "дупло", + "дурак", + "дуть", + "духи", + "душа", + "дуэт", + "дымить", + "дыня", + "дыра", + "дыханье", + "дышать", + "дьявол", + "дюжина", + "дюйм", + "дюна", + "дядя", + "дятел", + "егерь", + "единый", + "едкий", + "ежевика", + "ежик", + "езда", + "елка", + "емкость", + "ерунда", + "ехать", + "жадный", + "жажда", + "жалеть", + "жанр", + "жара", + "жать", + "жгучий", + "ждать", + "жевать", + "желание", + "жемчуг", + "женщина", + "жертва", + "жесткий", + "жечь", + "живой", + "жидкость", + "жизнь", + "жилье", + "жирный", + "житель", + "журнал", + "жюри", + "забывать", + "завод", + "загадка", + "задача", + "зажечь", + "зайти", + "закон", + "замечать", + "занимать", + "западный", + "зарплата", + "засыпать", + "затрата", + "захват", + "зацепка", + "зачет", + "защита", + "заявка", + "звать", + "звезда", + "звонить", + "звук", + "здание", + "здешний", + "здоровье", + "зебра", + "зевать", + "зеленый", + "земля", + "зенит", + "зеркало", + "зефир", + "зигзаг", + "зима", + "зиять", + "злак", + "злой", + "змея", + "знать", + "зной", + "зодчий", + "золотой", + "зомби", + "зона", + "зоопарк", + "зоркий", + "зрачок", + "зрение", + "зритель", + "зубной", + "зыбкий", + "зять", + "игла", + "иголка", + "играть", + "идея", + "идиот", + "идол", + "идти", + "иерархия", + "избрать", + "известие", + "изгонять", + "издание", + "излагать", + "изменять", + "износ", + "изоляция", + "изрядный", + "изучать", + "изымать", + "изящный", + "икона", + "икра", + "иллюзия", + "имбирь", + "иметь", + "имидж", + "иммунный", + "империя", + "инвестор", + "индивид", + "инерция", + "инженер", + "иномарка", + "институт", + "интерес", + "инфекция", + "инцидент", + "ипподром", + "ирис", + "ирония", + "искать", + "история", + "исходить", + "исчезать", + "итог", + "июль", + "июнь", + "кабинет", + "кавалер", + "кадр", + "казарма", + "кайф", + "кактус", + "калитка", + "камень", + "канал", + "капитан", + "картина", + "касса", + "катер", + "кафе", + "качество", + "каша", + "каюта", + "квартира", + "квинтет", + "квота", + "кедр", + "кекс", + "кенгуру", + "кепка", + "керосин", + "кетчуп", + "кефир", + "кибитка", + "кивнуть", + "кидать", + "километр", + "кино", + "киоск", + "кипеть", + "кирпич", + "кисть", + "китаец", + "класс", + "клетка", + "клиент", + "клоун", + "клуб", + "клык", + "ключ", + "клятва", + "книга", + "кнопка", + "кнут", + "князь", + "кобура", + "ковер", + "коготь", + "кодекс", + "кожа", + "козел", + "койка", + "коктейль", + "колено", + "компания", + "конец", + "копейка", + "короткий", + "костюм", + "котел", + "кофе", + "кошка", + "красный", + "кресло", + "кричать", + "кровь", + "крупный", + "крыша", + "крючок", + "кубок", + "кувшин", + "кудрявый", + "кузов", + "кукла", + "культура", + "кумир", + "купить", + "курс", + "кусок", + "кухня", + "куча", + "кушать", + "кювет", + "лабиринт", + "лавка", + "лагерь", + "ладонь", + "лазерный", + "лайнер", + "лакей", + "лампа", + "ландшафт", + "лапа", + "ларек", + "ласковый", + "лауреат", + "лачуга", + "лаять", + "лгать", + "лебедь", + "левый", + "легкий", + "ледяной", + "лежать", + "лекция", + "лента", + "лепесток", + "лесной", + "лето", + "лечь", + "леший", + "лживый", + "либерал", + "ливень", + "лига", + "лидер", + "ликовать", + "лиловый", + "лимон", + "линия", + "липа", + "лирика", + "лист", + "литр", + "лифт", + "лихой", + "лицо", + "личный", + "лишний", + "лобовой", + "ловить", + "логика", + "лодка", + "ложка", + "лозунг", + "локоть", + "ломать", + "лоно", + "лопата", + "лорд", + "лось", + "лоток", + "лохматый", + "лошадь", + "лужа", + "лукавый", + "луна", + "лупить", + "лучший", + "лыжный", + "лысый", + "львиный", + "льгота", + "льдина", + "любить", + "людской", + "люстра", + "лютый", + "лягушка", + "магазин", + "мадам", + "мазать", + "майор", + "максимум", + "мальчик", + "манера", + "март", + "масса", + "мать", + "мафия", + "махать", + "мачта", + "машина", + "маэстро", + "маяк", + "мгла", + "мебель", + "медведь", + "мелкий", + "мемуары", + "менять", + "мера", + "место", + "метод", + "механизм", + "мечтать", + "мешать", + "миграция", + "мизинец", + "микрофон", + "миллион", + "минута", + "мировой", + "миссия", + "митинг", + "мишень", + "младший", + "мнение", + "мнимый", + "могила", + "модель", + "мозг", + "мойка", + "мокрый", + "молодой", + "момент", + "монах", + "море", + "мост", + "мотор", + "мохнатый", + "мочь", + "мошенник", + "мощный", + "мрачный", + "мстить", + "мудрый", + "мужчина", + "музыка", + "мука", + "мумия", + "мундир", + "муравей", + "мусор", + "мутный", + "муфта", + "муха", + "мучить", + "мушкетер", + "мыло", + "мысль", + "мыть", + "мычать", + "мышь", + "мэтр", + "мюзикл", + "мягкий", + "мякиш", + "мясо", + "мятый", + "мячик", + "набор", + "навык", + "нагрузка", + "надежда", + "наемный", + "нажать", + "называть", + "наивный", + "накрыть", + "налог", + "намерен", + "наносить", + "написать", + "народ", + "натура", + "наука", + "нация", + "начать", + "небо", + "невеста", + "негодяй", + "неделя", + "нежный", + "незнание", + "нелепый", + "немалый", + "неправда", + "нервный", + "нести", + "нефть", + "нехватка", + "нечистый", + "неясный", + "нива", + "нижний", + "низкий", + "никель", + "нирвана", + "нить", + "ничья", + "ниша", + "нищий", + "новый", + "нога", + "ножницы", + "ноздря", + "ноль", + "номер", + "норма", + "нота", + "ночь", + "ноша", + "ноябрь", + "нрав", + "нужный", + "нутро", + "нынешний", + "нырнуть", + "ныть", + "нюанс", + "нюхать", + "няня", + "оазис", + "обаяние", + "обвинять", + "обгонять", + "обещать", + "обжигать", + "обзор", + "обида", + "область", + "обмен", + "обнимать", + "оборона", + "образ", + "обучение", + "обходить", + "обширный", + "общий", + "объект", + "обычный", + "обязать", + "овальный", + "овес", + "овощи", + "овраг", + "овца", + "овчарка", + "огненный", + "огонь", + "огромный", + "огурец", + "одежда", + "одинокий", + "одобрить", + "ожидать", + "ожог", + "озарение", + "озеро", + "означать", + "оказать", + "океан", + "оклад", + "окно", + "округ", + "октябрь", + "окурок", + "олень", + "опасный", + "операция", + "описать", + "оплата", + "опора", + "оппонент", + "опрос", + "оптимизм", + "опускать", + "опыт", + "орать", + "орбита", + "орган", + "орден", + "орел", + "оригинал", + "оркестр", + "орнамент", + "оружие", + "осадок", + "освещать", + "осень", + "осина", + "осколок", + "осмотр", + "основной", + "особый", + "осуждать", + "отбор", + "отвечать", + "отдать", + "отец", + "отзыв", + "открытие", + "отмечать", + "относить", + "отпуск", + "отрасль", + "отставка", + "оттенок", + "отходить", + "отчет", + "отъезд", + "офицер", + "охапка", + "охота", + "охрана", + "оценка", + "очаг", + "очередь", + "очищать", + "очки", + "ошейник", + "ошибка", + "ощущение", + "павильон", + "падать", + "паек", + "пакет", + "палец", + "память", + "панель", + "папка", + "партия", + "паспорт", + "патрон", + "пауза", + "пафос", + "пахнуть", + "пациент", + "пачка", + "пашня", + "певец", + "педагог", + "пейзаж", + "пельмень", + "пенсия", + "пепел", + "период", + "песня", + "петля", + "пехота", + "печать", + "пешеход", + "пещера", + "пианист", + "пиво", + "пиджак", + "пиковый", + "пилот", + "пионер", + "пирог", + "писать", + "пить", + "пицца", + "пишущий", + "пища", + "план", + "плечо", + "плита", + "плохой", + "плыть", + "плюс", + "пляж", + "победа", + "повод", + "погода", + "подумать", + "поехать", + "пожимать", + "позиция", + "поиск", + "покой", + "получать", + "помнить", + "пони", + "поощрять", + "попадать", + "порядок", + "пост", + "поток", + "похожий", + "поцелуй", + "почва", + "пощечина", + "поэт", + "пояснить", + "право", + "предмет", + "проблема", + "пруд", + "прыгать", + "прямой", + "психолог", + "птица", + "публика", + "пугать", + "пудра", + "пузырь", + "пуля", + "пункт", + "пурга", + "пустой", + "путь", + "пухлый", + "пучок", + "пушистый", + "пчела", + "пшеница", + "пыль", + "пытка", + "пыхтеть", + "пышный", + "пьеса", + "пьяный", + "пятно", + "работа", + "равный", + "радость", + "развитие", + "район", + "ракета", + "рамка", + "ранний", + "рапорт", + "рассказ", + "раунд", + "рация", + "рвать", + "реальный", + "ребенок", + "реветь", + "регион", + "редакция", + "реестр", + "режим", + "резкий", + "рейтинг", + "река", + "религия", + "ремонт", + "рента", + "реплика", + "ресурс", + "реформа", + "рецепт", + "речь", + "решение", + "ржавый", + "рисунок", + "ритм", + "рифма", + "робкий", + "ровный", + "рогатый", + "родитель", + "рождение", + "розовый", + "роковой", + "роль", + "роман", + "ронять", + "рост", + "рота", + "роща", + "рояль", + "рубль", + "ругать", + "руда", + "ружье", + "руины", + "рука", + "руль", + "румяный", + "русский", + "ручка", + "рыба", + "рывок", + "рыдать", + "рыжий", + "рынок", + "рысь", + "рыть", + "рыхлый", + "рыцарь", + "рычаг", + "рюкзак", + "рюмка", + "рябой", + "рядовой", + "сабля", + "садовый", + "сажать", + "салон", + "самолет", + "сани", + "сапог", + "сарай", + "сатира", + "сауна", + "сахар", + "сбегать", + "сбивать", + "сбор", + "сбыт", + "свадьба", + "свет", + "свидание", + "свобода", + "связь", + "сгорать", + "сдвигать", + "сеанс", + "северный", + "сегмент", + "седой", + "сезон", + "сейф", + "секунда", + "сельский", + "семья", + "сентябрь", + "сердце", + "сеть", + "сечение", + "сеять", + "сигнал", + "сидеть", + "сизый", + "сила", + "символ", + "синий", + "сирота", + "система", + "ситуация", + "сиять", + "сказать", + "скважина", + "скелет", + "скидка", + "склад", + "скорый", + "скрывать", + "скучный", + "слава", + "слеза", + "слияние", + "слово", + "случай", + "слышать", + "слюна", + "смех", + "смирение", + "смотреть", + "смутный", + "смысл", + "смятение", + "снаряд", + "снег", + "снижение", + "сносить", + "снять", + "событие", + "совет", + "согласие", + "сожалеть", + "сойти", + "сокол", + "солнце", + "сомнение", + "сонный", + "сообщать", + "соперник", + "сорт", + "состав", + "сотня", + "соус", + "социолог", + "сочинять", + "союз", + "спать", + "спешить", + "спина", + "сплошной", + "способ", + "спутник", + "средство", + "срок", + "срывать", + "стать", + "ствол", + "стена", + "стихи", + "сторона", + "страна", + "студент", + "стыд", + "субъект", + "сувенир", + "сугроб", + "судьба", + "суета", + "суждение", + "сукно", + "сулить", + "сумма", + "сунуть", + "супруг", + "суровый", + "сустав", + "суть", + "сухой", + "суша", + "существо", + "сфера", + "схема", + "сцена", + "счастье", + "счет", + "считать", + "сшивать", + "съезд", + "сынок", + "сыпать", + "сырье", + "сытый", + "сыщик", + "сюжет", + "сюрприз", + "таблица", + "таежный", + "таинство", + "тайна", + "такси", + "талант", + "таможня", + "танец", + "тарелка", + "таскать", + "тахта", + "тачка", + "таять", + "тварь", + "твердый", + "творить", + "театр", + "тезис", + "текст", + "тело", + "тема", + "тень", + "теория", + "теплый", + "терять", + "тесный", + "тетя", + "техника", + "течение", + "тигр", + "типичный", + "тираж", + "титул", + "тихий", + "тишина", + "ткань", + "товарищ", + "толпа", + "тонкий", + "топливо", + "торговля", + "тоска", + "точка", + "тощий", + "традиция", + "тревога", + "трибуна", + "трогать", + "труд", + "трюк", + "тряпка", + "туалет", + "тугой", + "туловище", + "туман", + "тундра", + "тупой", + "турнир", + "тусклый", + "туфля", + "туча", + "туша", + "тыкать", + "тысяча", + "тьма", + "тюльпан", + "тюрьма", + "тяга", + "тяжелый", + "тянуть", + "убеждать", + "убирать", + "убогий", + "убыток", + "уважение", + "уверять", + "увлекать", + "угнать", + "угол", + "угроза", + "удар", + "удивлять", + "удобный", + "уезд", + "ужас", + "ужин", + "узел", + "узкий", + "узнавать", + "узор", + "уйма", + "уклон", + "укол", + "уксус", + "улетать", + "улица", + "улучшать", + "улыбка", + "уметь", + "умиление", + "умный", + "умолять", + "умысел", + "унижать", + "уносить", + "уныние", + "упасть", + "уплата", + "упор", + "упрекать", + "упускать", + "уран", + "урна", + "уровень", + "усадьба", + "усердие", + "усилие", + "ускорять", + "условие", + "усмешка", + "уснуть", + "успеть", + "усыпать", + "утешать", + "утка", + "уточнять", + "утро", + "утюг", + "уходить", + "уцелеть", + "участие", + "ученый", + "учитель", + "ушко", + "ущерб", + "уютный", + "уяснять", + "фабрика", + "фаворит", + "фаза", + "файл", + "факт", + "фамилия", + "фантазия", + "фара", + "фасад", + "февраль", + "фельдшер", + "феномен", + "ферма", + "фигура", + "физика", + "фильм", + "финал", + "фирма", + "фишка", + "флаг", + "флейта", + "флот", + "фокус", + "фольклор", + "фонд", + "форма", + "фото", + "фраза", + "фреска", + "фронт", + "фрукт", + "функция", + "фуражка", + "футбол", + "фыркать", + "халат", + "хамство", + "хаос", + "характер", + "хата", + "хватать", + "хвост", + "хижина", + "хилый", + "химия", + "хирург", + "хитрый", + "хищник", + "хлам", + "хлеб", + "хлопать", + "хмурый", + "ходить", + "хозяин", + "хоккей", + "холодный", + "хороший", + "хотеть", + "хохотать", + "храм", + "хрен", + "хриплый", + "хроника", + "хрупкий", + "художник", + "хулиган", + "хутор", + "царь", + "цвет", + "цель", + "цемент", + "центр", + "цепь", + "церковь", + "цикл", + "цилиндр", + "циничный", + "цирк", + "цистерна", + "цитата", + "цифра", + "цыпленок", + "чадо", + "чайник", + "часть", + "чашка", + "человек", + "чемодан", + "чепуха", + "черный", + "честь", + "четкий", + "чехол", + "чиновник", + "число", + "читать", + "членство", + "чреватый", + "чтение", + "чувство", + "чугунный", + "чудо", + "чужой", + "чукча", + "чулок", + "чума", + "чуткий", + "чучело", + "чушь", + "шаблон", + "шагать", + "шайка", + "шакал", + "шалаш", + "шампунь", + "шанс", + "шапка", + "шарик", + "шасси", + "шатер", + "шахта", + "шашлык", + "швейный", + "швырять", + "шевелить", + "шедевр", + "шейка", + "шелковый", + "шептать", + "шерсть", + "шестерка", + "шикарный", + "шинель", + "шипеть", + "широкий", + "шить", + "шишка", + "шкаф", + "школа", + "шкура", + "шланг", + "шлем", + "шлюпка", + "шляпа", + "шнур", + "шоколад", + "шорох", + "шоссе", + "шофер", + "шпага", + "шпион", + "шприц", + "шрам", + "шрифт", + "штаб", + "штора", + "штраф", + "штука", + "штык", + "шуба", + "шуметь", + "шуршать", + "шутка", + "щадить", + "щедрый", + "щека", + "щель", + "щенок", + "щепка", + "щетка", + "щука", + "эволюция", + "эгоизм", + "экзамен", + "экипаж", + "экономия", + "экран", + "эксперт", + "элемент", + "элита", + "эмблема", + "эмигрант", + "эмоция", + "энергия", + "эпизод", + "эпоха", + "эскиз", + "эссе", + "эстрада", + "этап", + "этика", + "этюд", + "эфир", + "эффект", + "эшелон", + "юбилей", + "юбка", + "южный", + "юмор", + "юноша", + "юрист", + "яблоко", + "явление", + "ягода", + "ядерный", + "ядовитый", + "ядро", + "язва", + "язык", + "яйцо", + "якорь", + "январь", + "японец", + "яркий", + "ярмарка", + "ярость", + "ярус", + "ясный", + "яхта", + "ячейка", + "ящик" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mnemonics/spanish.dart b/cw_wownero/lib/mnemonics/spanish.dart new file mode 100644 index 000000000..531eafd35 --- /dev/null +++ b/cw_wownero/lib/mnemonics/spanish.dart @@ -0,0 +1,1630 @@ +class SpanishMnemonics { + static const words = [ + "ábaco", + "abdomen", + "abeja", + "abierto", + "abogado", + "abono", + "aborto", + "abrazo", + "abrir", + "abuelo", + "abuso", + "acabar", + "academia", + "acceso", + "acción", + "aceite", + "acelga", + "acento", + "aceptar", + "ácido", + "aclarar", + "acné", + "acoger", + "acoso", + "activo", + "acto", + "actriz", + "actuar", + "acudir", + "acuerdo", + "acusar", + "adicto", + "admitir", + "adoptar", + "adorno", + "aduana", + "adulto", + "aéreo", + "afectar", + "afición", + "afinar", + "afirmar", + "ágil", + "agitar", + "agonía", + "agosto", + "agotar", + "agregar", + "agrio", + "agua", + "agudo", + "águila", + "aguja", + "ahogo", + "ahorro", + "aire", + "aislar", + "ajedrez", + "ajeno", + "ajuste", + "alacrán", + "alambre", + "alarma", + "alba", + "álbum", + "alcalde", + "aldea", + "alegre", + "alejar", + "alerta", + "aleta", + "alfiler", + "alga", + "algodón", + "aliado", + "aliento", + "alivio", + "alma", + "almeja", + "almíbar", + "altar", + "alteza", + "altivo", + "alto", + "altura", + "alumno", + "alzar", + "amable", + "amante", + "amapola", + "amargo", + "amasar", + "ámbar", + "ámbito", + "ameno", + "amigo", + "amistad", + "amor", + "amparo", + "amplio", + "ancho", + "anciano", + "ancla", + "andar", + "andén", + "anemia", + "ángulo", + "anillo", + "ánimo", + "anís", + "anotar", + "antena", + "antiguo", + "antojo", + "anual", + "anular", + "anuncio", + "añadir", + "añejo", + "año", + "apagar", + "aparato", + "apetito", + "apio", + "aplicar", + "apodo", + "aporte", + "apoyo", + "aprender", + "aprobar", + "apuesta", + "apuro", + "arado", + "araña", + "arar", + "árbitro", + "árbol", + "arbusto", + "archivo", + "arco", + "arder", + "ardilla", + "arduo", + "área", + "árido", + "aries", + "armonía", + "arnés", + "aroma", + "arpa", + "arpón", + "arreglo", + "arroz", + "arruga", + "arte", + "artista", + "asa", + "asado", + "asalto", + "ascenso", + "asegurar", + "aseo", + "asesor", + "asiento", + "asilo", + "asistir", + "asno", + "asombro", + "áspero", + "astilla", + "astro", + "astuto", + "asumir", + "asunto", + "atajo", + "ataque", + "atar", + "atento", + "ateo", + "ático", + "atleta", + "átomo", + "atraer", + "atroz", + "atún", + "audaz", + "audio", + "auge", + "aula", + "aumento", + "ausente", + "autor", + "aval", + "avance", + "avaro", + "ave", + "avellana", + "avena", + "avestruz", + "avión", + "aviso", + "ayer", + "ayuda", + "ayuno", + "azafrán", + "azar", + "azote", + "azúcar", + "azufre", + "azul", + "baba", + "babor", + "bache", + "bahía", + "baile", + "bajar", + "balanza", + "balcón", + "balde", + "bambú", + "banco", + "banda", + "baño", + "barba", + "barco", + "barniz", + "barro", + "báscula", + "bastón", + "basura", + "batalla", + "batería", + "batir", + "batuta", + "baúl", + "bazar", + "bebé", + "bebida", + "bello", + "besar", + "beso", + "bestia", + "bicho", + "bien", + "bingo", + "blanco", + "bloque", + "blusa", + "boa", + "bobina", + "bobo", + "boca", + "bocina", + "boda", + "bodega", + "boina", + "bola", + "bolero", + "bolsa", + "bomba", + "bondad", + "bonito", + "bono", + "bonsái", + "borde", + "borrar", + "bosque", + "bote", + "botín", + "bóveda", + "bozal", + "bravo", + "brazo", + "brecha", + "breve", + "brillo", + "brinco", + "brisa", + "broca", + "broma", + "bronce", + "brote", + "bruja", + "brusco", + "bruto", + "buceo", + "bucle", + "bueno", + "buey", + "bufanda", + "bufón", + "búho", + "buitre", + "bulto", + "burbuja", + "burla", + "burro", + "buscar", + "butaca", + "buzón", + "caballo", + "cabeza", + "cabina", + "cabra", + "cacao", + "cadáver", + "cadena", + "caer", + "café", + "caída", + "caimán", + "caja", + "cajón", + "cal", + "calamar", + "calcio", + "caldo", + "calidad", + "calle", + "calma", + "calor", + "calvo", + "cama", + "cambio", + "camello", + "camino", + "campo", + "cáncer", + "candil", + "canela", + "canguro", + "canica", + "canto", + "caña", + "cañón", + "caoba", + "caos", + "capaz", + "capitán", + "capote", + "captar", + "capucha", + "cara", + "carbón", + "cárcel", + "careta", + "carga", + "cariño", + "carne", + "carpeta", + "carro", + "carta", + "casa", + "casco", + "casero", + "caspa", + "castor", + "catorce", + "catre", + "caudal", + "causa", + "cazo", + "cebolla", + "ceder", + "cedro", + "celda", + "célebre", + "celoso", + "célula", + "cemento", + "ceniza", + "centro", + "cerca", + "cerdo", + "cereza", + "cero", + "cerrar", + "certeza", + "césped", + "cetro", + "chacal", + "chaleco", + "champú", + "chancla", + "chapa", + "charla", + "chico", + "chiste", + "chivo", + "choque", + "choza", + "chuleta", + "chupar", + "ciclón", + "ciego", + "cielo", + "cien", + "cierto", + "cifra", + "cigarro", + "cima", + "cinco", + "cine", + "cinta", + "ciprés", + "circo", + "ciruela", + "cisne", + "cita", + "ciudad", + "clamor", + "clan", + "claro", + "clase", + "clave", + "cliente", + "clima", + "clínica", + "cobre", + "cocción", + "cochino", + "cocina", + "coco", + "código", + "codo", + "cofre", + "coger", + "cohete", + "cojín", + "cojo", + "cola", + "colcha", + "colegio", + "colgar", + "colina", + "collar", + "colmo", + "columna", + "combate", + "comer", + "comida", + "cómodo", + "compra", + "conde", + "conejo", + "conga", + "conocer", + "consejo", + "contar", + "copa", + "copia", + "corazón", + "corbata", + "corcho", + "cordón", + "corona", + "correr", + "coser", + "cosmos", + "costa", + "cráneo", + "cráter", + "crear", + "crecer", + "creído", + "crema", + "cría", + "crimen", + "cripta", + "crisis", + "cromo", + "crónica", + "croqueta", + "crudo", + "cruz", + "cuadro", + "cuarto", + "cuatro", + "cubo", + "cubrir", + "cuchara", + "cuello", + "cuento", + "cuerda", + "cuesta", + "cueva", + "cuidar", + "culebra", + "culpa", + "culto", + "cumbre", + "cumplir", + "cuna", + "cuneta", + "cuota", + "cupón", + "cúpula", + "curar", + "curioso", + "curso", + "curva", + "cutis", + "dama", + "danza", + "dar", + "dardo", + "dátil", + "deber", + "débil", + "década", + "decir", + "dedo", + "defensa", + "definir", + "dejar", + "delfín", + "delgado", + "delito", + "demora", + "denso", + "dental", + "deporte", + "derecho", + "derrota", + "desayuno", + "deseo", + "desfile", + "desnudo", + "destino", + "desvío", + "detalle", + "detener", + "deuda", + "día", + "diablo", + "diadema", + "diamante", + "diana", + "diario", + "dibujo", + "dictar", + "diente", + "dieta", + "diez", + "difícil", + "digno", + "dilema", + "diluir", + "dinero", + "directo", + "dirigir", + "disco", + "diseño", + "disfraz", + "diva", + "divino", + "doble", + "doce", + "dolor", + "domingo", + "don", + "donar", + "dorado", + "dormir", + "dorso", + "dos", + "dosis", + "dragón", + "droga", + "ducha", + "duda", + "duelo", + "dueño", + "dulce", + "dúo", + "duque", + "durar", + "dureza", + "duro", + "ébano", + "ebrio", + "echar", + "eco", + "ecuador", + "edad", + "edición", + "edificio", + "editor", + "educar", + "efecto", + "eficaz", + "eje", + "ejemplo", + "elefante", + "elegir", + "elemento", + "elevar", + "elipse", + "élite", + "elixir", + "elogio", + "eludir", + "embudo", + "emitir", + "emoción", + "empate", + "empeño", + "empleo", + "empresa", + "enano", + "encargo", + "enchufe", + "encía", + "enemigo", + "enero", + "enfado", + "enfermo", + "engaño", + "enigma", + "enlace", + "enorme", + "enredo", + "ensayo", + "enseñar", + "entero", + "entrar", + "envase", + "envío", + "época", + "equipo", + "erizo", + "escala", + "escena", + "escolar", + "escribir", + "escudo", + "esencia", + "esfera", + "esfuerzo", + "espada", + "espejo", + "espía", + "esposa", + "espuma", + "esquí", + "estar", + "este", + "estilo", + "estufa", + "etapa", + "eterno", + "ética", + "etnia", + "evadir", + "evaluar", + "evento", + "evitar", + "exacto", + "examen", + "exceso", + "excusa", + "exento", + "exigir", + "exilio", + "existir", + "éxito", + "experto", + "explicar", + "exponer", + "extremo", + "fábrica", + "fábula", + "fachada", + "fácil", + "factor", + "faena", + "faja", + "falda", + "fallo", + "falso", + "faltar", + "fama", + "familia", + "famoso", + "faraón", + "farmacia", + "farol", + "farsa", + "fase", + "fatiga", + "fauna", + "favor", + "fax", + "febrero", + "fecha", + "feliz", + "feo", + "feria", + "feroz", + "fértil", + "fervor", + "festín", + "fiable", + "fianza", + "fiar", + "fibra", + "ficción", + "ficha", + "fideo", + "fiebre", + "fiel", + "fiera", + "fiesta", + "figura", + "fijar", + "fijo", + "fila", + "filete", + "filial", + "filtro", + "fin", + "finca", + "fingir", + "finito", + "firma", + "flaco", + "flauta", + "flecha", + "flor", + "flota", + "fluir", + "flujo", + "flúor", + "fobia", + "foca", + "fogata", + "fogón", + "folio", + "folleto", + "fondo", + "forma", + "forro", + "fortuna", + "forzar", + "fosa", + "foto", + "fracaso", + "frágil", + "franja", + "frase", + "fraude", + "freír", + "freno", + "fresa", + "frío", + "frito", + "fruta", + "fuego", + "fuente", + "fuerza", + "fuga", + "fumar", + "función", + "funda", + "furgón", + "furia", + "fusil", + "fútbol", + "futuro", + "gacela", + "gafas", + "gaita", + "gajo", + "gala", + "galería", + "gallo", + "gamba", + "ganar", + "gancho", + "ganga", + "ganso", + "garaje", + "garza", + "gasolina", + "gastar", + "gato", + "gavilán", + "gemelo", + "gemir", + "gen", + "género", + "genio", + "gente", + "geranio", + "gerente", + "germen", + "gesto", + "gigante", + "gimnasio", + "girar", + "giro", + "glaciar", + "globo", + "gloria", + "gol", + "golfo", + "goloso", + "golpe", + "goma", + "gordo", + "gorila", + "gorra", + "gota", + "goteo", + "gozar", + "grada", + "gráfico", + "grano", + "grasa", + "gratis", + "grave", + "grieta", + "grillo", + "gripe", + "gris", + "grito", + "grosor", + "grúa", + "grueso", + "grumo", + "grupo", + "guante", + "guapo", + "guardia", + "guerra", + "guía", + "guiño", + "guion", + "guiso", + "guitarra", + "gusano", + "gustar", + "haber", + "hábil", + "hablar", + "hacer", + "hacha", + "hada", + "hallar", + "hamaca", + "harina", + "haz", + "hazaña", + "hebilla", + "hebra", + "hecho", + "helado", + "helio", + "hembra", + "herir", + "hermano", + "héroe", + "hervir", + "hielo", + "hierro", + "hígado", + "higiene", + "hijo", + "himno", + "historia", + "hocico", + "hogar", + "hoguera", + "hoja", + "hombre", + "hongo", + "honor", + "honra", + "hora", + "hormiga", + "horno", + "hostil", + "hoyo", + "hueco", + "huelga", + "huerta", + "hueso", + "huevo", + "huida", + "huir", + "humano", + "húmedo", + "humilde", + "humo", + "hundir", + "huracán", + "hurto", + "icono", + "ideal", + "idioma", + "ídolo", + "iglesia", + "iglú", + "igual", + "ilegal", + "ilusión", + "imagen", + "imán", + "imitar", + "impar", + "imperio", + "imponer", + "impulso", + "incapaz", + "índice", + "inerte", + "infiel", + "informe", + "ingenio", + "inicio", + "inmenso", + "inmune", + "innato", + "insecto", + "instante", + "interés", + "íntimo", + "intuir", + "inútil", + "invierno", + "ira", + "iris", + "ironía", + "isla", + "islote", + "jabalí", + "jabón", + "jamón", + "jarabe", + "jardín", + "jarra", + "jaula", + "jazmín", + "jefe", + "jeringa", + "jinete", + "jornada", + "joroba", + "joven", + "joya", + "juerga", + "jueves", + "juez", + "jugador", + "jugo", + "juguete", + "juicio", + "junco", + "jungla", + "junio", + "juntar", + "júpiter", + "jurar", + "justo", + "juvenil", + "juzgar", + "kilo", + "koala", + "labio", + "lacio", + "lacra", + "lado", + "ladrón", + "lagarto", + "lágrima", + "laguna", + "laico", + "lamer", + "lámina", + "lámpara", + "lana", + "lancha", + "langosta", + "lanza", + "lápiz", + "largo", + "larva", + "lástima", + "lata", + "látex", + "latir", + "laurel", + "lavar", + "lazo", + "leal", + "lección", + "leche", + "lector", + "leer", + "legión", + "legumbre", + "lejano", + "lengua", + "lento", + "leña", + "león", + "leopardo", + "lesión", + "letal", + "letra", + "leve", + "leyenda", + "libertad", + "libro", + "licor", + "líder", + "lidiar", + "lienzo", + "liga", + "ligero", + "lima", + "límite", + "limón", + "limpio", + "lince", + "lindo", + "línea", + "lingote", + "lino", + "linterna", + "líquido", + "liso", + "lista", + "litera", + "litio", + "litro", + "llaga", + "llama", + "llanto", + "llave", + "llegar", + "llenar", + "llevar", + "llorar", + "llover", + "lluvia", + "lobo", + "loción", + "loco", + "locura", + "lógica", + "logro", + "lombriz", + "lomo", + "lonja", + "lote", + "lucha", + "lucir", + "lugar", + "lujo", + "luna", + "lunes", + "lupa", + "lustro", + "luto", + "luz", + "maceta", + "macho", + "madera", + "madre", + "maduro", + "maestro", + "mafia", + "magia", + "mago", + "maíz", + "maldad", + "maleta", + "malla", + "malo", + "mamá", + "mambo", + "mamut", + "manco", + "mando", + "manejar", + "manga", + "maniquí", + "manjar", + "mano", + "manso", + "manta", + "mañana", + "mapa", + "máquina", + "mar", + "marco", + "marea", + "marfil", + "margen", + "marido", + "mármol", + "marrón", + "martes", + "marzo", + "masa", + "máscara", + "masivo", + "matar", + "materia", + "matiz", + "matriz", + "máximo", + "mayor", + "mazorca", + "mecha", + "medalla", + "medio", + "médula", + "mejilla", + "mejor", + "melena", + "melón", + "memoria", + "menor", + "mensaje", + "mente", + "menú", + "mercado", + "merengue", + "mérito", + "mes", + "mesón", + "meta", + "meter", + "método", + "metro", + "mezcla", + "miedo", + "miel", + "miembro", + "miga", + "mil", + "milagro", + "militar", + "millón", + "mimo", + "mina", + "minero", + "mínimo", + "minuto", + "miope", + "mirar", + "misa", + "miseria", + "misil", + "mismo", + "mitad", + "mito", + "mochila", + "moción", + "moda", + "modelo", + "moho", + "mojar", + "molde", + "moler", + "molino", + "momento", + "momia", + "monarca", + "moneda", + "monja", + "monto", + "moño", + "morada", + "morder", + "moreno", + "morir", + "morro", + "morsa", + "mortal", + "mosca", + "mostrar", + "motivo", + "mover", + "móvil", + "mozo", + "mucho", + "mudar", + "mueble", + "muela", + "muerte", + "muestra", + "mugre", + "mujer", + "mula", + "muleta", + "multa", + "mundo", + "muñeca", + "mural", + "muro", + "músculo", + "museo", + "musgo", + "música", + "muslo", + "nácar", + "nación", + "nadar", + "naipe", + "naranja", + "nariz", + "narrar", + "nasal", + "natal", + "nativo", + "natural", + "náusea", + "naval", + "nave", + "navidad", + "necio", + "néctar", + "negar", + "negocio", + "negro", + "neón", + "nervio", + "neto", + "neutro", + "nevar", + "nevera", + "nicho", + "nido", + "niebla", + "nieto", + "niñez", + "niño", + "nítido", + "nivel", + "nobleza", + "noche", + "nómina", + "noria", + "norma", + "norte", + "nota", + "noticia", + "novato", + "novela", + "novio", + "nube", + "nuca", + "núcleo", + "nudillo", + "nudo", + "nuera", + "nueve", + "nuez", + "nulo", + "número", + "nutria", + "oasis", + "obeso", + "obispo", + "objeto", + "obra", + "obrero", + "observar", + "obtener", + "obvio", + "oca", + "ocaso", + "océano", + "ochenta", + "ocho", + "ocio", + "ocre", + "octavo", + "octubre", + "oculto", + "ocupar", + "ocurrir", + "odiar", + "odio", + "odisea", + "oeste", + "ofensa", + "oferta", + "oficio", + "ofrecer", + "ogro", + "oído", + "oír", + "ojo", + "ola", + "oleada", + "olfato", + "olivo", + "olla", + "olmo", + "olor", + "olvido", + "ombligo", + "onda", + "onza", + "opaco", + "opción", + "ópera", + "opinar", + "oponer", + "optar", + "óptica", + "opuesto", + "oración", + "orador", + "oral", + "órbita", + "orca", + "orden", + "oreja", + "órgano", + "orgía", + "orgullo", + "oriente", + "origen", + "orilla", + "oro", + "orquesta", + "oruga", + "osadía", + "oscuro", + "osezno", + "oso", + "ostra", + "otoño", + "otro", + "oveja", + "óvulo", + "óxido", + "oxígeno", + "oyente", + "ozono", + "pacto", + "padre", + "paella", + "página", + "pago", + "país", + "pájaro", + "palabra", + "palco", + "paleta", + "pálido", + "palma", + "paloma", + "palpar", + "pan", + "panal", + "pánico", + "pantera", + "pañuelo", + "papá", + "papel", + "papilla", + "paquete", + "parar", + "parcela", + "pared", + "parir", + "paro", + "párpado", + "parque", + "párrafo", + "parte", + "pasar", + "paseo", + "pasión", + "paso", + "pasta", + "pata", + "patio", + "patria", + "pausa", + "pauta", + "pavo", + "payaso", + "peatón", + "pecado", + "pecera", + "pecho", + "pedal", + "pedir", + "pegar", + "peine", + "pelar", + "peldaño", + "pelea", + "peligro", + "pellejo", + "pelo", + "peluca", + "pena", + "pensar", + "peñón", + "peón", + "peor", + "pepino", + "pequeño", + "pera", + "percha", + "perder", + "pereza", + "perfil", + "perico", + "perla", + "permiso", + "perro", + "persona", + "pesa", + "pesca", + "pésimo", + "pestaña", + "pétalo", + "petróleo", + "pez", + "pezuña", + "picar", + "pichón", + "pie", + "piedra", + "pierna", + "pieza", + "pijama", + "pilar", + "piloto", + "pimienta", + "pino", + "pintor", + "pinza", + "piña", + "piojo", + "pipa", + "pirata", + "pisar", + "piscina", + "piso", + "pista", + "pitón", + "pizca", + "placa", + "plan", + "plata", + "playa", + "plaza", + "pleito", + "pleno", + "plomo", + "pluma", + "plural", + "pobre", + "poco", + "poder", + "podio", + "poema", + "poesía", + "poeta", + "polen", + "policía", + "pollo", + "polvo", + "pomada", + "pomelo", + "pomo", + "pompa", + "poner", + "porción", + "portal", + "posada", + "poseer", + "posible", + "poste", + "potencia", + "potro", + "pozo", + "prado", + "precoz", + "pregunta", + "premio", + "prensa", + "preso", + "previo", + "primo", + "príncipe", + "prisión", + "privar", + "proa", + "probar", + "proceso", + "producto", + "proeza", + "profesor", + "programa", + "prole", + "promesa", + "pronto", + "propio", + "próximo", + "prueba", + "público", + "puchero", + "pudor", + "pueblo", + "puerta", + "puesto", + "pulga", + "pulir", + "pulmón", + "pulpo", + "pulso", + "puma", + "punto", + "puñal", + "puño", + "pupa", + "pupila", + "puré", + "quedar", + "queja", + "quemar", + "querer", + "queso", + "quieto", + "química", + "quince", + "quitar", + "rábano", + "rabia", + "rabo", + "ración", + "radical", + "raíz", + "rama", + "rampa", + "rancho", + "rango", + "rapaz", + "rápido", + "rapto", + "rasgo", + "raspa", + "rato", + "rayo", + "raza", + "razón", + "reacción", + "realidad", + "rebaño", + "rebote", + "recaer", + "receta", + "rechazo", + "recoger", + "recreo", + "recto", + "recurso", + "red", + "redondo", + "reducir", + "reflejo", + "reforma", + "refrán", + "refugio", + "regalo", + "regir", + "regla", + "regreso", + "rehén", + "reino", + "reír", + "reja", + "relato", + "relevo", + "relieve", + "relleno", + "reloj", + "remar", + "remedio", + "remo", + "rencor", + "rendir", + "renta", + "reparto", + "repetir", + "reposo", + "reptil", + "res", + "rescate", + "resina", + "respeto", + "resto", + "resumen", + "retiro", + "retorno", + "retrato", + "reunir", + "revés", + "revista", + "rey", + "rezar", + "rico", + "riego", + "rienda", + "riesgo", + "rifa", + "rígido", + "rigor", + "rincón", + "riñón", + "río", + "riqueza", + "risa", + "ritmo", + "rito" + ]; +} \ No newline at end of file diff --git a/cw_wownero/lib/mywownero.dart b/cw_wownero/lib/mywownero.dart new file mode 100644 index 000000000..d50e48b64 --- /dev/null +++ b/cw_wownero/lib/mywownero.dart @@ -0,0 +1,1689 @@ +const prefixLength = 3; + +String swapEndianBytes(String original) { + if (original.length != 8) { + return ''; + } + + return original[6] + + original[7] + + original[4] + + original[5] + + original[2] + + original[3] + + original[0] + + original[1]; +} + +List tructWords(List wordSet) { + final start = 0; + final end = prefixLength; + + return wordSet.map((word) => word.substring(start, end)).toList(); +} + +String mnemonicDecode(String seed) { + final n = englistWordSet.length; + var out = ''; + var wlist = seed.split(' '); + wlist.removeLast(); + + for (var i = 0; i < wlist.length; i += 3) { + final w1 = + tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); + final w2 = tructWords(englistWordSet) + .indexOf(wlist[i + 1].substring(0, prefixLength)); + final w3 = tructWords(englistWordSet) + .indexOf(wlist[i + 2].substring(0, prefixLength)); + + if (w1 == -1 || w2 == -1 || w3 == -1) { + print("invalid word in mnemonic"); + return ''; + } + + final x = w1 + n * (((n - w1) + w2) % n) + n * n * (((n - w2) + w3) % n); + + if (x % n != w1) { + print("Something went wrong when decoding your private key, please try again"); + return ''; + } + + final _res = '0000000' + x.toRadixString(16); + final start = _res.length - 8; + final end = _res.length; + final res = _res.substring(start, end); + + out += swapEndianBytes(res); + } + + return out; +} + +final englistWordSet = [ + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "guru", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "menu", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" +]; diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart new file mode 100644 index 000000000..1fc1805eb --- /dev/null +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -0,0 +1,53 @@ +import 'package:cw_wownero/api/structs/pending_transaction.dart'; +import 'package:cw_wownero/api/transaction_history.dart' + as wownero_transaction_history; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/amount_converter.dart'; + +import 'package:cw_core/pending_transaction.dart'; + +class DoubleSpendException implements Exception { + DoubleSpendException(); + + @override + String toString() => + 'This transaction cannot be committed. This can be due to many reasons including the wallet not being synced, there is not enough WOW in your available balance, or previous transactions are not yet fully processed.'; +} + +class PendingWowneroTransaction with PendingTransaction { + PendingWowneroTransaction(this.pendingTransactionDescription); + + final PendingTransactionDescription pendingTransactionDescription; + + @override + String get id => pendingTransactionDescription.hash; + + @override + String get hex => pendingTransactionDescription.hex; + + String get txKey => pendingTransactionDescription.txKey; + + @override + String get amountFormatted => + AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.amount); + + @override + String get feeFormatted => + AmountConverter.amountIntToString(CryptoCurrency.wow, pendingTransactionDescription.fee); + + @override + Future commit() async { + try { + wownero_transaction_history.commitTransactionFromPointerAddress( + address: pendingTransactionDescription.pointerAddress); + } catch (e) { + final message = e.toString(); + + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } + } +} diff --git a/cw_wownero/lib/wownero_account_list.dart b/cw_wownero/lib/wownero_account_list.dart new file mode 100644 index 000000000..6d408ba8f --- /dev/null +++ b/cw_wownero/lib/wownero_account_list.dart @@ -0,0 +1,81 @@ +import 'package:cw_core/wownero_amount_format.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/account.dart'; +import 'package:cw_wownero/api/account_list.dart' as account_list; +import 'package:monero/wownero.dart' as wownero; + +part 'wownero_account_list.g.dart'; + +class WowneroAccountList = WowneroAccountListBase with _$WowneroAccountList; + +abstract class WowneroAccountListBase with Store { + WowneroAccountListBase() + : accounts = ObservableList(), + _isRefreshing = false, + _isUpdating = false { + refresh(); + } + + @observable + ObservableList accounts; + bool _isRefreshing; + bool _isUpdating; + + void update() async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(); + final accounts = getAll(); + + if (accounts.isNotEmpty) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + List getAll() => account_list.getAllAccount().map((accountRow) { + final balance = wownero.SubaddressAccountRow_getUnlockedBalance(accountRow); + + return Account( + id: wownero.SubaddressAccountRow_getRowId(accountRow), + label: wownero.SubaddressAccountRow_getLabel(accountRow), + balance: wowneroAmountToString(amount: wownero.Wallet_amountFromString(balance)), + ); + }).toList(); + + Future addAccount({required String label}) async { + await account_list.addAccount(label: label); + update(); + } + + Future setLabelAccount({required int accountIndex, required String label}) async { + await account_list.setLabelForAccount(accountIndex: accountIndex, label: label); + update(); + } + + void refresh() { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + account_list.refreshAccounts(); + _isRefreshing = false; + } catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } +} diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart new file mode 100644 index 000000000..61fd09ef9 --- /dev/null +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -0,0 +1,162 @@ +import 'package:cw_core/subaddress.dart'; +import 'package:cw_wownero/api/coins_info.dart'; +import 'package:cw_wownero/api/subaddress_list.dart' as subaddress_list; +import 'package:flutter/services.dart'; +import 'package:mobx/mobx.dart'; + +part 'wownero_subaddress_list.g.dart'; + +class WowneroSubaddressList = WowneroSubaddressListBase with _$WowneroSubaddressList; + +abstract class WowneroSubaddressListBase with Store { + WowneroSubaddressListBase() + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList(); + + final List _usedAddresses = []; + + @observable + ObservableList subaddresses; + + bool _isRefreshing; + bool _isUpdating; + + void update({required int accountIndex}) { + refreshCoins(accountIndex); + + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + subaddresses.addAll(getAll()); + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + List getAll() { + var subaddresses = subaddress_list.getAllSubaddresses(); + + if (subaddresses.length > 2) { + final primary = subaddresses.first; + final rest = subaddresses.sublist(1).reversed; + subaddresses = [primary] + rest.toList(); + } + + return subaddresses.map((s) { + final address = s.address; + final label = s.label; + final id = s.addressIndex; + final hasDefaultAddressName = + label.toLowerCase() == 'Primary account'.toLowerCase() || + label.toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = id == 0 && hasDefaultAddressName; + return Subaddress( + id: id, + address: address, + label: isPrimaryAddress + ? 'Primary address' + : hasDefaultAddressName + ? '' + : label); + }).toList(); + } + + Future addSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + update(accountIndex: accountIndex); + } + + Future setLabelSubaddress( + {required int accountIndex, required int addressIndex, required String label}) async { + await subaddress_list.setLabelForSubaddress( + accountIndex: accountIndex, addressIndex: addressIndex, label: label); + update(accountIndex: accountIndex); + } + + void refresh({required int accountIndex}) { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + subaddress_list.refreshSubaddresses(accountIndex: accountIndex); + _isRefreshing = false; + } on PlatformException catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } + + Future updateWithAutoGenerate({ + required int accountIndex, + required String defaultLabel, + required List usedAddresses, + }) async { + _usedAddresses.addAll(usedAddresses); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + final newSubAddresses = + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel); + subaddresses.addAll(newSubAddresses); + } catch (e) { + rethrow; + } finally { + _isUpdating = false; + } + } + + Future> _getAllUnusedAddresses( + {required int accountIndex, required String label}) async { + final allAddresses = subaddress_list.getAllSubaddresses(); + final lastAddress = allAddresses.last.address; + if (allAddresses.isEmpty || _usedAddresses.contains(lastAddress)) { + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); + if (!isAddressUnused) { + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); + } + } + + return allAddresses + .map((s) { + final id = s.addressIndex; + final address = s.address; + final label = s.label; + return Subaddress( + id: id, + address: address, + label: id == 0 && + label.toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : label); + }) + .toList(); + } + + Future _newSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + + return subaddress_list + .getAllSubaddresses() + .where((s) { + final address = s.address; + return !_usedAddresses.contains(address); + }) + .isNotEmpty; + } +} diff --git a/cw_wownero/lib/wownero_transaction_creation_credentials.dart b/cw_wownero/lib/wownero_transaction_creation_credentials.dart new file mode 100644 index 000000000..85a622b6f --- /dev/null +++ b/cw_wownero/lib/wownero_transaction_creation_credentials.dart @@ -0,0 +1,9 @@ +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class WowneroTransactionCreationCredentials { + WowneroTransactionCreationCredentials({required this.outputs, required this.priority}); + + final List outputs; + final MoneroTransactionPriority priority; +} diff --git a/cw_wownero/lib/wownero_transaction_history.dart b/cw_wownero/lib/wownero_transaction_history.dart new file mode 100644 index 000000000..c4d2d1e81 --- /dev/null +++ b/cw_wownero/lib/wownero_transaction_history.dart @@ -0,0 +1,28 @@ +import 'dart:core'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_wownero/wownero_transaction_info.dart'; + +part 'wownero_transaction_history.g.dart'; + +class WowneroTransactionHistory = WowneroTransactionHistoryBase + with _$WowneroTransactionHistory; + +abstract class WowneroTransactionHistoryBase + extends TransactionHistoryBase with Store { + WowneroTransactionHistoryBase() { + transactions = ObservableMap(); + } + + @override + Future save() async {} + + @override + void addOne(WowneroTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + +} diff --git a/cw_wownero/lib/wownero_transaction_info.dart b/cw_wownero/lib/wownero_transaction_info.dart new file mode 100644 index 000000000..2060f1f95 --- /dev/null +++ b/cw_wownero/lib/wownero_transaction_info.dart @@ -0,0 +1,80 @@ +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wownero_amount_format.dart'; +import 'package:cw_wownero/api/structs/transaction_info_row.dart'; +import 'package:cw_core/parseBoolFromString.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_wownero/api/transaction_history.dart'; + +class WowneroTransactionInfo extends TransactionInfo { + WowneroTransactionInfo(this.id, this.height, this.direction, this.date, + this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee, + this.confirmations); + + WowneroTransactionInfo.fromMap(Map map) + : id = (map['hash'] ?? '') as String, + height = (map['height'] ?? 0) as int, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, + date = DateTime.fromMillisecondsSinceEpoch( + (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), + isPending = parseBoolFromString(map['isPending'] as String), + amount = map['amount'] as int, + accountIndex = int.parse(map['accountIndex'] as String), + addressIndex = map['addressIndex'] as int, + confirmations = map['confirmations'] as int, + key = getTxKey((map['hash'] ?? '') as String), + fee = map['fee'] as int? ?? 0 { + additionalInfo = { + 'key': key, + 'accountIndex': accountIndex, + 'addressIndex': addressIndex + }; + } + + WowneroTransactionInfo.fromRow(TransactionInfoRow row) + : id = row.getHash(), + height = row.blockHeight, + direction = parseTransactionDirectionFromInt(row.direction), + date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), + isPending = row.isPending != 0, + amount = row.getAmount(), + accountIndex = row.subaddrAccount, + addressIndex = row.subaddrIndex, + confirmations = row.confirmations, + key = getTxKey(row.getHash()), + fee = row.fee { + additionalInfo = { + 'key': key, + 'accountIndex': accountIndex, + 'addressIndex': addressIndex + }; + } + + final String id; + final int height; + final TransactionDirection direction; + final DateTime date; + final int accountIndex; + final bool isPending; + final int amount; + final int fee; + final int addressIndex; + final int confirmations; + String? recipientAddress; + String? key; + String? _fiatAmount; + + @override + String amountFormatted() => '${formatAmount(wowneroAmountToString(amount: amount))} WOW'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${formatAmount(wowneroAmountToString(amount: fee))} WOW'; +} diff --git a/cw_wownero/lib/wownero_unspent.dart b/cw_wownero/lib/wownero_unspent.dart new file mode 100644 index 000000000..a79106886 --- /dev/null +++ b/cw_wownero/lib/wownero_unspent.dart @@ -0,0 +1,20 @@ +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_wownero/api/structs/coins_info_row.dart'; + +class WowneroUnspent extends Unspent { + WowneroUnspent( + String address, String hash, String keyImage, int value, bool isFrozen, this.isUnlocked) + : super(address, hash, value, 0, keyImage) { + this.isFrozen = isFrozen; + } + + factory WowneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) => WowneroUnspent( + coinsInfoRow.getAddress(), + coinsInfoRow.getHash(), + coinsInfoRow.getKeyImage(), + coinsInfoRow.amount, + coinsInfoRow.frozen == 1, + coinsInfoRow.unlocked == 1); + + final bool isUnlocked; +} diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart new file mode 100644 index 000000000..52f84e26a --- /dev/null +++ b/cw_wownero/lib/wownero_wallet.dart @@ -0,0 +1,747 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:cw_core/account.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_wallet_utils.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wownero_amount_format.dart'; +import 'package:cw_core/wownero_balance.dart'; +import 'package:cw_wownero/api/coins_info.dart'; +import 'package:cw_wownero/api/structs/pending_transaction.dart'; +import 'package:cw_wownero/api/transaction_history.dart' as transaction_history; +import 'package:cw_wownero/api/wallet.dart' as wownero_wallet; +import 'package:cw_wownero/api/wallet_manager.dart'; +import 'package:cw_wownero/api/wownero_output.dart'; +import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart'; +import 'package:cw_wownero/exceptions/wownero_transaction_no_inputs_exception.dart'; +import 'package:cw_wownero/pending_wownero_transaction.dart'; +import 'package:cw_wownero/wownero_transaction_creation_credentials.dart'; +import 'package:cw_wownero/wownero_transaction_history.dart'; +import 'package:cw_wownero/wownero_transaction_info.dart'; +import 'package:cw_wownero/wownero_unspent.dart'; +import 'package:cw_wownero/wownero_wallet_addresses.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:monero/wownero.dart' as wownero; + +part 'wownero_wallet.g.dart'; + +const wowneroBlockSize = 1000; +// not sure if this should just be 0 but setting it higher feels safer / should catch more cases: +const MIN_RESTORE_HEIGHT = 1000; + +class WowneroWallet = WowneroWalletBase with _$WowneroWallet; + +abstract class WowneroWalletBase + extends WalletBase + with Store { + WowneroWalletBase( + {required WalletInfo walletInfo, required Box unspentCoinsInfo}) + : balance = ObservableMap.of({ + CryptoCurrency.wow: WowneroBalance( + fullBalance: wownero_wallet.getFullBalance(accountIndex: 0), + unlockedBalance: wownero_wallet.getFullBalance(accountIndex: 0)) + }), + _isTransactionUpdating = false, + _hasSyncAfterStartup = false, + isEnabledAutoGenerateSubaddress = false, + syncStatus = NotConnectedSyncStatus(), + unspentCoins = [], + this.unspentCoinsInfo = unspentCoinsInfo, + super(walletInfo) { + transactionHistory = WowneroTransactionHistory(); + walletAddresses = WowneroWalletAddresses(walletInfo, transactionHistory); + + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { + if (account == null) return; + + balance = ObservableMap.of({ + currency: WowneroBalance( + fullBalance: wownero_wallet.getFullBalance(accountIndex: account.id), + unlockedBalance: wownero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); + _askForUpdateTransactionHistory(); + }); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + _updateSubAddress(enabled, account: walletAddresses.account); + }); + } + + static const int _autoSaveInterval = 30; + + Box unspentCoinsInfo; + + void Function(FlutterErrorDetails)? onError; + + @override + late WowneroWalletAddresses walletAddresses; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + ObservableMap balance; + + @override + String get seed => wownero_wallet.getSeed(); + + String seedLegacy(String? language) { + return wownero_wallet.getSeedLegacy(language); + } + + @override + MoneroWalletKeys get keys => MoneroWalletKeys( + privateSpendKey: wownero_wallet.getSecretSpendKey(), + privateViewKey: wownero_wallet.getSecretViewKey(), + publicSpendKey: wownero_wallet.getPublicSpendKey(), + publicViewKey: wownero_wallet.getPublicViewKey()); + + wownero_wallet.SyncListener? _listener; + ReactionDisposer? _onAccountChangeReaction; + bool _isTransactionUpdating; + bool _hasSyncAfterStartup; + Timer? _autoSaveTimer; + List unspentCoins; + + Future init() async { + await walletAddresses.init(); + balance = ObservableMap.of({ + currency: WowneroBalance( + fullBalance: wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), + unlockedBalance: + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id)) + }); + _setListeners(); + await updateTransactions(); + + if (walletInfo.isRecovery) { + wownero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); + + if (wownero_wallet.getCurrentHeight() <= 1) { + wownero_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight); + } + } + + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); + } + + @override + Future? updateBalance() => null; + + @override + void close() async { + _listener?.stop(); + _onAccountChangeReaction?.reaction.dispose(); + _autoSaveTimer?.cancel(); + } + + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + await wownero_wallet.setupNode( + address: node.uri.toString(), + login: node.login, + password: node.password, + useSSL: node.isSSL, + isLightWallet: false, + // FIXME: hardcoded value + socksProxyAddress: node.socksProxyAddress); + + wownero_wallet.setTrustedDaemon(node.trusted); + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + print(e); + } + } + + @override + Future startSync() async { + try { + _setInitialHeight(); + } catch (_) { + // our restore height wasn't correct, so lets see if using the backup works: + try { + await resetCache(name); + _setInitialHeight(); + } catch (e) { + // we still couldn't get a valid height from the backup?!: + // try to use the date instead: + try { + _setHeightFromDate(); + } catch (_) { + // we still couldn't get a valid sync height :/ + } + } + } + + try { + syncStatus = AttemptingSyncStatus(); + wownero_wallet.startRefresh(); + _setListeners(); + _listener?.start(); + } catch (e) { + syncStatus = FailedSyncStatus(); + print(e); + rethrow; + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as WowneroTransactionCreationCredentials; + final inputs = []; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final unlockedBalance = + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + var allInputsAmount = 0; + + PendingTransactionDescription pendingTransactionDescription; + + if (!(syncStatus is SyncedSyncStatus)) { + throw WowneroTransactionCreationException('The wallet is not synced.'); + } + + if (unspentCoins.isEmpty) { + await updateUnspent(); + } + + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx.keyImage!); + } + } + final spendAllCoins = inputs.length == unspentCoins.length; + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw WowneroTransactionCreationException( + 'You do not have enough WOW to send this amount.'); + } + + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount); + if (unlockedBalance < totalAmount) { + throw WowneroTransactionCreationException( + 'You do not have enough WOW to send this amount.'); + } + + if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) { + throw WowneroTransactionNoInputsException(inputs.length); + } + + final wowneroOutputs = outputs.map((output) { + final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address; + + return WowneroOutput( + address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.')); + }).toList(); + + pendingTransactionDescription = await transaction_history.createTransactionMultDest( + outputs: wowneroOutputs, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); + } else { + final output = outputs.first; + final address = output.isParsedAddress ? output.extractedAddress : output.address; + final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.'); + final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount; + + if ((formattedAmount != null && unlockedBalance < formattedAmount) || + (formattedAmount == null && unlockedBalance <= 0)) { + final formattedBalance = wowneroAmountToString(amount: unlockedBalance); + + throw WowneroTransactionCreationException( + 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); + } + + final estimatedFee = calculateEstimatedFee(_credentials.priority, formattedAmount); + if (!spendAllCoins && + ((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) || + formattedAmount == null)) { + throw WowneroTransactionNoInputsException(inputs.length); + } + + pendingTransactionDescription = await transaction_history.createTransaction( + address: address!, + amount: amount, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account!.id, + preferredInputs: inputs); + } + + return PendingWowneroTransaction(pendingTransactionDescription); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + // FIXME: hardcoded value; + + if (priority is MoneroTransactionPriority) { + switch (priority) { + case MoneroTransactionPriority.slow: + return 24590000; + case MoneroTransactionPriority.automatic: + return 123050000; + case MoneroTransactionPriority.medium: + return 245029999; + case MoneroTransactionPriority.fast: + return 614530000; + case MoneroTransactionPriority.fastest: + return 26021600000; + } + } + + return 0; + } + + @override + Future save() async { + await walletAddresses.updateUsedSubaddress(); + + if (isEnabledAutoGenerateSubaddress) { + walletAddresses.updateUnusedSubaddress( + accountIndex: walletAddresses.account?.id ?? 0, + defaultLabel: walletAddresses.account?.label ?? ''); + } + + await walletAddresses.updateAddressesInBox(); + await wownero_wallet.store(); + try { + await backupWalletFiles(name); + } catch (e) { + print("¯\\_(ツ)_/¯"); + print(e); + } + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletDirPath = await pathForWalletDir(name: name, type: type); + if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) { + // NOTE: this is realistically only required on windows. + print("closing wallet"); + final wmaddr = wmPtr.address; + final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address; + await Isolate.run(() { + wownero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true); + }); + openedWalletsByPath.remove("$currentWalletDirPath/$name"); + print("wallet closed"); + } + try { + // -- rename the waller folder -- + final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type)); + final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentWalletDir.rename(newWalletDirPath); + + // -- use new waller folder to rename files with old names still -- + final renamedWalletPath = newWalletDirPath + '/$name'; + + final currentCacheFile = File(renamedWalletPath); + final currentKeysFile = File('$renamedWalletPath.keys'); + final currentAddressListFile = File('$renamedWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + if (currentCacheFile.existsSync()) { + await currentCacheFile.rename(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.rename('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.rename('$newWalletPath.address.txt'); + } + + await backupWalletFiles(newWalletName); + } catch (e) { + final currentWalletPath = await pathForWallet(name: name, type: type); + + final currentCacheFile = File(currentWalletPath); + final currentKeysFile = File('$currentWalletPath.keys'); + final currentAddressListFile = File('$currentWalletPath.address.txt'); + + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + + // Copies current wallet files into new wallet name's dir and files + if (currentCacheFile.existsSync()) { + await currentCacheFile.copy(newWalletPath); + } + if (currentKeysFile.existsSync()) { + await currentKeysFile.copy('$newWalletPath.keys'); + } + if (currentAddressListFile.existsSync()) { + await currentAddressListFile.copy('$newWalletPath.address.txt'); + } + + // Delete old name's dir and files + await Directory(currentWalletDirPath).delete(recursive: true); + } + } + + @override + Future changePassword(String password) async => wownero_wallet.setPasswordSync(password); + + Future getNodeHeight() async => wownero_wallet.getNodeHeight(); + + Future isConnected() async => wownero_wallet.isConnected(); + + Future setAsRecovered() async { + walletInfo.isRecovery = false; + await walletInfo.save(); + } + + @override + Future rescan({required int height}) async { + walletInfo.restoreHeight = height; + walletInfo.isRecovery = true; + wownero_wallet.setRefreshFromBlockHeight(height: height); + wownero_wallet.rescanBlockchainAsync(); + await startSync(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + await _askForUpdateTransactionHistory(); + await save(); + await walletInfo.save(); + } + + Future updateUnspent() async { + try { + refreshCoins(walletAddresses.account!.id); + + unspentCoins.clear(); + + final coinCount = countOfCoins(); + for (var i = 0; i < coinCount; i++) { + final coin = getCoin(i); + final coinSpent = wownero.CoinsInfo_spent(coin); + if (coinSpent == false) { + final unspent = WowneroUnspent( + wownero.CoinsInfo_address(coin), + wownero.CoinsInfo_hash(coin), + wownero.CoinsInfo_keyImage(coin), + wownero.CoinsInfo_amount(coin), + wownero.CoinsInfo_frozen(coin), + wownero.CoinsInfo_unlocked(coin), + ); + if (unspent.hash.isNotEmpty) { + unspent.isChange = transaction_history.getTransaction(unspent.hash) == 1; + } + unspentCoins.add(unspent); + } + } + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && + element.accountIndex == walletAddresses.account!.id && + element.keyImage!.contains(coin.keyImage!)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + _askForUpdateBalance(); + } catch (e, s) { + print(e.toString()); + onError?.call(FlutterErrorDetails( + exception: e, + stack: s, + library: this.runtimeType.toString(), + )); + } + } + + Future _addCoinInfo(WowneroUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.address, + value: coin.value, + vout: 0, + keyImage: coin.keyImage, + isChange: coin.isChange, + accountIndex: walletAddresses.account!.id); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = + unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } + } + + String getTransactionAddress(int accountIndex, int addressIndex) => + wownero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); + + @override + Future> fetchTransactions() async { + transaction_history.refreshTransactions(); + return _getAllTransactionsOfAccount(walletAddresses.account?.id) + .fold>({}, + (Map acc, WowneroTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + transactionHistory.clear(); + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + String getSubaddressLabel(int accountIndex, int addressIndex) => + wownero_wallet.getSubaddressLabel(accountIndex, addressIndex); + + List _getAllTransactionsOfAccount(int? accountIndex) => + transaction_history + .getAllTransactions() + .map( + (row) => WowneroTransactionInfo( + row.hash, + row.blockheight, + row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming, + row.timeStamp, + row.isPending, + row.amount, + row.accountIndex, + 0, + row.fee, + row.confirmations, + )..additionalInfo = { + 'key': row.key, + 'accountIndex': row.accountIndex, + 'addressIndex': row.addressIndex + }, + ) + .where((element) => element.accountIndex == (accountIndex ?? 0)) + .toList(); + + void _setListeners() { + _listener?.stop(); + _listener = wownero_wallet.setListeners(_onNewBlock, _onNewTransaction); + } + + // check if the height is correct: + void _setInitialHeight() { + if (walletInfo.isRecovery) { + return; + } + + final height = wownero_wallet.getCurrentHeight(); + + if (height > MIN_RESTORE_HEIGHT) { + // the restore height is probably correct, so we do nothing: + return; + } + + throw Exception("height isn't > $MIN_RESTORE_HEIGHT!"); + } + + void _setHeightFromDate() { + if (walletInfo.isRecovery) { + return; + } + + int height = 0; + try { + height = _getHeightByDate(walletInfo.date); + } catch (_) {} + + wownero_wallet.setRecoveringFromSeed(isRecovery: true); + wownero_wallet.setRefreshFromBlockHeight(height: height); + } + + int _getHeightDistance(DateTime date) { + final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final daysTmp = (distance / 86400).round(); + final days = daysTmp < 1 ? 1 : daysTmp; + + return days * 1000; + } + + int _getHeightByDate(DateTime date) { + final nodeHeight = wownero_wallet.getNodeHeightSync(); + final heightDistance = _getHeightDistance(date); + + if (nodeHeight <= 0) { + // the node returned 0 (an error state) + throw Exception("nodeHeight is <= 0!"); + } + + return nodeHeight - heightDistance; + } + + void _askForUpdateBalance() { + final unlockedBalance = _getUnlockedBalance(); + final fullBalance = _getFullBalance(); + final frozenBalance = _getFrozenBalance(); + + if (balance[currency]!.fullBalance != fullBalance || + balance[currency]!.unlockedBalance != unlockedBalance || + balance[currency]!.frozenBalance != frozenBalance) { + balance[currency] = WowneroBalance( + fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance); + } + } + + Future _askForUpdateTransactionHistory() async => await updateTransactions(); + + int _getFullBalance() => wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id); + + int _getUnlockedBalance() => + wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + + int _getFrozenBalance() { + var frozenBalance = 0; + + for (var coin in unspentCoinsInfo.values.where((element) => + element.walletId == id && element.accountIndex == walletAddresses.account!.id)) { + if (coin.isFrozen) frozenBalance += coin.value; + } + + return frozenBalance; + } + + void _onNewBlock(int height, int blocksLeft, double ptc) async { + try { + if (walletInfo.isRecovery) { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + } + + if (blocksLeft < 100) { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + syncStatus = SyncedSyncStatus(); + + if (!_hasSyncAfterStartup) { + _hasSyncAfterStartup = true; + await save(); + } + + if (walletInfo.isRecovery) { + await setAsRecovered(); + } + } else { + syncStatus = SyncingSyncStatus(blocksLeft, ptc); + } + } catch (e) { + print(e.toString()); + } + } + + void _onNewTransaction() async { + try { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + await Future.delayed(Duration(seconds: 1)); + } catch (e) { + print(e.toString()); + } + } + + void _updateSubAddress(bool enableAutoGenerate, {Account? account}) { + if (enableAutoGenerate) { + walletAddresses.updateUnusedSubaddress( + accountIndex: account?.id ?? 0, + defaultLabel: account?.label ?? '', + ); + } else { + walletAddresses.updateSubaddressList(accountIndex: account?.id ?? 0); + } + } + + @override + void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e; + + @override + Future signMessage(String message, {String? address}) async { + final useAddress = address ?? ""; + return wownero_wallet.signMessage(message, address: useAddress); + } +} diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart new file mode 100644 index 000000000..dc4b42840 --- /dev/null +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -0,0 +1,119 @@ +import 'package:cw_core/account.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/subaddress.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_wownero/api/wallet.dart'; +import 'package:cw_wownero/wownero_account_list.dart'; +import 'package:cw_wownero/wownero_subaddress_list.dart'; +import 'package:cw_wownero/wownero_transaction_history.dart'; +import 'package:mobx/mobx.dart'; + +part 'wownero_wallet_addresses.g.dart'; + +class WowneroWalletAddresses = WowneroWalletAddressesBase with _$WowneroWalletAddresses; + +abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { + WowneroWalletAddressesBase( + WalletInfo walletInfo, WowneroTransactionHistory wowneroTransactionHistory) + : accountList = WowneroAccountList(), + _wowneroTransactionHistory = wowneroTransactionHistory, + subaddressList = WowneroSubaddressList(), + address = '', + super(walletInfo); + + final WowneroTransactionHistory _wowneroTransactionHistory; + @override + @observable + String address; + + @observable + Account? account; + + @observable + Subaddress? subaddress; + + WowneroSubaddressList subaddressList; + + WowneroAccountList accountList; + + @override + Future init() async { + accountList.update(); + account = accountList.accounts.first; + updateSubaddressList(accountIndex: account?.id ?? 0); + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + final _subaddressList = WowneroSubaddressList(); + + addressesMap.clear(); + addressInfos.clear(); + + accountList.accounts.forEach((account) { + _subaddressList.update(accountIndex: account.id); + _subaddressList.subaddresses.forEach((subaddress) { + addressesMap[subaddress.address] = subaddress.label; + addressInfos[account.id] ??= []; + addressInfos[account.id]?.add(AddressInfo( + address: subaddress.address, label: subaddress.label, accountIndex: account.id)); + }); + }); + + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } + + bool validate() { + accountList.update(); + final accountListLength = accountList.accounts.length; + + if (accountListLength <= 0) { + return false; + } + + subaddressList.update(accountIndex: accountList.accounts.first.id); + final subaddressListLength = subaddressList.subaddresses.length; + + if (subaddressListLength <= 0) { + return false; + } + + return true; + } + + void updateSubaddressList({required int accountIndex}) { + subaddressList.update(accountIndex: accountIndex); + subaddress = subaddressList.subaddresses.first; + address = subaddress!.address; + } + + Future updateUsedSubaddress() async { + final transactions = _wowneroTransactionHistory.transactions.values.toList(); + + transactions.forEach((element) { + final accountIndex = element.accountIndex; + final addressIndex = element.addressIndex; + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); + }); + } + + Future updateUnusedSubaddress( + {required int accountIndex, required String defaultLabel}) async { + await subaddressList.updateWithAutoGenerate( + accountIndex: accountIndex, + defaultLabel: defaultLabel, + usedAddresses: usedAddresses.toList()); + subaddress = subaddressList.subaddresses.last; + address = subaddress!.address; + } + + @override + bool containsAddress(String address) => + addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; +} diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart new file mode 100644 index 000000000..13cab8f61 --- /dev/null +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -0,0 +1,354 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'package:cw_core/monero_wallet_utils.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_wownero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_wownero/api/wallet_manager.dart' as wownero_wallet_manager; +import 'package:cw_wownero/api/wallet_manager.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; +import 'package:polyseed/polyseed.dart'; +import 'package:monero/wownero.dart' as wownero; + +class WowneroNewWalletCredentials extends WalletCredentials { + WowneroNewWalletCredentials( + {required String name, required this.language, required this.isPolyseed, String? password}) + : super(name: name, password: password); + + final String language; + final bool isPolyseed; +} + +class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials { + WowneroRestoreWalletFromSeedCredentials( + {required String name, required this.mnemonic, int height = 0, String? password}) + : super(name: name, password: password, height: height); + + final String mnemonic; +} + +class WowneroWalletLoadingException implements Exception { + @override + String toString() => 'Failure to load the wallet.'; +} + +class WowneroRestoreWalletFromKeysCredentials extends WalletCredentials { + WowneroRestoreWalletFromKeysCredentials( + {required String name, + required String password, + required this.language, + required this.address, + required this.viewKey, + required this.spendKey, + int height = 0}) + : super(name: name, password: password, height: height); + + final String language; + final String address; + final String viewKey; + final String spendKey; +} + +class WowneroWalletService extends WalletService< + WowneroNewWalletCredentials, + WowneroRestoreWalletFromSeedCredentials, + WowneroRestoreWalletFromKeysCredentials, + WowneroNewWalletCredentials> { + WowneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + + static bool walletFilesExist(String path) => + !File(path).existsSync() && !File('$path.keys').existsSync(); + + @override + WalletType getType() => WalletType.wownero; + + @override + Future create(WowneroNewWalletCredentials credentials, {bool? isTestnet}) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + + if (credentials.isPolyseed) { + final polyseed = Polyseed.create(); + final lang = PolyseedLang.getByEnglishName(credentials.language); + + final heightOverride = + getWowneroHeightByDate(date: DateTime.now().subtract(Duration(days: 2))); + + return _restoreFromPolyseed( + path, credentials.password!, polyseed, credentials.walletInfo!, lang, + overrideHeight: heightOverride); + } + + await wownero_wallet_manager.createWallet( + path: path, password: credentials.password!, language: credentials.language); + final wallet = WowneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('WowneroWalletsManager Error: ${e.toString()}'); + rethrow; + } + } + + @override + Future isWalletExit(String name) async { + try { + final path = await pathForWallet(name: name, type: getType()); + return wownero_wallet_manager.isWalletExist(path: path); + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('WowneroWalletsManager Error: $e'); + rethrow; + } + } + + @override + Future openWallet(String name, String password) async { + WowneroWallet? wallet; + try { + final path = await pathForWallet(name: name, type: getType()); + + if (walletFilesExist(path)) { + await repairOldAndroidWallet(name); + } + + await wownero_wallet_manager.openWalletAsync({'path': path, 'password': password}); + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + final isValid = wallet.walletAddresses.validate(); + + if (!isValid) { + await restoreOrResetWalletFiles(name); + wallet.close(); + return openWallet(name, password); + } + + await wallet.init(); + + return wallet; + } catch (e, s) { + // TODO: Implement Exception for wallet list service. + + final bool isBadAlloc = e.toString().contains('bad_alloc') || + (e is WalletOpeningException && + (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); + + final bool doesNotCorrespond = e.toString().contains('does not correspond') || + (e is WalletOpeningException && e.message.contains('does not correspond')); + + final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || + (e is WalletOpeningException && e.message.contains('basic_string')); + + final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || + e.toString().contains('input stream error') || + (e is WalletOpeningException && + (e.message.contains('input_stream') || e.message.contains('input stream error'))); + + final bool invalidSignature = e.toString().contains('invalid signature') || + (e is WalletOpeningException && e.message.contains('invalid signature')); + + if (!isBadAlloc && + !doesNotCorrespond && + !isMissingCacheFilesIOS && + !isMissingCacheFilesAndroid && + !invalidSignature && + wallet != null && + wallet.onError != null) { + wallet.onError!(FlutterErrorDetails(exception: e, stack: s)); + } + + await restoreOrResetWalletFiles(name); + return openWallet(name, password); + } + } + + @override + Future remove(String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + if (openedWalletsByPath["$path/$wallet"] != null) { + // NOTE: this is realistically only required on windows. + print("closing wallet"); + final wmaddr = wmPtr.address; + final waddr = openedWalletsByPath["$path/$wallet"]!.address; + // await Isolate.run(() { + wownero.WalletManager_closeWallet( + Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), false); + // }); + openedWalletsByPath.remove("$path/$wallet"); + print("wallet closed"); + } + + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + + final walletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(wallet, getType())); + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = + WowneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromKeys(WowneroRestoreWalletFromKeysCredentials credentials, + {bool? isTestnet}) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + await wownero_wallet_manager.restoreFromKeys( + path: path, + password: credentials.password!, + language: credentials.language, + restoreHeight: credentials.height!, + address: credentials.address, + viewKey: credentials.viewKey, + spendKey: credentials.spendKey); + final wallet = WowneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('WowneroWalletsManager Error: $e'); + rethrow; + } + } + + @override + Future restoreFromHardwareWallet(WowneroNewWalletCredentials credentials) { + throw UnimplementedError( + "Restoring a Wownero wallet from a hardware wallet is not yet supported!"); + } + + @override + Future restoreFromSeed(WowneroRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + // Restore from Polyseed + if (Polyseed.isValidSeed(credentials.mnemonic)) { + return restoreFromPolyseed(credentials); + } + + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + await wownero_wallet_manager.restoreFromSeed( + path: path, + password: credentials.password!, + seed: credentials.mnemonic, + restoreHeight: credentials.height!); + final wallet = WowneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('WowneroWalletsManager Error: $e'); + rethrow; + } + } + + Future restoreFromPolyseed( + WowneroRestoreWalletFromSeedCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + final polyseedCoin = PolyseedCoin.POLYSEED_WOWNERO; + final lang = PolyseedLang.getByPhrase(credentials.mnemonic); + final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); + + return _restoreFromPolyseed( + path, credentials.password!, polyseed, credentials.walletInfo!, lang); + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('WowneroWalletsManager Error: $e'); + rethrow; + } + } + + Future _restoreFromPolyseed( + String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, + {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight}) async { + final height = overrideHeight ?? + getWowneroHeightByDate(date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); + final spendKey = polyseed.generateKey(coin, 32).toHexString(); + final seed = polyseed.encode(lang, coin); + + walletInfo.isRecovery = true; + walletInfo.restoreHeight = height; + + await wownero_wallet_manager.restoreFromSpendKey( + path: path, + password: password, + seed: seed, + language: lang.nameEnglish, + restoreHeight: height, + spendKey: spendKey); + + final wallet = WowneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); + await wallet.init(); + + return wallet; + } + + Future repairOldAndroidWallet(String name) async { + try { + if (!Platform.isAndroid) { + return; + } + + final oldAndroidWalletDirPath = await outdatedAndroidPathForWalletDir(name: name); + final dir = Directory(oldAndroidWalletDirPath); + + if (!dir.existsSync()) { + return; + } + + final newWalletDirPath = await pathForWalletDir(name: name, type: getType()); + + dir.listSync().forEach((f) { + final file = File(f.path); + final name = f.path.split('/').last; + final newPath = newWalletDirPath + '/$name'; + final newFile = File(newPath); + + if (!newFile.existsSync()) { + newFile.createSync(); + } + newFile.writeAsBytesSync(file.readAsBytesSync()); + }); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_monero/example/pubspec.lock b/cw_wownero/pubspec.lock similarity index 57% rename from cw_monero/example/pubspec.lock rename to cw_wownero/pubspec.lock index 7816a7fad..f5dc3de3f 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_wownero/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" args: dependency: transitive description: @@ -33,6 +49,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" + source: hosted + version: "7.2.7" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" + source: hosted + version: "8.4.3" characters: dependency: transitive description: @@ -41,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" + source: hosted + version: "2.0.2" clock: dependency: transitive description: @@ -49,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" + source: hosted + version: "4.4.0" collection: dependency: transitive description: @@ -73,30 +169,23 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" - cupertino_icons: + cw_core: dependency: "direct main" description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" url: "https://pub.dev" source: hosted - version: "1.0.5" - cw_core: - dependency: transitive - description: - path: "../../cw_core" - relative: true - source: path - version: "0.0.1" - cw_monero: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.0.1" + version: "2.2.4" encrypt: - dependency: transitive + dependency: "direct main" description: name: encrypt sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" @@ -112,13 +201,13 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" file: dependency: transitive description: @@ -127,21 +216,21 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c - url: "https://pub.dev" - source: hosted - version: "2.0.1" flutter_mobx: - dependency: transitive + dependency: "direct main" description: name: flutter_mobx sha256: "0da4add0016387a7bf309a0d0c41d36c6b3ae25ed7a176409267f166509e723e" @@ -153,6 +242,30 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" + source: hosted + version: "2.2.0" hashlib: dependency: transitive description: @@ -169,14 +282,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - http: + hive: dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "81fd20125cb2ce8fd23623d7744ffbaf653aae93706c9bd3bf7019ea0ace3938" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + http: + dependency: "direct main" description: name: http sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -186,13 +323,21 @@ packages: source: hosted version: "4.0.2" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: @@ -201,6 +346,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" + source: hosted + version: "4.8.0" leak_tracker: dependency: transitive description: @@ -225,14 +378,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - lints: + logging: dependency: transitive description: - name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.1.1" matcher: dependency: transitive description: @@ -257,14 +410,55 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" - mobx: + mime: dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mobx: + dependency: "direct main" description: name: mobx sha256: f1862bd92c6a903fab67338f27e2f731117c3cb9ea37cee1a487f9e4e0de314a url: "https://pub.dev" source: hosted version: "2.1.3+1" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: "86122e410d8ea24dda0c69adb5c2a6ccadd5ce02ad46e144764e0d0184a06181" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + monero: + dependency: "direct main" + description: + path: "." + ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + resolved-ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + url: "https://github.com/mrcyjanek/monero.dart" + source: git + version: "0.0.0" + mutex: + dependency: "direct main" + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -274,7 +468,7 @@ packages: source: hosted version: "1.9.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa @@ -346,13 +540,21 @@ packages: source: hosted version: "3.7.3" polyseed: - dependency: transitive + dependency: "direct main" description: name: polyseed - sha256: "9b48ec535b10863f78f6354ec983b4cc0c88ca69ff48fee469d0fd1954b01d4f" + sha256: a340962242d7917b0f3e6bd02c4acc3f90eae8ff766f1244f793ae7a6414dd68 url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.4" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -361,6 +563,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" + source: hosted + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -374,6 +608,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: @@ -398,6 +648,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -422,6 +680,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" typed_data: dependency: transitive description: @@ -446,14 +712,30 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + watcher: + dependency: "direct overridden" + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" + source: hosted + version: "2.3.0" win32: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.3" xdg_directories: dependency: transitive description: @@ -462,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0+3" + yaml: + dependency: transitive + description: + name: yaml + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" + source: hosted + version: "3.1.1" sdks: dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.7.0" diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml new file mode 100644 index 000000000..5e2a11461 --- /dev/null +++ b/cw_wownero/pubspec.yaml @@ -0,0 +1,82 @@ +name: cw_wownero +description: A new flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: ">=2.19.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + ffi: ^2.0.1 + http: ^1.1.0 + path_provider: ^2.0.11 + mobx: ^2.0.7+4 + flutter_mobx: ^2.0.6+1 + intl: ^0.18.0 + encrypt: ^5.0.1 + polyseed: ^0.0.4 + cw_core: + path: ../cw_core + monero: + git: + url: https://github.com/mrcyjanek/monero.dart + ref: d46753eca865e9e56c2f0ef6fe485c42e11982c5 + mutex: ^3.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.7 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +dependency_overrides: + watcher: ^1.1.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The androidPackage and pluginClass identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/env.json b/env.json new file mode 100644 index 000000000..33a4bc52b --- /dev/null +++ b/env.json @@ -0,0 +1,6 @@ +{ + "CW_WIN_APP_NAME":"Cake Wallet", + "CW_WIN_APP_PACKAGE_NAME": "com.cakewallet.cake_wallet", + "CW_WIN_APP_VERSION": "1.0.0", + "CW_WIN_APP_BUILD_NUMBER": "1" +} diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index 95b82d802..917e87cf4 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -7,7 +7,7 @@ **Core Folder/Files Setup** - Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc - Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. -- Fill out the necessary information int he various functions in the files, concerning the wallet name, the native currency type, symbol etc. +- Fill out the necessary information in the various functions in the files, concerning the wallet name, the native currency type, symbol etc. - Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. - If the cryptocurrency for walletx is not available among the default cryptocurrencies, add a new cryptocurrency entry in `cw_core/lib/cryptocurrency.dart`. - Add the newly created cryptocurrency name to the list named `all` in this file. @@ -70,7 +70,7 @@ A `Proxy` class is used to communicate with the specific wallet package we have. outputContent += '\tWalletType.walletx,\n’; } -- Head over to `scripts/android/pubspec.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params. +- Head over to `scripts/android/pubspec_gen.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params. - Repeat this in `scripts/ios/app_config.sh` and `scripts/macos/app_config.sh` - Open a terminal and cd into `scripts/android/`. Run the following commands to run setup configuration scripts(proxy class, add walletx to list of wallet types and add cw_walletx to pubspec). @@ -214,7 +214,7 @@ Now you can run the codebase and successfully create a wallet for type walletX s **Restore Wallet** - Go to `lib/core/seed_validator.dart` - In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds. -- Next, go to `lib/restore_view_model.dart` +- Next, go to `lib/wallet_restore_view_model.dart` - Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key - Add a switch case to handle the various restore modes that walletX supports - Modify the `getCredential` method to handle the restore flows for `WalletType.walletx` diff --git a/howto-build-android.md b/howto-build-android.md index c3fe415ee..4ad88ea0d 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -8,7 +8,7 @@ The following are the system requirements to build CakeWallet for your Android d Ubuntu >= 20.04 Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 3.10.x or earlier +Flutter 3.19.x or earlier ``` ## Building CakeWallet on Android @@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +Need to install flutter with version `3.19.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,7 +66,7 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) +[✓] Flutter (Channel stable, 3.19.x, on Linux, locale en_US.UTF-8) [✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) [✓] Android Studio (version 4.0 or higher) ``` @@ -116,10 +116,6 @@ Build the Monero libraries and their dependencies: `$ ./build_all.sh` -Now the dependencies need to be copied into the CakeWallet project with this command: - -`$ ./copy_monero_deps.sh` - It is now time to change back to the base directory of the CakeWallet source code: `$ cd ../../` diff --git a/howto-build-ios.md b/howto-build-ios.md new file mode 100644 index 000000000..3bb345861 --- /dev/null +++ b/howto-build-ios.md @@ -0,0 +1,101 @@ +# Building CakeWallet for iOS + +## Requirements and Setup + +The following are the system requirements to build CakeWallet for your iOS device. + +``` +macOS >= 14.0 +Xcode 15.3 +Flutter 3.19.x +``` + +## Building CakeWallet on iOS + +These steps will help you configure and execute a build of CakeWallet from its source code. + +### 1. Installing Package Dependencies + +CakeWallet cannot be built without the following packages installed on your build system. + +For installing dependency tools you can use brew [Install brew](https://brew.sh). + +You may easily install them on your build system with the following command: + +`$ brew install cmake xz cocoapods` + +### 2. Installing Xcode + +You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. + +### 3. Installing Flutter + +Need to install flutter with version `3.19.x`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/mobile-ios?tab=download). + +### 4. Verify Installations + +Verify that the Flutter and Xcode have been correctly installed on your system with the following command: + +`$ flutter doctor` + +The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. +``` +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel stable, 3.19.x, on macOS 14.x.x) +[✓] Xcode - develop for iOS and macOS (Xcode 15.3) +``` + +### 5. Acquiring the CakeWallet source code + +Download the source code. + +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` + +Proceed into the source code before proceeding with the next steps: + +`$ cd cake_wallet/scripts/ios/` + +### 6. Execute Build & Setup Commands for CakeWallet + +We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. + +Please pick what app you want to build: cakewallet or monero.com. + +`$ source ./app_env.sh ` +(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`) + +Then run configuration script for setup app name, app icon and etc: + +`$ ./app_config.sh` + +Build the Monero libraries and their dependencies: + +`$ ./build_monero_all.sh` + +It is now time to change back to the base directory of the CakeWallet source code: + +`$ cd ../../` + +Install Flutter package dependencies with this command: + +`$ flutter pub get` + +Your CakeWallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command: + +`$ flutter packages pub run tool/generate_new_secrets.dart` + +Then we need to generate localization files and mobx models. + +`$ ./configure_cake_wallet.sh ios` + +### 7. Build! + +`$ flutter build ios --release` + +Then you can open `ios/Runner.xcworkspace` with Xcode and you can to archive the application. + +Or if you want to run to connected device: + +`$ flutter run --release` + +Copyright (c) 2024 Cake Technologies LLC. diff --git a/howto-build-macos.md b/howto-build-macos.md new file mode 100644 index 000000000..24d3a9d85 --- /dev/null +++ b/howto-build-macos.md @@ -0,0 +1,112 @@ +# Building CakeWallet for macOS + +## Requirements and Setup + +The following are the system requirements to build CakeWallet for your macOS device. + +``` +macOS >= 14.0 +Xcode 15.3 +Flutter 3.19.x +``` + +## Building CakeWallet on macOS + +These steps will help you configure and execute a build of CakeWallet from its source code. + +### 1. Installing Package Dependencies + +CakeWallet cannot be built without the following packages installed on your build system. + +For installing dependency tools you can use brew [Install brew](https://brew.sh). + +You may easily install them on your build system with the following command: + +`$ brew install cmake xz automake autoconf libtool boost@1.76 zmq cocoapods` + +`$ brew link boost@1.76` + +### 2. Installing Xcode + +You may download and install the latest version of [Xcode](https://developer.apple.com/xcode/) from macOS App Store. + +### 3. Installing Flutter + +Need to install flutter with version `3.19.x`. For this please check section [Install Flutter](https://docs.flutter.dev/get-started/install/macos/desktop?tab=download). + +### 4. Verify Installations + +Verify that Flutter and Xcode have been correctly installed on your system with the following command: + +`$ flutter doctor` + +The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. +``` +Doctor summary (to see all details, run flutter doctor -v): +[✓] Flutter (Channel stable, 3.19.x, on macOS 14.x.x) +[✓] Xcode - develop for iOS and macOS (Xcode 15.3) +``` + +### 5. Acquiring the CakeWallet source code + +Download the source code. + +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` + +Proceed into the source code before proceeding with the next steps: + +`$ cd cake_wallet/scripts/macos/` + +### 6. Execute Build & Setup Commands for CakeWallet + +We need to generate project settings like app name, app icon, package name, etc. For this need to setup environment variables and configure project files. + +Please pick what app you want to build: cakewallet or monero.com. + +`$ source ./app_env.sh ` +(it should be like `$ source ./app_env.sh cakewallet` or `$ source ./app_env.sh monero.com`) + +Then run configuration script for setup app name, app icon and etc: + +`$ ./app_config.sh` + +Build the Monero libraries and their dependencies: + +`$ ./build_monero_all.sh` + +If you be needed to build universal monero lib, then it will require additional steps. Steps for build universal monero lib on mac with Apple Silicon (arm64): + +- Need to install Rosetta: `$ softwareupdate --install-rosetta` +- Need to install [Brew](https://brew.sh/) with rosetta: `$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` (or take another way to install brew, but be use that you have installed it into /usr/local as it's using for x86_64 macs) +- Install dependencies for build monero wallet lib for x86_64 with brew: `$ arch -x86_64 /usr/local/bin/brew install automake autoconf libtool openssl boost@1.76 zmq` and link installed boost@1.76 for x86_64 `$ arch -x86_64 /usr/local/bin/brew link boost@1.76` +- Run building script with additional argument: `$ ./build_monero_all.sh universal` + +If you will be needed to build monero wallet lib only for x86_64 on arm64 mac, then you need use steps above, but run build script with rosetta without arguments: `$ arch -x86_64 ./build_monero_all.sh`. + +It is now time to change back to the base directory of the CakeWallet source code: + +`$ cd ../../` + +Install Flutter package dependencies with this command: + +`$ flutter pub get` + +Your CakeWallet binary will be built with cryptographic salts, which are used for secure encryption of your data. You may generate these secret salts with the following command: + +`$ flutter packages pub run tool/generate_new_secrets.dart` + +Then we need to generate localization files and mobx models. + +`$ ./configure_cake_wallet.sh macos` + +### 7. Build! + +`$ flutter build macos --release` + +Then you can open `macos/Runner.xcworkspace` with Xcode and you can to archive the application. + +Or if you want to run to connected device: + +`$ flutter run --release` + +Copyright (c) 2024 Cake Technologies LLC. diff --git a/ios/MoneroWallet.framework/Info.plist b/ios/MoneroWallet.framework/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..8858589f7070c754a01b0519387f9cab5f664005 GIT binary patch literal 793 zcmZXQ&rcIU6vtUcNFuadk3vEj@Mp#?9$l6SH&kckVuTD5sjMYb$}%;&g-9bW<~Q zZM}D^YevHoE&4RdtOSg=OldOi)#x7O!nLX6S7?U`$CO6nT8(<$D3gq+BC&RuLrZ$} z+R}_NCw^OacJCKcO2$~3Si7V{jeR%FrsAx=BRs!9QTILObWRro*A2_G6_4zi(o{?q zoVL)I<%d#;xBpMnSX|G)pjP0MZQfgPRoE`$)H9`YwNRnY1Lo0IxFoaaDsjm+HkkGv_d;rn^AA8S~!N+h|T!EDJ4$U?sLt)zkO#Du`Hc+9O4IFXu{|T z6m>O=!l9n16V9pMWbRJ*6kT;$&Km0Cf>O(<`HZSmsPjavWft<8Otuj>8EeJ*x~|H~ z;Y@>-dtgb|mt@71W-MXL#C189!&_uRSLS@rmMu=4j;xx>;q5B%?4|IRh)DH_w(tcP z)~+X?7WL`geF;lo^fcAg#e7D|5 3.5) - SwiftProtobuf (~> 1.0) - - SDWebImage (5.18.11): - - SDWebImage/Core (= 5.18.11) - - SDWebImage/Core (5.18.11) + - SDWebImage (5.19.4): + - SDWebImage/Core (= 5.19.4) + - SDWebImage/Core (5.19.4) - sensitive_clipboard (0.0.1): - Flutter - share_plus (0.0.1): @@ -149,9 +126,9 @@ PODS: - FlutterMacOS - sp_scanner (0.0.1): - Flutter - - SwiftProtobuf (1.25.2) - - SwiftyGif (5.4.4) - - Toast (4.1.0) + - SwiftProtobuf (1.26.0) + - SwiftyGif (5.4.5) + - Toast (4.1.1) - uni_links (0.0.1): - Flutter - UnstoppableDomainsResolution (4.0.0): @@ -169,7 +146,6 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -220,8 +196,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" cw_haven: :path: ".symlinks/plugins/cw_haven/ios" - cw_monero: - :path: ".symlinks/plugins/cw_monero/ios" cw_shared_external: :path: ".symlinks/plugins/cw_shared_external/ios" device_display_brightness: @@ -277,15 +251,14 @@ SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d - CryptoSwift: b9c701d6f5011df23794dbf7f2e480a77835d83d + CryptoSwift: c63a805d8bb5e5538e88af4e44bb537776af11ea cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a - cw_monero: 4cf3b96f2da8e95e2ef7d6703dd4d2c509127b7d cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: b22617f40038496deffba44747101255cee005b0 - DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac - DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 @@ -300,17 +273,17 @@ SPEC CHECKSUMS: package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - Protobuf: 8e9074797a13c484a79959fdb819ef4ae6da7dbe - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + Protobuf: fb2c13674723f76ff6eede14f78847a776455fa2 + ReachabilitySwift: 7f151ff156cea1481a8411701195ac6a984f4979 reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c - SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 + SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 - SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 - SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: ec33c32b8688982cecc6348adeae667c1b9938da + SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2196bc289..417c522a6 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,32 +8,70 @@ /* Begin PBXBuildFile section */ 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44A7192518EF8000B570ED /* decrypt.swift */; }; + 0C50DFB92BF3CB56002B0EB3 /* MoneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D68C8264854B60011B691 /* secRandom.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */ = {isa = PBXBuildFile; fileRef = 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 495FEFF9B395392FED3425DE /* TaskProtocol.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0F42D8065219E0653321EE2B /* TaskProtocol.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 4DFD1BB54A3A50573E19A583 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C663361C56EBB242598F609 /* Pods_Runner.framework */; }; + 525A2200C6C2A43EDC5C8FC5 /* BreezSDKConnector.swift in Resources */ = {isa = PBXBuildFile; fileRef = 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + 6909E1D79C9986ADF2DE41E9 /* LnurlPayInvoice.swift in Resources */ = {isa = PBXBuildFile; fileRef = DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + 724FDA327BF191BC29DCAA2E /* Constants.swift in Resources */ = {isa = PBXBuildFile; fileRef = 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + 73138617307FA4F838D21D62 /* ServiceLogger.swift in Resources */ = {isa = PBXBuildFile; fileRef = F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */; }; + A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */ = {isa = PBXBuildFile; fileRef = 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */ = {isa = PBXBuildFile; fileRef = 91C524F800843E0A3F17E004 /* ReceivePayment.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */ = {isa = PBXBuildFile; fileRef = ABD6FCBB0F4244B090459128 /* BreezSDK.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + F5EE19868D6F10D814BF73AD /* SDKNotificationService.swift in Resources */ = {isa = PBXBuildFile; fileRef = 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + CE5E8A222BEE19C700608EA1 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CE291CFE2C15DB9A00B9F709 /* WowneroWallet.framework in CopyFiles */, + 0C50DFB92BF3CB56002B0EB3 /* MoneroWallet.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 0C400E0F25B21ABB0025E469 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; + 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MoneroWallet.framework; sourceTree = ""; }; 0C9986A3251A932F00D566FD /* CryptoSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; + 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Constants.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Constants.swift"; sourceTree = ""; }; + 0F42D8065219E0653321EE2B /* TaskProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TaskProtocol.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/TaskProtocol.swift"; sourceTree = ""; }; 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDKConnector.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDKConnector.swift"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3C663361C56EBB242598F609 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SDKNotificationService.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/SDKNotificationService.swift"; sourceTree = ""; }; + 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInfo.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInfo.swift"; sourceTree = ""; }; 5AFFEBFC279AD49C00F906A4 /* wakeLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = wakeLock.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResourceHelper.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ResourceHelper.swift"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7D3364C03978A8A74B6D586E /* LnurlPay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPay.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPay.swift"; sourceTree = ""; }; + 91C524F800843E0A3F17E004 /* ReceivePayment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ReceivePayment.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/ReceivePayment.swift"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -41,8 +79,14 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedeemSwap.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/RedeemSwap.swift"; sourceTree = ""; }; 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + ABD6FCBB0F4244B090459128 /* BreezSDK.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BreezSDK.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/BreezSDK.swift"; sourceTree = ""; }; AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; + CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WowneroWallet.framework; sourceTree = ""; }; + DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; + F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceLogger.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceLogger.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -66,6 +110,14 @@ name = Frameworks; sourceTree = ""; }; + 0B80439B9064C9708DDB0ADA /* breez_sdk-OnDemandResources */ = { + isa = PBXGroup; + children = ( + ADEC151FA90C8F1EBCDA8CA3 /* BreezSDK */, + ); + name = "breez_sdk-OnDemandResources"; + sourceTree = ""; + }; 0C44A7182518EF4A00B570ED /* CakeWallet */ = { isa = PBXGroup; children = ( @@ -82,6 +134,7 @@ 11F9FC13F9EE2A705B213FA9 /* Pods-Runner.debug.xcconfig */, 1F083F2041D1F553F2AF8B62 /* Pods-Runner.release.xcconfig */, AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */, + 0B80439B9064C9708DDB0ADA /* breez_sdk-OnDemandResources */, ); path = Pods; sourceTree = ""; @@ -100,6 +153,8 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + CE291CFD2C15DB9A00B9F709 /* WowneroWallet.framework */, + 0C50DFB82BF3CB56002B0EB3 /* MoneroWallet.framework */, 0C44A7182518EF4A00B570ED /* CakeWallet */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, @@ -134,6 +189,26 @@ path = Runner; sourceTree = ""; }; + ADEC151FA90C8F1EBCDA8CA3 /* BreezSDK */ = { + isa = PBXGroup; + children = ( + ABD6FCBB0F4244B090459128 /* BreezSDK.swift */, + 1FB06A93B13D606F06B3924D /* BreezSDKConnector.swift */, + 0CCA7ADAD6FF9185EBBB2BCA /* Constants.swift */, + 78D25C60B94E9D9E48D52E5E /* ResourceHelper.swift */, + 41102141140E57B1DC27FBA1 /* SDKNotificationService.swift */, + B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */, + F42258C3697CFE3C8C8D1933 /* ServiceLogger.swift */, + 0F42D8065219E0653321EE2B /* TaskProtocol.swift */, + 7D3364C03978A8A74B6D586E /* LnurlPay.swift */, + 58C22CBD8C22B9D6023D59F8 /* LnurlPayInfo.swift */, + DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */, + 91C524F800843E0A3F17E004 /* ReceivePayment.swift */, + 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */, + ); + name = BreezSDK; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -142,6 +217,7 @@ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( B91154210ADCED81FBF06A85 /* [CP] Check Pods Manifest.lock */, + CE5E8A222BEE19C700608EA1 /* CopyFiles */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -164,6 +240,9 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { + KnownAssetTags = ( + BreezSDK, + ); LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -201,6 +280,19 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + D3AD73A327249AFE8F016A51 /* BreezSDK.swift in Resources */, + 525A2200C6C2A43EDC5C8FC5 /* BreezSDKConnector.swift in Resources */, + 724FDA327BF191BC29DCAA2E /* Constants.swift in Resources */, + 2193F104374FA2746CE8945B /* ResourceHelper.swift in Resources */, + F5EE19868D6F10D814BF73AD /* SDKNotificationService.swift in Resources */, + B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */, + 73138617307FA4F838D21D62 /* ServiceLogger.swift in Resources */, + 495FEFF9B395392FED3425DE /* TaskProtocol.swift in Resources */, + A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */, + CFEFC24F82F78FE747DF1D22 /* LnurlPayInfo.swift in Resources */, + 6909E1D79C9986ADF2DE41E9 /* LnurlPayInvoice.swift in Resources */, + D0D7A0D4E13F31C4E02E235B /* ReceivePayment.swift in Resources */, + A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -391,7 +483,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -537,7 +629,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -575,7 +667,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.fotolockr.cakewallet"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 83e60b542..aec00022b 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -200,7 +200,7 @@ solana-wallet - + CFBundleTypeRole Viewer CFBundleURLName @@ -220,6 +220,26 @@ tron-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + wownero + CFBundleURLSchemes + + wownero + + + + CFBundleTypeRole + Viewer + CFBundleURLName + wownero-wallet + CFBundleURLSchemes + + wownero-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/ios/WowneroWallet.framework/Info.plist b/ios/WowneroWallet.framework/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..61ab961f43e56ddd3df1dc28cb158225c781ff60 GIT binary patch literal 811 zcmZWl%Wl&^6rH&g2$a(4gF;_CN-5=K>@;n~s#4=rimJ4sc9VcYt*J9fhK@bPu>)Q3 z1uR$~A=C{j5@LbGiXDHzhEG7OSn>lbaNL+uF`Ikm-h0kH_iTHDw*poDPDU^s>p6bn zWa`xE-ZOn?`^muI(D1pD^A|?P#wR8(Ub=kc>h!gl*&8?SD0j!I*_GwctqHb99ri#s z4Sltfm36aX%NlzaSC&IAY8DwyW_8wPLV6B!gALZ;(zQr`(kn5)6<3C0RDs$}?y_|w z{%z@IQP7|+eW$aPR>hJy)HJJ2s&zKzsbX#;z%u2`Og83Gi*vxORCn5J)Ejp6hEK5DQ%>@rQ zi|9##yW9z#b>n}=d@Ztr*E#dIHuDGI73y%YYmtG&v%9!z*Wa$Q1BonyH(VNoeq=)b zAt;`DRF+R&=F2h_f(2FXHKogBOIuVjrZzvPmeN`-t*IN#y|0zhbGdXnQ%Q!*l|p&d z&=hrls=|vM;JRTfDC$v8i%i+lQK;t$rbJonNlOu;?I^aOZbQ*5b^rP%^__)iXN0%N zcn49JffUl-@s%SQ-`%=RFafh8v8>JK(rShpOG`^Ag%**H)=&vi^c=lJ@6l)U75&6L zcpBfsI<|2ezrcI=6aIp~;UD-H{tXyX& _exportBackupV2(String password) async { final zipEncoder = ZipFileEncoder(); - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final now = DateTime.now(); final tmpDir = Directory('${appDir.path}/~_BACKUP_TMP'); final archivePath = '${tmpDir.path}/backup_${now.toString()}.zip'; @@ -116,7 +117,7 @@ class BackupService { } Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -139,7 +140,7 @@ class BackupService { } Future _importBackupV2(Uint8List data, String password) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final decryptedData = await _decryptV2(data, password); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -172,7 +173,7 @@ class BackupService { } Future> _reloadHiveWalletInfoBox() async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); await CakeHive.close(); CakeHive.init(appDir.path); @@ -184,7 +185,7 @@ class BackupService { } Future _importPreferencesDump() async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final preferencesFile = File('${appDir.path}/~_preferences_dump'); if (!preferencesFile.existsSync()) { @@ -361,7 +362,7 @@ class BackupService { Future _importKeychainDumpV1(String password, {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); final decryptedKeychainDumpFileData = await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); @@ -387,7 +388,7 @@ class BackupService { Future _importKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); final decryptedKeychainDumpFileData = await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 9fb839ea2..2d2a0c6ee 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -43,7 +44,9 @@ class SeedValidator extends Validator { return solana!.getSolanaWordList(language); case WalletType.tron: return tron!.getTronWordList(language); - default: + case WalletType.wownero: + return wownero!.getWowneroWordList(language); + case WalletType.none: return []; } } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 1fa50a6be..2f3acb6c9 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -81,6 +81,7 @@ class WalletCreationService { case WalletType.tron: return true; case WalletType.monero: + case WalletType.wownero: case WalletType.none: case WalletType.bitcoin: case WalletType.litecoin: diff --git a/lib/di.dart b/lib/di.dart index 2c09d8cbc..9136260e5 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -215,6 +215,7 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_new_vm.dart'; import 'package:cake_wallet/view_model/wallet_restore_view_model.dart'; import 'package:cake_wallet/view_model/wallet_seed_view_model.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_info.dart'; @@ -900,6 +901,7 @@ Future setup({ return bitcoinCash! .createBitcoinCashWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.nano: + case WalletType.banano: return nano!.createNanoWalletService(_walletInfoSource); case WalletType.polygon: return polygon!.createPolygonWalletService(_walletInfoSource); @@ -907,7 +909,9 @@ Future setup({ return solana!.createSolanaWalletService(_walletInfoSource); case WalletType.tron: return tron!.createTronWalletService(_walletInfoSource); - default: + case WalletType.wownero: + return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); + case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } }); diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart index 5db42381e..a373ca0ad 100644 --- a/lib/entities/background_tasks.dart +++ b/lib/entities/background_tasks.dart @@ -31,8 +31,6 @@ void callbackDispatcher() { final walletLoadingService = getIt.get(); - final node = getIt.get().getCurrentNode(WalletType.monero); - final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType); WalletBase? wallet; @@ -42,23 +40,25 @@ void callbackDispatcher() { final List moneroWallets = getIt .get() .wallets - .where((element) => element.type == WalletType.monero) + .where((element) => [WalletType.monero, WalletType.wownero].contains(element.type)) .toList(); for (int i = 0; i < moneroWallets.length; i++) { - wallet = await walletLoadingService.load(WalletType.monero, moneroWallets[i].name); - + wallet = + await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name); + final node = getIt.get().getCurrentNode(moneroWallets[i].type); await wallet.connectToNode(node: node); await wallet.startSync(); } } else { /// if the user chose to sync only active wallet /// if the current wallet is monero; sync it only - if (typeRaw == WalletType.monero.index) { + if (typeRaw == WalletType.monero.index || typeRaw == WalletType.wownero.index) { final name = getIt.get().getString(PreferencesKey.currentWalletName); - wallet = await walletLoadingService.load(WalletType.monero, name!); + wallet = await walletLoadingService.load(WalletType.values[typeRaw!], name!); + final node = getIt.get().getCurrentNode(WalletType.values[typeRaw]); await wallet.connectToNode(node: node); await wallet.startSync(); @@ -66,7 +66,7 @@ void callbackDispatcher() { } if (wallet?.syncStatus.progress() == null) { - return Future.error("No Monero wallet found"); + return Future.error("No Monero/Wownero wallet found"); } for (int i = 0;; i++) { diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 891d53f59..71a971a9a 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -5,8 +5,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; @@ -39,6 +39,7 @@ const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; const tronDefaultNodeUri = 'trx.nownodes.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; +const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; Future defaultSettingsMigration( {required int version, @@ -231,7 +232,8 @@ Future defaultSettingsMigration( await _switchElectRsNode(nodes, sharedPreferences); break; case 36: - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWowneroNodeList(nodes: nodes); + await changeWowneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); @@ -305,7 +307,7 @@ Future _updateMoneroPriority(SharedPreferences sharedPreferences) async { Future _validateWalletInfoBoxData(Box walletInfoSource) async { try { - final root = await getApplicationDocumentsDirectory(); + final root = await getAppDir(); for (var type in WalletType.values) { if (type == WalletType.none) { @@ -498,6 +500,32 @@ Node? getTronDefaultNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); } +Node getWowneroDefaultNode({required Box nodes}) { + final timeZone = DateTime.now().timeZoneOffset.inHours; + var nodeUri = ''; + + if (timeZone >= 1) { + // Eurasia + nodeUri = 'node2.monerodevs.org.lol:34568'; + } else if (timeZone <= -4) { + // America + nodeUri = 'node3.monerodevs.org:34568'; + } + + if (nodeUri == '') { + return nodes.values.where((element) => element.type == WalletType.wownero).first; + } + + try { + return nodes.values.firstWhere( + (Node node) => node.uriRaw == nodeUri, + orElse: () => nodes.values.where((element) => element.type == WalletType.wownero).first, + ); + } catch (_) { + return nodes.values.where((element) => element.type == WalletType.wownero).first; + } +} + Future insecureStorageMigration({ required SharedPreferences sharedPreferences, required SecureStorage secureStorage, @@ -898,6 +926,7 @@ Future checkCurrentNodes( sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); + final currentWowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -920,6 +949,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId); final currentTronNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); + final currentWowneroNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -997,6 +1028,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); } + + if (currentWowneroNodeServer == null) { + final node = Node(uri: wowneroDefaultNodeUri, type: WalletType.wownero); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1063,6 +1100,23 @@ Future changeEthereumCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); } +Future addWowneroNodeList({required Box nodes}) async { + final nodeList = await loadDefaultWowneroNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeWowneroCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getWowneroDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); +} + Future addNanoNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoNodes(); for (var node in nodeList) { diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index cfb850889..23d27dd38 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -63,6 +63,8 @@ class LanguageService { static final list = {}; + static const defaultLocale = 'en'; + static void loadLocaleList() { supportedLocales.forEach((key, value) { if (locales.contains(key)) { @@ -72,9 +74,16 @@ class LanguageService { } static Future localeDetection() async { - var locale = await Devicelocale.currentLocale ?? ''; - locale = Intl.shortLocale(locale); + try { + var locale = await Devicelocale.currentLocale ?? ''; + locale = Intl.shortLocale(locale); - return list.keys.contains(locale) ? locale : 'en'; + if (list.keys.contains(locale)) { + return locale; + } + return LanguageService.defaultLocale; + } catch(_) { + return LanguageService.defaultLocale; + } } } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index c1211d2fe..85e37a7bc 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -183,6 +183,23 @@ Future> loadDefaultTronNodes() async { return nodes; } +Future> loadDefaultWowneroNodes() async { + final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + + node.type = WalletType.wownero; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index fdcd54c9c..e1ee0ada3 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -15,6 +15,7 @@ class PreferencesKey { static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentTronNodeIdKey = 'current_node_id_trx'; + static const currentWowneroNodeIdKey = 'current_node_id_wow'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; @@ -43,6 +44,7 @@ class PreferencesKey { static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; + static const wowneroTransactionPriority = 'current_fee_priority_wownero'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 0151c8115..534287494 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -11,6 +12,8 @@ List priorityForWalletType(WalletType type) { switch (type) { case WalletType.monero: return monero!.getTransactionPriorities(); + case WalletType.wownero: + return wownero!.getTransactionPriorities(); case WalletType.bitcoin: return bitcoin!.getTransactionPriorities(); case WalletType.litecoin: diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 37a492987..da7bae4c1 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -51,6 +51,7 @@ class ProvidersHelper { switch (walletType) { case WalletType.nano: case WalletType.banano: + case WalletType.wownero: return [ProviderType.askEachTime, ProviderType.onramper]; case WalletType.monero: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.dfx]; @@ -114,6 +115,7 @@ class ProvidersHelper { case WalletType.banano: case WalletType.none: case WalletType.haven: + case WalletType.wownero: return []; } } diff --git a/lib/entities/seed_type.dart b/lib/entities/seed_type.dart index ab9965528..bc2f6cff7 100644 --- a/lib/entities/seed_type.dart +++ b/lib/entities/seed_type.dart @@ -10,6 +10,7 @@ class SeedType extends EnumerableItem with Serializable { static const legacy = SeedType(raw: 0, title: 'Legacy (25 words)'); static const polyseed = SeedType(raw: 1, title: 'Polyseed (16 words)'); + static const wowneroSeed = SeedType(raw: 2, title: 'Wownero (14 words)'); static SeedType deserialize({required int raw}) { switch (raw) { @@ -17,6 +18,8 @@ class SeedType extends EnumerableItem with Serializable { return legacy; case 1: return polyseed; + case 2: + return wowneroSeed; default: throw Exception('Unexpected token: $raw for SeedType deserialize'); } @@ -29,6 +32,8 @@ class SeedType extends EnumerableItem with Serializable { return S.current.seedtype_legacy; case SeedType.polyseed: return S.current.seedtype_polyseed; + case SeedType.wowneroSeed: + return S.current.seedtype_wownero; default: return ''; } diff --git a/lib/main.dart b/lib/main.dart index fbe77bd31..8539ac803 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,45 +1,45 @@ import 'dart:async'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/contact.dart'; +import 'package:cake_wallet/entities/default_settings_migration.dart'; +import 'package:cake_wallet/entities/get_encryption_key.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/language_service.dart'; -import 'package:cake_wallet/buy/order.dart'; +import 'package:cake_wallet/entities/template.dart'; +import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/exchange/exchange_template.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/locales/locale.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/reactions/bootstrap.dart'; +import 'package:cake_wallet/router.dart' as Router; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/root/root.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; -import 'package:cw_core/address_info.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:hive/hive.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/router.dart' as Router; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/reactions/bootstrap.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/entities/transaction_description.dart'; -import 'package:cake_wallet/entities/get_encryption_key.dart'; -import 'package:cake_wallet/entities/contact.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cake_wallet/entities/default_settings_migration.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/entities/template.dart'; -import 'package:cake_wallet/exchange/trade.dart'; -import 'package:cake_wallet/exchange/exchange_template.dart'; -import 'package:cake_wallet/src/screens/root/root.dart'; -import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cw_core/cake_hive.dart'; +import 'package:hive/hive.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; final navigatorKey = GlobalKey(); @@ -103,7 +103,8 @@ Future main() async { } Future initializeAppConfigs() async { - final appDir = await getApplicationDocumentsDirectory(); + setRootDirFromEnv(); + final appDir = await getAppDir(); CakeHive.init(appDir.path); if (!CakeHive.isAdapterRegistered(Contact.typeId)) { diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index b4d85089a..c1384a3df 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -341,4 +341,9 @@ class CWMonero extends Monero { final moneroWallet = wallet as MoneroWallet; await moneroWallet.updateUnspent(); } + + @override + Future getCurrentHeight() async { + return monero_wallet_api.getCurrentHeight(); + } } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index a6ce2bae9..6f1ba1d8c 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -71,6 +71,7 @@ void startCurrentWalletChangeReaction( .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); if (wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash) { diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index bec10435e..7a2055930 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sideba import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/bottom_sheet_listener.dart'; import 'package:cake_wallet/src/widgets/gradient_background.dart'; +import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart'; import 'package:cake_wallet/src/widgets/services_updates_widget.dart'; import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart'; import 'package:cake_wallet/themes/extensions/sync_indicator_theme.dart'; @@ -351,10 +352,12 @@ class _DashboardPageView extends BasePage { _showVulnerableSeedsPopup(context); + _showHavenPopup(context); + var needToPresentYat = false; var isInactive = false; - _onInactiveSub = rootKey.currentState!.isInactive.listen( + _onInactiveSub = rootKey.currentState?.isInactive.listen( (inactive) { isInactive = inactive; @@ -428,4 +431,22 @@ class _DashboardPageView extends BasePage { ); } } + + void _showHavenPopup(BuildContext context) async { + final List havenWalletList = await dashboardViewModel.checkForHavenWallets(); + + if (havenWalletList.isNotEmpty) { + Future.delayed( + Duration(seconds: 1), + () { + showPopUp( + context: context, + builder: (BuildContext context) { + return HavenWalletRemovalPopup(havenWalletList); + }, + ); + }, + ); + } + } } diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 663675849..46e63af01 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -40,6 +40,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 7eda20bff..78d8abc95 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -36,7 +36,8 @@ class MenuWidgetState extends State { this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'), this.polygonIcon = Image.asset('assets/images/matic_icon.png'), this.solanaIcon = Image.asset('assets/images/sol_icon.png'), - this.tronIcon = Image.asset('assets/images/trx_icon.png'); + this.tronIcon = Image.asset('assets/images/trx_icon.png'), + this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'); final largeScreen = 731; @@ -60,6 +61,7 @@ class MenuWidgetState extends State { Image polygonIcon; Image solanaIcon; Image tronIcon; + Image wowneroIcon; @override void initState() { @@ -236,6 +238,8 @@ class MenuWidgetState extends State { return solanaIcon; case WalletType.tron: return tronIcon; + case WalletType.wownero: + return wowneroIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index 8d46827eb..306c41479 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -82,7 +82,7 @@ class _WalletNameFormState extends State { void initState() { _stateReaction ??= reaction((_) => _walletNewVM.state, (ExecutionState state) async { if (state is ExecutedSuccessfullyState) { - Navigator.of(navigatorKey.currentContext!) + Navigator.of(navigatorKey.currentContext ?? context) .pushNamed(Routes.preSeedPage, arguments: _walletNewVM.seedPhraseWordsLength); } diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index c59ae4ad0..4b2327c43 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -33,6 +33,7 @@ class RescanPage extends BasePage { doSingleScan: _rescanViewModel.doSingleScan, toggleSingleScan: () => _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, + walletType: _rescanViewModel.wallet.type, )), Observer( builder: (_) => LoadingPrimaryButton( diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 588fd7187..f8336a2e8 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -171,6 +171,7 @@ class WalletRestoreFromKeysFromState extends State { hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, onHeightChange: (_) => null, onHeightOrDateEntered: widget.onHeightOrDateEntered, + walletType: widget.walletRestoreViewModel.type, ), ], ); diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 288862ce7..1f22af0cb 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -83,12 +83,17 @@ class WalletRestoreFromSeedFormState extends State { } void onSeedChange(String seed) { - if (widget.type == WalletType.monero && Polyseed.isValidSeed(seed)) { + if ((widget.type == WalletType.monero || widget.type == WalletType.wownero) && + Polyseed.isValidSeed(seed)) { final lang = PolyseedLang.getByPhrase(seed); _changeSeedType(SeedType.polyseed); _changeLanguage(lang.nameEnglish); } + if (widget.type == WalletType.wownero && seed.split(" ").length == 14) { + _changeSeedType(SeedType.wowneroSeed); + _changeLanguage("English"); + } widget.onSeedChange?.call(seed); } @@ -142,14 +147,18 @@ class WalletRestoreFromSeedFormState extends State { language: language, type: widget.type, onSeedChange: onSeedChange), - if (widget.type == WalletType.monero) + if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( onTap: () async { await showPopUp( context: context, builder: (_) => Picker( - items: SeedType.all, - selectedAtIndex: isPolyseed ? 1 : 0, + items: _getItems(), + selectedAtIndex: isPolyseed + ? 1 + : seedTypeController.value.text.contains("14") + ? 2 + : 0, mainAxisAlignment: MainAxisAlignment.start, onItemSelected: _changeSeedType, isSeparated: false, @@ -168,7 +177,7 @@ class WalletRestoreFromSeedFormState extends State { ), ), ), - if (widget.displayLanguageSelector) + if (!seedTypeController.value.text.contains("14") && widget.displayLanguageSelector) GestureDetector( onTap: () async { await showPopUp( @@ -192,12 +201,14 @@ class WalletRestoreFromSeedFormState extends State { ), ), ), - if (!isPolyseed && widget.displayBlockHeightSelector) + if ((!isPolyseed) && widget.displayBlockHeightSelector) BlockchainHeightWidget( - focusNode: widget.blockHeightFocusNode, - key: blockchainHeightKey, - onHeightOrDateEntered: widget.onHeightOrDateEntered, - hasDatePicker: widget.type == WalletType.monero), + focusNode: widget.blockHeightFocusNode, + key: blockchainHeightKey, + onHeightOrDateEntered: widget.onHeightOrDateEntered, + hasDatePicker: widget.type == WalletType.monero || widget.type == WalletType.wownero, + walletType: widget.type, + ), if (widget.displayPassphrase) ...[ const SizedBox(height: 10), BaseTextFormField( @@ -209,7 +220,9 @@ class WalletRestoreFromSeedFormState extends State { ])); } - bool get isPolyseed => widget.seedTypeViewModel.moneroSeedType == SeedType.polyseed; + bool get isPolyseed => + widget.seedTypeViewModel.moneroSeedType == SeedType.polyseed && + (widget.type == WalletType.monero || widget.type == WalletType.wownero); Widget get expandIcon => Container( padding: EdgeInsets.all(18), @@ -223,7 +236,11 @@ class WalletRestoreFromSeedFormState extends State { ); void _changeLanguage(String language) { - final setLang = isPolyseed ? "POLYSEED_$language" : language; + final setLang = isPolyseed + ? "POLYSEED_$language" + : seedTypeController.value.text.contains("14") + ? "WOWSEED_" + language + : language; setState(() { this.language = setLang; seedWidgetStateKey.currentState!.changeSeedLanguage(setLang); @@ -244,4 +261,15 @@ class WalletRestoreFromSeedFormState extends State { void _setSeedType(SeedType item) { seedTypeController.text = item.toString(); } + + List _getItems() { + switch (widget.type) { + case WalletType.monero: + return [SeedType.legacy, SeedType.polyseed]; + case WalletType.wownero: + return [SeedType.legacy, SeedType.polyseed, SeedType.wowneroSeed]; + default: + return [SeedType.legacy]; + } + } } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 8e6ee0983..a9bd52b26 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -20,7 +20,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; -import 'package:polyseed/polyseed.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class WalletRestorePage extends BasePage { @@ -48,8 +47,7 @@ class WalletRestorePage extends BasePage { } }, onSeedChange: (String seed) { - final isPolyseed = - walletRestoreViewModel.type == WalletType.monero && Polyseed.isValidSeed(seed); + final isPolyseed = walletRestoreViewModel.isPolyseed(seed); _validateOnChange(isPolyseed: isPolyseed); }, onLanguageChange: (String language) { @@ -103,11 +101,11 @@ class WalletRestorePage extends BasePage { @override Function(BuildContext)? get pushToNextWidget => (context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.focusedChild?.unfocus(); - } - }; + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.focusedChild?.unfocus(); + } + }; @override Widget body(BuildContext context) { @@ -253,12 +251,14 @@ class WalletRestorePage extends BasePage { bool _isValidSeed() { final seedPhrase = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; - if (walletRestoreViewModel.type == WalletType.monero && Polyseed.isValidSeed(seedPhrase)) - return true; + if (walletRestoreViewModel.isPolyseed(seedPhrase)) return true; final seedWords = seedPhrase.split(' '); + if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true; + if ((walletRestoreViewModel.type == WalletType.monero || + walletRestoreViewModel.type == WalletType.wownero || walletRestoreViewModel.type == WalletType.haven) && seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) { return false; diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index a89d5e66f..2a4841608 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -106,6 +106,7 @@ class WalletListBodyState extends State { final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24); final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24); final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); + final wowneroIcon = Image.asset('assets/images/wownero_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -319,6 +320,7 @@ class WalletListBodyState extends State { case WalletType.bitcoinCash: return bitcoinCashIcon; case WalletType.nano: + case WalletType.banano: return nanoIcon; case WalletType.polygon: return polygonIcon; @@ -326,7 +328,9 @@ class WalletListBodyState extends State { return solanaIcon; case WalletType.tron: return tronIcon; - default: + case WalletType.wownero: + return wowneroIcon; + case WalletType.none: return nonWalletTypeIcon; } } diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index d85680cc8..4023e66ad 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -2,6 +2,8 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/utils/date_picker.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -18,6 +20,7 @@ class BlockchainHeightWidget extends StatefulWidget { this.isSilentPaymentsScan = false, this.toggleSingleScan, this.doSingleScan = false, + required this.walletType, }) : super(key: key); final Function(int)? onHeightChange; @@ -27,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget { final bool isSilentPaymentsScan; final bool doSingleScan; final Function()? toggleSingleScan; + final WalletType walletType; @override State createState() => BlockchainHeightState(); @@ -160,7 +164,13 @@ class BlockchainHeightState extends State { if (widget.isSilentPaymentsScan) { height = bitcoin!.getHeightByDate(date: date); } else { - height = monero!.getHeightByDate(date: date); + if (widget.walletType == WalletType.monero) { + height = monero!.getHeightByDate(date: date); + } else { + assert(widget.walletType == WalletType.wownero, + "unknown currency in BlockchainHeightWidget"); + height = wownero!.getHeightByDate(date: date); + } } setState(() { dateController.text = DateFormat('yyyy-MM-dd').format(date); diff --git a/lib/src/widgets/haven_wallet_removal_popup.dart b/lib/src/widgets/haven_wallet_removal_popup.dart new file mode 100644 index 000000000..05a38c41e --- /dev/null +++ b/lib/src/widgets/haven_wallet_removal_popup.dart @@ -0,0 +1,91 @@ +import 'package:cake_wallet/src/widgets/alert_background.dart'; +import 'package:cake_wallet/src/widgets/alert_close_button.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:flutter/material.dart'; + +class HavenWalletRemovalPopup extends StatelessWidget { + final List affectedWalletNames; + + const HavenWalletRemovalPopup(this.affectedWalletNames, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + AlertBackground( + child: AlertDialog( + insetPadding: EdgeInsets.only(left: 16, right: 16, bottom: 48), + elevation: 0.0, + contentPadding: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(30))), + content: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + gradient: LinearGradient(colors: [ + Theme.of(context).extension()!.firstGradientBackgroundColor, + Theme.of(context) + .extension()! + .secondGradientBackgroundColor, + ], begin: Alignment.centerLeft, end: Alignment.centerRight)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + alignment: Alignment.bottomCenter, + child: DefaultTextStyle( + style: TextStyle( + decoration: TextDecoration.none, + fontSize: 24.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Theme.of(context).extension()!.textColor, + ), + child: Text("Emergency Notice"), + ), + ), + ), + ), + SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(top: 48, bottom: 16), + child: Container( + width: double.maxFinite, + child: Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + ), + child: Text( + "It looks like you have Haven wallets in your list. Haven is getting removed in next release of Cake Wallet, and you currently have Haven in the following wallets:\n\n[${affectedWalletNames.join(", ")}]\n\nPlease move your funds to other wallet, as you will lose access to your Haven funds in next update.\n\nFor assistance, please use the in-app support or email support@cakewallet.com", + style: TextStyle( + decoration: TextDecoration.none, + fontSize: 16.0, + fontFamily: 'Lato', + color: Theme.of(context) + .extension()! + .textColor, + ), + ), + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + AlertCloseButton(bottom: 30) + ], + ); + } +} diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 05af3f3b1..8e16adbff 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -20,6 +20,7 @@ import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; @@ -27,7 +28,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; -import 'package:package_info/package_info.dart'; +import 'package:cake_wallet/utils/package_info.dart'; import 'package:cake_wallet/di.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -111,6 +112,7 @@ abstract class SettingsStoreBase with Store { required this.silentPaymentsAlwaysScan, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, + TransactionPriority? initialWowneroTransactionPriority, TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialLitecoinTransactionPriority, TransactionPriority? initialEthereumTransactionPriority, @@ -168,6 +170,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.monero] = initialMoneroTransactionPriority; } + if (initialWowneroTransactionPriority != null) { + priority[WalletType.wownero] = initialWowneroTransactionPriority; + } + if (initialBitcoinTransactionPriority != null) { priority[WalletType.bitcoin] = initialBitcoinTransactionPriority; } @@ -249,6 +255,7 @@ abstract class SettingsStoreBase with Store { final String? key; switch (change.key) { case WalletType.monero: + case WalletType.wownero: key = PreferencesKey.moneroTransactionPriority; break; case WalletType.bitcoin: @@ -789,6 +796,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? ethereumTransactionPriority; TransactionPriority? polygonTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; + TransactionPriority? wowneroTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -810,6 +818,10 @@ abstract class SettingsStoreBase with Store { bitcoinCashTransactionPriority = bitcoinCash?.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority) != null) { + wowneroTransactionPriority = wownero?.deserializeWowneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -817,6 +829,7 @@ abstract class SettingsStoreBase with Store { litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium(); ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); + wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( @@ -902,6 +915,7 @@ abstract class SettingsStoreBase with Store { final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); + final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -913,6 +927,7 @@ abstract class SettingsStoreBase with Store { final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); + final wowneroNode = nodeSource.get(wowneroNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -979,6 +994,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.tron] = tronNode; } + if (wowneroNode != null) { + nodes[WalletType.wownero] = wowneroNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1127,6 +1146,7 @@ abstract class SettingsStoreBase with Store { silentPaymentsCardDisplay: silentPaymentsCardDisplay, silentPaymentsAlwaysScan: silentPaymentsAlwaysScan, initialMoneroTransactionPriority: moneroTransactionPriority, + initialWowneroTransactionPriority: wowneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, @@ -1164,6 +1184,10 @@ abstract class SettingsStoreBase with Store { raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? priority[WalletType.monero]!; + priority[WalletType.wownero] = wownero?.deserializeWowneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.wowneroTransactionPriority)!) ?? + priority[WalletType.wownero]!; + if (bitcoin != null && sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority) != null) { priority[WalletType.bitcoin] = bitcoin!.deserializeBitcoinTransactionPriority( @@ -1282,6 +1306,7 @@ abstract class SettingsStoreBase with Store { final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); + final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1292,6 +1317,7 @@ abstract class SettingsStoreBase with Store { final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); + final wowneroNode = nodeSource.get(wowneroNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1332,6 +1358,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.tron] = tronNode; } + if (wowneroNode != null) { + nodes[WalletType.wownero] = wowneroNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1465,6 +1495,9 @@ abstract class SettingsStoreBase with Store { case WalletType.tron: await _sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); break; + case WalletType.wownero: + await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/utils/distribution_info.dart b/lib/utils/distribution_info.dart index 859c507a3..5a2cb8e9d 100644 --- a/lib/utils/distribution_info.dart +++ b/lib/utils/distribution_info.dart @@ -1,5 +1,5 @@ import 'dart:io'; -import 'package:package_info/package_info.dart'; +import 'package:cake_wallet/utils/package_info.dart'; enum DistributionType { googleplay, github, appstore, fdroid } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 5a07ab0f0..b19b1bb7e 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -5,12 +5,12 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/root_dir.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; -import 'package:package_info/package_info.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:cake_wallet/utils/package_info.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ExceptionHandler { @@ -20,7 +20,7 @@ class ExceptionHandler { static void _saveException(String? error, StackTrace? stackTrace, {String? library}) async { if (_file == null) { - final appDocDir = await getApplicationDocumentsDirectory(); + final appDocDir = await getAppDir(); _file = File('${appDocDir.path}/error.txt'); } @@ -53,7 +53,7 @@ class ExceptionHandler { static void _sendExceptionFile() async { try { if (_file == null) { - final appDocDir = await getApplicationDocumentsDirectory(); + final appDocDir = await getAppDir(); _file = File('${appDocDir.path}/error.txt'); } diff --git a/lib/utils/package_info.dart b/lib/utils/package_info.dart new file mode 100644 index 000000000..8b911f887 --- /dev/null +++ b/lib/utils/package_info.dart @@ -0,0 +1,54 @@ +import 'dart:io'; +import 'package:package_info/package_info.dart' as __package_info__; + +abstract class _EnvKeys { + static const kWinAppName = 'CW_WIN_APP_NAME'; + static const kWinAppPackageName = 'CW_WIN_APP_PACKAGE_NAME'; + static const kWinAppVersion = 'CW_WIN_APP_VERSION'; + static const kWinAppBuildNumber = 'CW_WIN_APP_BUILD_NUMBER'; +} + +class PackageInfo { + static Future fromPlatform() async { + if (Platform.isWindows) { + return _windowsPackageInfo; + } + + final packageInfo = await __package_info__.PackageInfo.fromPlatform(); + return PackageInfo._( + appName: packageInfo.appName, + packageName: packageInfo.packageName, + version: packageInfo.version, + buildNumber: packageInfo.buildNumber); + } + + static const _defaultCWAppName = 'Cake Wallet'; + static const _defaultCWAppPackageName = 'com.cakewallet.cake_wallet'; + static const _defaultCWAppVersion = '1.0.0'; + static const _defaultCWAppBuildNumber = '1'; + + static const _windowsPackageInfo = PackageInfo._( + appName: const String + .fromEnvironment(_EnvKeys.kWinAppName, + defaultValue: _defaultCWAppName), + packageName: const String + .fromEnvironment(_EnvKeys.kWinAppPackageName, + defaultValue: _defaultCWAppPackageName), + version: const String + .fromEnvironment(_EnvKeys.kWinAppVersion, + defaultValue: _defaultCWAppVersion), + buildNumber: const String + .fromEnvironment(_EnvKeys.kWinAppBuildNumber, + defaultValue: _defaultCWAppBuildNumber)); + + final String appName; + final String packageName; + final String version; + final String buildNumber; + + const PackageInfo._({ + required this.appName, + required this.packageName, + required this.version, + required this.buildNumber}); +} diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index c87e097c3..73308f15a 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -41,6 +41,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.tron: return true; case WalletType.monero: + case WalletType.wownero: case WalletType.none: case WalletType.bitcoin: case WalletType.litecoin: @@ -51,7 +52,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { } } - bool get hasSeedTypeOption => type == WalletType.monero; + bool get hasSeedTypeOption => type == WalletType.monero || type == WalletType.wownero; @computed bool get addCustomNode => _addCustomNode; diff --git a/lib/view_model/backup_view_model.dart b/lib/view_model/backup_view_model.dart index 1b9b5e4ff..bbd147e2b 100644 --- a/lib/view_model/backup_view_model.dart +++ b/lib/view_model/backup_view_model.dart @@ -4,6 +4,8 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/store/secret_store.dart'; +import 'package:cw_core/root_dir.dart'; +import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:intl/intl.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; @@ -72,7 +74,7 @@ abstract class BackupViewModelBase with Store { } Future saveBackupFileLocally(BackupExportFile backup) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final path = '${appDir.path}/${backup.name}'; final backupFile = File(path); await backupFile.writeAsBytes(backup.content); @@ -80,7 +82,7 @@ abstract class BackupViewModelBase with Store { } Future removeBackupFileLocally(BackupExportFile backup) async { - final appDir = await getApplicationDocumentsDirectory(); + final appDir = await getAppDir(); final path = '${appDir.path}/${backup.name}'; final backupFile = File(path); await backupFile.delete(); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 5ae532bb6..c8acb9c2c 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -125,6 +125,7 @@ abstract class BalanceViewModelBase with Store { String get availableBalanceLabel { switch (wallet.type) { case WalletType.monero: + case WalletType.wownero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: @@ -142,6 +143,7 @@ abstract class BalanceViewModelBase with Store { String get additionalBalanceLabel { switch (wallet.type) { case WalletType.monero: + case WalletType.wownero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index b59dd1592..8f22e5be1 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -303,6 +303,7 @@ abstract class DashboardViewModelBase with Store { bool get hasRescan => wallet.type == WalletType.bitcoin || wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || wallet.type == WalletType.haven; @computed @@ -532,6 +533,11 @@ abstract class DashboardViewModelBase with Store { @action void setSyncAll(bool value) => settingsStore.currentSyncAll = value; + Future> checkForHavenWallets() async { + final walletInfoSource = await CakeHive.openBox(WalletInfo.boxName); + return walletInfoSource.values.where((element) => element.type == WalletType.haven).map((e) => e.name).toList(); + } + Future> checkAffectedWallets() async { try { // await load file diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index fb5348a29..176b4e58d 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -55,8 +56,14 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedPendingStatus { - if (transaction.confirmations >= 0 && transaction.confirmations < 10) { - return ' (${transaction.confirmations}/10)'; + if (balanceViewModel.wallet.type == WalletType.monero || balanceViewModel.wallet.type == WalletType.haven) { + if (transaction.confirmations >= 0 && transaction.confirmations < 10) { + return ' (${transaction.confirmations}/10)'; + } + } else if (balanceViewModel.wallet.type == WalletType.wownero) { + if (transaction.confirmations >= 0 && transaction.confirmations < 3) { + return ' (${transaction.confirmations}/3)'; + } } return ''; } @@ -64,6 +71,7 @@ class TransactionListItem extends ActionListItem with Keyable { String get formattedStatus { if (transaction.direction == TransactionDirection.incoming) { if (balanceViewModel.wallet.type == WalletType.monero || + balanceViewModel.wallet.type == WalletType.wownero || balanceViewModel.wallet.type == WalletType.haven) { return formattedPendingStatus; } @@ -95,7 +103,7 @@ class TransactionListItem extends ActionListItem with Keyable { } catch (e) { return null; } - + return null; } @@ -108,6 +116,11 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: monero!.formatterMoneroAmountToDouble(amount: transaction.amount), price: price); break; + case WalletType.wownero: + amount = calculateFiatAmountRaw( + cryptoAmount: wownero!.formatterWowneroAmountToDouble(amount: transaction.amount), + price: price); + break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 5e0443bf8..b4711068c 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -287,6 +287,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isLowFee { switch (wallet.type) { case WalletType.monero: + case WalletType.wownero: case WalletType.haven: return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); case WalletType.bitcoin: @@ -670,6 +671,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.nano; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.banano: + depositCurrency = CryptoCurrency.banano; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.polygon: depositCurrency = CryptoCurrency.maticpoly; receiveCurrency = CryptoCurrency.xmr; @@ -682,7 +687,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.trx; receiveCurrency = CryptoCurrency.xmr; break; - default: + case WalletType.wownero: + depositCurrency = CryptoCurrency.wow; + receiveCurrency = CryptoCurrency.xmr; + break; + case WalletType.none: break; } } @@ -755,6 +764,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with switch (wallet.type) { case WalletType.monero: case WalletType.haven: + case WalletType.wownero: _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); break; case WalletType.bitcoin: diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 000c9bdea..850e248f2 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -80,6 +80,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { return true; case WalletType.none: case WalletType.monero: + case WalletType.wownero: case WalletType.haven: case WalletType.litecoin: case WalletType.bitcoinCash: diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index a7fe9c6ca..ea1dd574e 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -85,6 +85,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.tron: node = getTronDefaultNode(nodes: _nodeSource)!; break; + case WalletType.wownero: + node = getWowneroDefaultNode(nodes: _nodeSource); + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 1e9aea2c2..f5938911b 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -48,7 +49,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store @observable String address; - bool get hasRestorationHeight => type == WalletType.monero; + bool get hasRestorationHeight => type == WalletType.monero || type == WalletType.wownero; @override WalletCredentials getCredentialsFromRestoredWallet( @@ -74,6 +75,15 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store viewKey: restoreWallet.viewKey ?? '', spendKey: restoreWallet.spendKey ?? '', height: restoreWallet.height ?? 0); + case WalletType.wownero: + return wownero!.createWowneroRestoreWalletFromKeysCredentials( + name: name, + password: password, + language: 'English', + address: restoreWallet.address ?? '', + viewKey: restoreWallet.viewKey ?? '', + spendKey: restoreWallet.spendKey ?? '', + height: restoreWallet.height ?? 0); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials( @@ -137,6 +147,13 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.tron: return tron!.createTronRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.wownero: + return wownero!.createWowneroRestoreWalletFromSeedCredentials( + name: name, + height: restoreWallet.height ?? 0, + mnemonic: restoreWallet.mnemonicSeed ?? '', + password: password, + ); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 09b5c9d96..335b1a006 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -36,6 +36,9 @@ class WalletRestoreFromQRCode { 'tron': WalletType.tron, 'tron-wallet': WalletType.tron, 'tron_wallet': WalletType.tron, + 'wownero': WalletType.wownero, + 'wownero-wallet': WalletType.wownero, + 'wownero_wallet': WalletType.wownero, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -57,7 +60,9 @@ class WalletRestoreFromQRCode { RegExp _getPattern(int wordCount) => RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)'); - List patternCounts = walletType == WalletType.monero ? [25, 16, 14, 13] : [24, 18, 12]; + List patternCounts = walletType == WalletType.monero || walletType == WalletType.wownero + ? [25, 16, 14, 13] + : [24, 18, 12]; for (final count in patternCounts) { final pattern = _getPattern(count); @@ -132,7 +137,8 @@ class WalletRestoreFromQRCode { final seedValue = credentials['seed'] as String; final words = SeedValidator.getWordList(type: type, language: 'english'); - if (type == WalletType.monero && Polyseed.isValidSeed(seedValue)) { + if ((type == WalletType.monero || type == WalletType.wownero) && + Polyseed.isValidSeed(seedValue)) { return WalletRestoreMode.seed; } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index d6f2589c1..892841a60 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -100,6 +101,9 @@ abstract class OutputBase with Store { case WalletType.polygon: _amount = polygon!.formatterPolygonParseAmount(_cryptoAmount); break; + case WalletType.wownero: + _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); + break; default: break; } @@ -154,6 +158,10 @@ abstract class OutputBase with Store { return monero!.formatterMoneroAmountToDouble(amount: fee); } + if (_wallet.type == WalletType.wownero) { + return wownero!.formatterWowneroAmountToDouble(amount: fee); + } + if (_wallet.type == WalletType.haven) { return haven!.formatterMoneroAmountToDouble(amount: fee); } @@ -287,6 +295,9 @@ abstract class OutputBase with Store { case WalletType.tron: maximumFractionDigits = 12; break; + case WalletType.wownero: + maximumFractionDigits = 11; + break; default: break; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a00cfe0cc..a1997e81d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -16,6 +16,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/exceptions.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; @@ -244,6 +245,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || wallet.type == WalletType.monero || + wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoinCash; @computed @@ -478,6 +480,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return monero! .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!); + case WalletType.wownero: + return wownero! + .createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!); + case WalletType.haven: return haven!.createHavenTransactionCreationCredentials( outputs: outputs, priority: priority!, assetType: selectedCryptoCurrency.title); diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 7e751a920..bd04755fa 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -1,8 +1,11 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/package_info.dart'; +// import 'package:package_info/package_info.dart'; +import 'package:collection/collection.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; @@ -10,19 +13,18 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; -import 'package:package_info/package_info.dart'; -import 'package:collection/collection.dart'; part 'other_settings_view_model.g.dart'; -class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel; +class OtherSettingsViewModel = OtherSettingsViewModelBase + with _$OtherSettingsViewModel; abstract class OtherSettingsViewModelBase with Store { OtherSettingsViewModelBase(this._settingsStore, this._wallet) : walletType = _wallet.type, currentVersion = '' { - PackageInfo.fromPlatform() - .then((PackageInfo packageInfo) => currentVersion = packageInfo.version); + PackageInfo.fromPlatform().then( + (PackageInfo packageInfo) => currentVersion = packageInfo.version); final priority = _settingsStore.priority[_wallet.type]; final priorities = priorityForWalletType(_wallet.type); @@ -33,7 +35,8 @@ abstract class OtherSettingsViewModelBase with Store { } final WalletType walletType; - final WalletBase, TransactionInfo> _wallet; + final WalletBase, + TransactionInfo> _wallet; @observable String currentVersion; @@ -61,10 +64,12 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.tron); @computed - bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven; + bool get isEnabledBuyAction => + !_settingsStore.disableBuy && _wallet.type != WalletType.haven; @computed - bool get isEnabledSellAction => !_settingsStore.disableSell && _wallet.type != WalletType.haven; + bool get isEnabledSellAction => + !_settingsStore.disableSell && _wallet.type != WalletType.haven; List get availableBuyProvidersTypes { return ProvidersHelper.getAvailableBuyProviderTypes(walletType); @@ -74,12 +79,12 @@ abstract class OtherSettingsViewModelBase with Store { ProvidersHelper.getAvailableSellProviderTypes(walletType); ProviderType get buyProviderType => - _settingsStore.defaultBuyProviders[walletType] ?? ProviderType.askEachTime; + _settingsStore.defaultBuyProviders[walletType] ?? + ProviderType.askEachTime; ProviderType get sellProviderType => - _settingsStore.defaultSellProviders[walletType] ?? ProviderType.askEachTime; - - + _settingsStore.defaultSellProviders[walletType] ?? + ProviderType.askEachTime; String getDisplayPriority(dynamic priority) { final _priority = priority as TransactionPriority; @@ -101,7 +106,8 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) { final rate = bitcoin!.getFeeRate(_wallet, _priority); - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue); + return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, + customRate: customValue); } return priority.toString(); @@ -124,7 +130,8 @@ abstract class OtherSettingsViewModelBase with Store { void onDisplayPrioritySelected(TransactionPriority priority) => _settingsStore.priority[walletType] = priority; - void onDisplayBitcoinPrioritySelected(TransactionPriority priority, double customValue) { + void onDisplayBitcoinPrioritySelected( + TransactionPriority priority, double customValue) { if (_wallet.type == WalletType.bitcoin) { _settingsStore.customBitcoinFeeRate = customValue.round(); } @@ -132,12 +139,13 @@ abstract class OtherSettingsViewModelBase with Store { } @computed - double get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate.toDouble(); + double get customBitcoinFeeRate => + _settingsStore.customBitcoinFeeRate.toDouble(); int? get customPriorityItemIndex { final priorities = priorityForWalletType(walletType); - final customItem = priorities - .firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); + final customItem = priorities.firstWhereOrNull( + (element) => element == bitcoin!.getBitcoinTransactionPriorityCustom()); return customItem != null ? priorities.indexOf(customItem) : null; } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 9f0ffa14c..90511af8e 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -41,6 +41,7 @@ abstract class PrivacySettingsViewModelBase with Store { bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero || + _wallet.type == WalletType.wownero || _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 526ff0335..5b7a1a8db 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -75,6 +76,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.tron: _addTronListItems(tx, dateFormat); break; + case WalletType.wownero: + _addWowneroListItems(tx, dateFormat); + break; default: break; } @@ -166,7 +170,9 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://solscan.io/tx/${txId}'; case WalletType.tron: return 'https://tronscan.org/#/transaction/${txId}'; - default: + case WalletType.wownero: + return 'https://explore.wownero.com/tx/${txId}'; + case WalletType.none: return ''; } } @@ -194,7 +200,9 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'solscan.io'; case WalletType.tron: return S.current.view_transaction_on + 'tronscan.org'; - default: + case WalletType.wownero: + return S.current.view_transaction_on + 'Wownero.com'; + case WalletType.none: return ''; } } @@ -439,4 +447,44 @@ abstract class TransactionDetailsViewModelBase with Store { String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled ? '' : sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title; + + void _addWowneroListItems(TransactionInfo tx, DateFormat dateFormat) { + final key = tx.additionalInfo['key'] as String?; + final accountIndex = tx.additionalInfo['accountIndex'] as int; + final addressIndex = tx.additionalInfo['addressIndex'] as int; + final feeFormatted = tx.feeFormatted(); + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (feeFormatted != null) + StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), + if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + ]; + + if (tx.direction == TransactionDirection.incoming) { + try { + final address = wownero!.getTransactionAddress(wallet, accountIndex, addressIndex); + final label = wownero!.getSubaddressLabel(wallet, accountIndex, addressIndex); + + if (address.isNotEmpty) { + isRecipientAddressShown = true; + _items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + )); + } + + if (label.isNotEmpty) { + _items.add(StandartListItem(title: S.current.address_label, value: label)); + } + } catch (e) { + print(e.toString()); + } + } + + items.addAll(_items); + } } diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 2a4383d38..e2d8469f1 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/wallet_base.dart'; @@ -62,6 +63,8 @@ abstract class UnspentCoinsListViewModelBase with Store { String formatAmountToString(int fullBalance) { if (wallet.type == WalletType.monero) return monero!.formatterMoneroAmountToString(amount: fullBalance); + if (wallet.type == WalletType.wownero) + return wownero!.formatterWowneroAmountToString(amount: fullBalance); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); return ''; @@ -71,6 +74,9 @@ abstract class UnspentCoinsListViewModelBase with Store { if (wallet.type == WalletType.monero) { await monero!.updateUnspents(wallet); } + if (wallet.type == WalletType.wownero) { + await wownero!.updateUnspents(wallet); + } if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { await bitcoin!.updateUnspents(wallet); } @@ -80,6 +86,7 @@ abstract class UnspentCoinsListViewModelBase with Store { List _getUnspents() { if (wallet.type == WalletType.monero) return monero!.getUnspents(wallet); + if (wallet.type == WalletType.wownero) return wownero!.getUnspents(wallet); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.getUnspents(wallet); return List.empty(); diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 6b59c9033..dd7f02407 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_type.dart'; @@ -191,6 +192,22 @@ class TronURI extends PaymentURI { } } +class WowneroURI extends PaymentURI { + WowneroURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'wownero:' + address; + + if (amount.isNotEmpty) { + base += '?tx_amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -293,6 +310,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return TronURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.wownero) { + return WowneroURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -409,6 +430,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.wownero) { + final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final isPrimary = subaddress == primaryAddress; + + return WalletAddressListItem( + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address); + }); + addressList.addAll(addressItems); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 841a88e7e..36661ac7e 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -13,6 +13,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:polyseed/polyseed.dart'; part 'wallet_creation_vm.g.dart'; @@ -42,6 +43,10 @@ abstract class WalletCreationVMBase with Store { final Box _walletInfoSource; final AppStore _appStore; + bool isPolyseed(String seed) => + (type == WalletType.monero || type == WalletType.wownero) && + (Polyseed.isValidSeed(seed) || (seed.split(" ").length == 14)); + bool nameExists(String name) => walletCreationService.exists(name); bool typeExists(WalletType type) => walletCreationService.typeExists(type); @@ -86,7 +91,9 @@ abstract class WalletCreationVMBase with Store { getIt.get().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); - } catch (e) { + } catch (e, s) { + print("@@@@@@@@"); + print(s); state = FailureState(e.toString()); } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 060770273..5102ba8eb 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -4,11 +4,13 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; import 'package:mobx/mobx.dart'; import 'package:polyseed/polyseed.dart'; @@ -32,13 +34,17 @@ abstract class WalletKeysViewModelBase with Store { _populateItems(); }); - if (_appStore.wallet!.type == WalletType.monero || _appStore.wallet!.type == WalletType.haven) { + if (_appStore.wallet!.type == WalletType.monero || + _appStore.wallet!.type == WalletType.haven || + _appStore.wallet!.type == WalletType.wownero) { final accountTransactions = _getWalletTransactions(_appStore.wallet!); if (accountTransactions.isNotEmpty) { - final incomingAccountTransactions = - accountTransactions.where((tx) => tx.direction == TransactionDirection.incoming); + final incomingAccountTransactions = accountTransactions + .where((tx) => tx.direction == TransactionDirection.incoming); if (incomingAccountTransactions.isNotEmpty) { - incomingAccountTransactions.toList().sort((a, b) => a.date.compareTo(b.date)); + incomingAccountTransactions + .toList() + .sort((a, b) => a.date.compareTo(b.date)); _restoreHeightByTransactions = _getRestoreHeightByTransactions( _appStore.wallet!.type, incomingAccountTransactions.first.date); } @@ -64,29 +70,38 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) - StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), + StandartListItem( + title: S.current.spend_key_public, + value: keys['publicSpendKey']!), if (keys['privateSpendKey'] != null) - StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), + StandartListItem( + title: S.current.spend_key_private, + value: keys['privateSpendKey']!), if (keys['publicViewKey'] != null) - StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), + StandartListItem( + title: S.current.view_key_public, value: keys['publicViewKey']!), if (keys['privateViewKey'] != null) - StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + title: S.current.view_key_private, + value: keys['privateViewKey']!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); - if (_appStore.wallet?.seed != null && Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + if (_appStore.wallet?.seed != null && + Polyseed.isValidSeed(_appStore.wallet!.seed!)) { final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); - final legacyLang = _getLegacySeedLang(lang); - final legacySeed = - Polyseed.decode(_appStore.wallet!.seed!, lang, PolyseedCoin.POLYSEED_MONERO) - .toLegacySeed(legacyLang); - items.add(StandartListItem(title: S.current.wallet_seed_legacy, value: legacySeed)); + items.add(StandartListItem( + title: S.current.wallet_seed_legacy, + value: (_appStore.wallet as MoneroWalletBase) + .seedLegacy(lang.nameEnglish))); } final restoreHeight = monero!.getRestoreHeight(_appStore.wallet!); if (restoreHeight != null) { items.add(StandartListItem( - title: S.current.wallet_recovery_height, value: restoreHeight.toString())); + title: S.current.wallet_recovery_height, + value: restoreHeight.toString())); } } @@ -95,17 +110,58 @@ abstract class WalletKeysViewModelBase with Store { items.addAll([ if (keys['publicSpendKey'] != null) - StandartListItem(title: S.current.spend_key_public, value: keys['publicSpendKey']!), + StandartListItem( + title: S.current.spend_key_public, + value: keys['publicSpendKey']!), if (keys['privateSpendKey'] != null) - StandartListItem(title: S.current.spend_key_private, value: keys['privateSpendKey']!), + StandartListItem( + title: S.current.spend_key_private, + value: keys['privateSpendKey']!), if (keys['publicViewKey'] != null) - StandartListItem(title: S.current.view_key_public, value: keys['publicViewKey']!), + StandartListItem( + title: S.current.view_key_public, value: keys['publicViewKey']!), if (keys['privateViewKey'] != null) - StandartListItem(title: S.current.view_key_private, value: keys['privateViewKey']!), - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + title: S.current.view_key_private, + value: keys['privateViewKey']!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); } + if (_appStore.wallet!.type == WalletType.wownero) { + final keys = wownero!.getKeys(_appStore.wallet!); + + items.addAll([ + if (keys['publicSpendKey'] != null) + StandartListItem( + title: S.current.spend_key_public, + value: keys['publicSpendKey']!), + if (keys['privateSpendKey'] != null) + StandartListItem( + title: S.current.spend_key_private, + value: keys['privateSpendKey']!), + if (keys['publicViewKey'] != null) + StandartListItem( + title: S.current.view_key_public, value: keys['publicViewKey']!), + if (keys['privateViewKey'] != null) + StandartListItem( + title: S.current.view_key_private, + value: keys['privateViewKey']!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + ]); + + if (_appStore.wallet?.seed != null && + Polyseed.isValidSeed(_appStore.wallet!.seed!)) { + final lang = PolyseedLang.getByPhrase(_appStore.wallet!.seed!); + items.add(StandartListItem( + title: S.current.wallet_seed_legacy, + value: (_appStore.wallet as WowneroWalletBase) + .seedLegacy(lang.nameEnglish))); + } + } + if (_appStore.wallet!.type == WalletType.bitcoin || _appStore.wallet!.type == WalletType.litecoin || _appStore.wallet!.type == WalletType.bitcoinCash) { @@ -118,7 +174,8 @@ abstract class WalletKeysViewModelBase with Store { // StandartListItem(title: S.current.private_key, value: keys['privateKey']!), // if (keys['publicKey'] != null) // StandartListItem(title: S.current.public_key, value: keys['publicKey']!), - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); } @@ -127,24 +184,32 @@ abstract class WalletKeysViewModelBase with Store { _appStore.wallet!.type == WalletType.tron) { items.addAll([ if (_appStore.wallet!.privateKey != null) - StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), + StandartListItem( + title: S.current.private_key, + value: _appStore.wallet!.privateKey!), if (_appStore.wallet!.seed != null) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), ]); } - bool nanoBased = - _appStore.wallet!.type == WalletType.nano || _appStore.wallet!.type == WalletType.banano; + bool nanoBased = _appStore.wallet!.type == WalletType.nano || + _appStore.wallet!.type == WalletType.banano; if (nanoBased) { // we always have the hex version of the seed and private key: items.addAll([ if (_appStore.wallet!.seed != null) - StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed!), + StandartListItem( + title: S.current.wallet_seed, value: _appStore.wallet!.seed!), if (_appStore.wallet!.hexSeed != null) - StandartListItem(title: S.current.seed_hex_form, value: _appStore.wallet!.hexSeed!), + StandartListItem( + title: S.current.seed_hex_form, + value: _appStore.wallet!.hexSeed!), if (_appStore.wallet!.privateKey != null) - StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), + StandartListItem( + title: S.current.private_key, + value: _appStore.wallet!.privateKey!), ]); } } @@ -154,7 +219,10 @@ abstract class WalletKeysViewModelBase with Store { return await haven!.getCurrentHeight(); } if (_appStore.wallet!.type == WalletType.monero) { - return monero_wallet.getCurrentHeight(); + return await monero!.getCurrentHeight(); + } + if (_appStore.wallet!.type == WalletType.wownero) { + return await wownero!.getCurrentHeight(); } return null; } @@ -183,8 +251,10 @@ abstract class WalletKeysViewModelBase with Store { return 'solana-wallet'; case WalletType.tron: return 'tron-wallet'; + case WalletType.wownero: + return 'wownero-wallet'; default: - throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); + throw Exception('Unexpected wallet type: ${_appStore.wallet!.type.toString()}'); } } @@ -205,7 +275,8 @@ abstract class WalletKeysViewModelBase with Store { if (_appStore.wallet!.seed != null) 'seed': _appStore.wallet!.seed!, if (_appStore.wallet!.seed == null && _appStore.wallet!.hexSeed != null) 'hexSeed': _appStore.wallet!.hexSeed!, - if (_appStore.wallet!.seed == null && _appStore.wallet!.privateKey != null) + if (_appStore.wallet!.seed == null && + _appStore.wallet!.privateKey != null) 'private_key': _appStore.wallet!.privateKey!, if (restoreHeightResult != null) ...{'height': restoreHeightResult} }; @@ -221,6 +292,12 @@ abstract class WalletKeysViewModelBase with Store { return monero!.getTransactionHistory(wallet).transactions.values.toList(); } else if (wallet.type == WalletType.haven) { return haven!.getTransactionHistory(wallet).transactions.values.toList(); + } else if (wallet.type == WalletType.wownero) { + return wownero! + .getTransactionHistory(wallet) + .transactions + .values + .toList(); } return []; } @@ -230,11 +307,14 @@ abstract class WalletKeysViewModelBase with Store { return monero!.getHeightByDate(date: date); } else if (type == WalletType.haven) { return haven!.getHeightByDate(date: date); + } else if (type == WalletType.wownero) { + return wownero!.getHeightByDate(date: date); } return 0; } - String getRoundedRestoreHeight(int height) => ((height / 1000).floor() * 1000).toString(); + String getRoundedRestoreHeight(int height) => + ((height / 1000).floor() * 1000).toString(); LegacySeedLang _getLegacySeedLang(PolyseedLang lang) { switch (lang.nameEnglish) { diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index e19efabc5..4729a38b2 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -35,11 +36,13 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { @observable String selectedMnemonicLanguage; - bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven; + bool get hasLanguageSelector => + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero; int get seedPhraseWordsLength { switch (type) { case WalletType.monero: + case WalletType.wownero: if (advancedPrivacySettingsViewModel.isPolySeed) { return 16; } @@ -55,7 +58,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { } } - bool get hasSeedType => type == WalletType.monero; + bool get hasSeedType => type == WalletType.monero || type == WalletType.wownero; @override WalletCredentials getCredentials(dynamic _options) { @@ -76,6 +79,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name); case WalletType.nano: + case WalletType.banano: return nano!.createNanoNewWalletCredentials(name: name); case WalletType.polygon: return polygon!.createPolygonNewWalletCredentials(name: name); @@ -83,7 +87,10 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return solana!.createSolanaNewWalletCredentials(name: name); case WalletType.tron: return tron!.createTronNewWalletCredentials(name: name); - default: + case WalletType.wownero: + return wownero!.createWowneroNewWalletCredentials( + name: name, language: options!.first as String, isPolyseed: options.last as bool); + case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index e19a83bc3..25a555b44 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -2,7 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cw_core/nano_account_info_response.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -29,8 +29,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, {required WalletType type}) - : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, - hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, + : hasSeedLanguageSelector = + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, + hasBlockchainHeightLanguageSelector = + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, hasRestoreFromPrivateKey = type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.nano || @@ -42,18 +44,22 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { switch (type) { case WalletType.monero: - case WalletType.haven: - case WalletType.ethereum: - case WalletType.polygon: availableModes = WalletRestoreMode.values; break; case WalletType.nano: case WalletType.banano: case WalletType.solana: case WalletType.tron: + case WalletType.wownero: + case WalletType.haven: + case WalletType.ethereum: + case WalletType.polygon: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; - default: + case WalletType.bitcoin: + case WalletType.litecoin: + case WalletType.bitcoinCash: + case WalletType.none: availableModes = [WalletRestoreMode.seed]; break; } @@ -112,6 +118,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); case WalletType.nano: + case WalletType.banano: return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, @@ -136,7 +143,14 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, ); - default: + case WalletType.wownero: + return wownero!.createWowneroRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + height: height, + ); + case WalletType.none: break; } } @@ -200,6 +214,16 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, privateKey: options['private_key'] as String, ); + case WalletType.wownero: + return wownero!.createWowneroRestoreWalletFromKeysCredentials( + name: name, + height: height, + spendKey: spendKey!, + viewKey: viewKey!, + address: address!, + password: password, + language: 'English', + ); default: break; } diff --git a/lib/wallet_type_utils.dart b/lib/wallet_type_utils.dart index 5ed78dc64..459ca992b 100644 --- a/lib/wallet_type_utils.dart +++ b/lib/wallet_type_utils.dart @@ -16,6 +16,10 @@ bool get isSingleCoin { return availableWalletTypes.length == 1; } +bool get hasMonero { + return availableWalletTypes.contains(WalletType.monero); +} + String get approximatedAppName { if (isMoneroOnly) { return 'Monero.com'; @@ -26,4 +30,4 @@ String get approximatedAppName { } return 'Cake Wallet'; -} \ No newline at end of file +} diff --git a/lib/wownero/cw_wownero.dart b/lib/wownero/cw_wownero.dart new file mode 100644 index 000000000..eccb0f126 --- /dev/null +++ b/lib/wownero/cw_wownero.dart @@ -0,0 +1,347 @@ +part of 'wownero.dart'; + +class CWWowneroAccountList extends WowneroAccountList { + CWWowneroAccountList(this._wallet); + + final Object _wallet; + + @override + @computed + ObservableList get accounts { + final wowneroWallet = _wallet as WowneroWallet; + final accounts = wowneroWallet.walletAddresses.accountList.accounts + .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) + .toList(); + return ObservableList.of(accounts); + } + + @override + void update(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + wowneroWallet.walletAddresses.accountList.update(); + } + + @override + void refresh(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + wowneroWallet.walletAddresses.accountList.refresh(); + } + + @override + List getAll(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.walletAddresses.accountList + .getAll() + .map((acc) => Account(id: acc.id, label: acc.label, balance: acc.balance)) + .toList(); + } + + @override + Future addAccount(Object wallet, {required String label}) async { + final wowneroWallet = wallet as WowneroWallet; + await wowneroWallet.walletAddresses.accountList.addAccount(label: label); + } + + @override + Future setLabelAccount(Object wallet, + {required int accountIndex, required String label}) async { + final wowneroWallet = wallet as WowneroWallet; + await wowneroWallet.walletAddresses.accountList + .setLabelAccount(accountIndex: accountIndex, label: label); + } +} + +class CWWowneroSubaddressList extends WowneroSubaddressList { + CWWowneroSubaddressList(this._wallet); + + final Object _wallet; + + @override + @computed + ObservableList get subaddresses { + final wowneroWallet = _wallet as WowneroWallet; + final subAddresses = wowneroWallet.walletAddresses.subaddressList.subaddresses + .map((sub) => Subaddress(id: sub.id, address: sub.address, label: sub.label)) + .toList(); + return ObservableList.of(subAddresses); + } + + @override + void update(Object wallet, {required int accountIndex}) { + final wowneroWallet = wallet as WowneroWallet; + wowneroWallet.walletAddresses.subaddressList.update(accountIndex: accountIndex); + } + + @override + void refresh(Object wallet, {required int accountIndex}) { + final wowneroWallet = wallet as WowneroWallet; + wowneroWallet.walletAddresses.subaddressList.refresh(accountIndex: accountIndex); + } + + @override + List getAll(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.walletAddresses.subaddressList + .getAll() + .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .toList(); + } + + @override + Future addSubaddress(Object wallet, + {required int accountIndex, required String label}) async { + final wowneroWallet = wallet as WowneroWallet; + await wowneroWallet.walletAddresses.subaddressList + .addSubaddress(accountIndex: accountIndex, label: label); + } + + @override + Future setLabelSubaddress(Object wallet, + {required int accountIndex, required int addressIndex, required String label}) async { + final wowneroWallet = wallet as WowneroWallet; + await wowneroWallet.walletAddresses.subaddressList + .setLabelSubaddress(accountIndex: accountIndex, addressIndex: addressIndex, label: label); + } +} + +class CWWowneroWalletDetails extends WowneroWalletDetails { + CWWowneroWalletDetails(this._wallet); + + final Object _wallet; + + @computed + @override + Account get account { + final wowneroWallet = _wallet as WowneroWallet; + final acc = wowneroWallet.walletAddresses.account; + return Account(id: acc!.id, label: acc.label, balance: acc.balance); + } + + @computed + @override + WowneroBalance get balance { + throw Exception('Unimplemented'); + // return WowneroBalance(); + //return WowneroBalance( + // fullBalance: balance.fullBalance, + // unlockedBalance: balance.unlockedBalance); + } +} + +class CWWownero extends Wownero { + @override + WowneroAccountList getAccountList(Object wallet) => CWWowneroAccountList(wallet); + + @override + WowneroSubaddressList getSubaddressList(Object wallet) => CWWowneroSubaddressList(wallet); + + @override + TransactionHistoryBase getTransactionHistory(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.transactionHistory; + } + + @override + WowneroWalletDetails getWowneroWalletDetails(Object wallet) => CWWowneroWalletDetails(wallet); + + @override + int getHeightByDate({required DateTime date}) => getWowneroHeightByDate(date: date); + + @override + TransactionPriority getDefaultTransactionPriority() => MoneroTransactionPriority.automatic; + + @override + TransactionPriority getWowneroTransactionPrioritySlow() => MoneroTransactionPriority.slow; + + @override + TransactionPriority getWowneroTransactionPriorityAutomatic() => + MoneroTransactionPriority.automatic; + + @override + TransactionPriority deserializeWowneroTransactionPriority({required int raw}) => + MoneroTransactionPriority.deserialize(raw: raw); + + @override + List getTransactionPriorities() => MoneroTransactionPriority.all; + + @override + List getWowneroWordList(String language) { + if (language.startsWith("POLYSEED_")) { + final lang = language.replaceAll("POLYSEED_", ""); + return PolyseedLang.getByEnglishName(lang).words; + } + if (language.startsWith("WOWSEED_")) { + final lang = language.replaceAll("WOWSEED_", ""); + return PolyseedLang.getByEnglishName(lang).words; + } + switch (language.toLowerCase()) { + case 'english': + return EnglishMnemonics.words; + case 'chinese (simplified)': + return ChineseSimplifiedMnemonics.words; + case 'dutch': + return DutchMnemonics.words; + case 'german': + return GermanMnemonics.words; + case 'japanese': + return JapaneseMnemonics.words; + case 'portuguese': + return PortugueseMnemonics.words; + case 'russian': + return RussianMnemonics.words; + case 'spanish': + return SpanishMnemonics.words; + case 'french': + return FrenchMnemonics.words; + case 'italian': + return ItalianMnemonics.words; + default: + return EnglishMnemonics.words; + } + } + + @override + WalletCredentials createWowneroRestoreWalletFromKeysCredentials( + {required String name, + required String spendKey, + required String viewKey, + required String address, + required String password, + required String language, + required int height}) => + WowneroRestoreWalletFromKeysCredentials( + name: name, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: language, + height: height); + + @override + WalletCredentials createWowneroRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required int height, + required String mnemonic}) => + WowneroRestoreWalletFromSeedCredentials( + name: name, password: password, height: height, mnemonic: mnemonic); + + @override + WalletCredentials createWowneroNewWalletCredentials( + {required String name, + required String language, + required bool isPolyseed, + String? password}) => + WowneroNewWalletCredentials( + name: name, password: password, language: language, isPolyseed: isPolyseed); + + @override + Map getKeys(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + final keys = wowneroWallet.keys; + return { + 'privateSpendKey': keys.privateSpendKey, + 'privateViewKey': keys.privateViewKey, + 'publicSpendKey': keys.publicSpendKey, + 'publicViewKey': keys.publicViewKey + }; + } + + @override + Object createWowneroTransactionCreationCredentials( + {required List outputs, required TransactionPriority priority}) => + WowneroTransactionCreationCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as MoneroTransactionPriority); + + @override + Object createWowneroTransactionCreationCredentialsRaw( + {required List outputs, required TransactionPriority priority}) => + WowneroTransactionCreationCredentials( + outputs: outputs, priority: priority as MoneroTransactionPriority); + + @override + String formatterWowneroAmountToString({required int amount}) => + wowneroAmountToString(amount: amount); + + @override + double formatterWowneroAmountToDouble({required int amount}) => + wowneroAmountToDouble(amount: amount); + + @override + int formatterWowneroParseAmount({required String amount}) => wowneroParseAmount(amount: amount); + + @override + Account getCurrentAccount(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + final acc = wowneroWallet.walletAddresses.account; + return Account(id: acc!.id, label: acc.label, balance: acc.balance); + } + + @override + void setCurrentAccount(Object wallet, int id, String label, String? balance) { + final wowneroWallet = wallet as WowneroWallet; + wowneroWallet.walletAddresses.account = + wownero_account.Account(id: id, label: label, balance: balance); + } + + @override + void onStartup() => wownero_wallet_api.onStartup(); + + @override + int getTransactionInfoAccountId(TransactionInfo tx) { + final wowneroTransactionInfo = tx as WowneroTransactionInfo; + return wowneroTransactionInfo.accountIndex; + } + + @override + WalletService createWowneroWalletService( + Box walletInfoSource, Box unspentCoinSource) => + WowneroWalletService(walletInfoSource, unspentCoinSource); + + @override + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.getTransactionAddress(accountIndex, addressIndex); + } + + @override + String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.getSubaddressLabel(accountIndex, addressIndex); + } + + @override + Map pendingTransactionInfo(Object transaction) { + final ptx = transaction as PendingWowneroTransaction; + return {'id': ptx.id, 'hex': ptx.hex, 'key': ptx.txKey}; + } + + @override + List getUnspents(Object wallet) { + final wowneroWallet = wallet as WowneroWallet; + return wowneroWallet.unspentCoins; + } + + @override + Future updateUnspents(Object wallet) async { + final wowneroWallet = wallet as WowneroWallet; + await wowneroWallet.updateUnspent(); + } + + @override + Future getCurrentHeight() async { + return wownero_wallet_api.getCurrentHeight(); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 51f61d9e3..7ef453eb1 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,10 @@ import FlutterMacOS import Foundation import connectivity_plus -import cw_monero import device_info_plus import devicelocale import flutter_inappwebview_macos import flutter_local_authentication -import flutter_secure_storage_macos import in_app_review import package_info import package_info_plus @@ -23,12 +21,10 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index b71468adc..c2f37a3f3 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -2,23 +2,6 @@ PODS: - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift - - cw_monero (0.0.1): - - cw_monero/Boost (= 0.0.1) - - cw_monero/Monero (= 0.0.1) - - cw_monero/OpenSSL (= 0.0.1) - - cw_monero/Sodium (= 0.0.1) - - cw_monero/Unbound (= 0.0.1) - - FlutterMacOS - - cw_monero/Boost (0.0.1): - - FlutterMacOS - - cw_monero/Monero (0.0.1): - - FlutterMacOS - - cw_monero/OpenSSL (0.0.1): - - FlutterMacOS - - cw_monero/Sodium (0.0.1): - - FlutterMacOS - - cw_monero/Unbound (0.0.1): - - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - devicelocale (0.0.1): @@ -56,7 +39,6 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) @@ -81,8 +63,6 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos - cw_monero: - :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -116,7 +96,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 - cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index d14d203c6..0dccffc4f 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -26,8 +26,10 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 4171CB1F5A4EA2E4DC33F52F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */; }; + 83FF6BF911B29965D3589BE7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD06F8F6E3797AC031038136 /* Pods_Runner.framework */; }; 9F565D5929954F53009A75FB /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F565D5729954F53009A75FB /* secRandom.swift */; }; + CE75FC4A2C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CED5DBE42BE59BBF0065028F /* monero_libwallet2_api_c.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,12 +53,22 @@ name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; }; + CED5DBE02BE59B230065028F /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CE75FC4A2C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib in CopyFiles */, + CED5DBE42BE59BBF0065028F /* monero_libwallet2_api_c.dylib in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 094BF982245FD1012D60A103 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 0C090639294D3AAC00954DC9 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; - 2A820A13B0719E9E0CD6686F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3294B60E86F47055290F9DFC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* Cake Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Cake Wallet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -72,10 +84,14 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9646C67C7114830A5ACFF5DF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 81434A9891ECC211D4A12B7B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 981E08F6A19E64F1F4D21DB8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 9F1E7BFB2BF2D27500C28C9A /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 9F565D5729954F53009A75FB /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = secRandom.swift; path = CakeWallet/secRandom.swift; sourceTree = ""; }; - B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BD06F8F6E3797AC031038136 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = wownero_libwallet2_api_c.dylib; sourceTree = ""; }; + CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = monero_libwallet2_api_c.dylib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,7 +99,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4171CB1F5A4EA2E4DC33F52F /* Pods_Runner.framework in Frameworks */, + 83FF6BF911B29965D3589BE7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -104,6 +120,8 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( + CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */, + CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */, 9F565D5729954F53009A75FB /* secRandom.swift */, 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, @@ -146,6 +164,7 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( + 9F1E7BFB2BF2D27500C28C9A /* Runner.entitlements */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, @@ -159,9 +178,9 @@ 9B6E7CA3983216A9E173F00F /* Pods */ = { isa = PBXGroup; children = ( - 9646C67C7114830A5ACFF5DF /* Pods-Runner.debug.xcconfig */, - 2A820A13B0719E9E0CD6686F /* Pods-Runner.release.xcconfig */, - 094BF982245FD1012D60A103 /* Pods-Runner.profile.xcconfig */, + 81434A9891ECC211D4A12B7B /* Pods-Runner.debug.xcconfig */, + 3294B60E86F47055290F9DFC /* Pods-Runner.release.xcconfig */, + 981E08F6A19E64F1F4D21DB8 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -170,7 +189,7 @@ isa = PBXGroup; children = ( 0C090639294D3AAC00954DC9 /* libiconv.tbd */, - B38D1DBC56DBD386923BC063 /* Pods_Runner.framework */, + BD06F8F6E3797AC031038136 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -182,13 +201,14 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 93B711AB4B96E7C8C5C5B844 /* [CP] Check Pods Manifest.lock */, + 88D0DCAF0B588F71DC405272 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 5592D00118C2EA3C5E0B5FDF /* [CP] Embed Pods Frameworks */, + CED5DBE02BE59B230065028F /* CopyFiles */, + 283A25C672FDD605ACF2FDFC /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -257,6 +277,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 283A25C672FDD605ACF2FDFC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -295,24 +332,7 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 5592D00118C2EA3C5E0B5FDF /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 93B711AB4B96E7C8C5C5B844 /* [CP] Check Pods Manifest.lock */ = { + 88D0DCAF0B588F71DC405272 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -424,8 +444,9 @@ ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 10; @@ -558,8 +579,9 @@ ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 10; @@ -586,8 +608,9 @@ ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 10; diff --git a/cw_monero/example/macos/Runner/DebugProfile.entitlements b/macos/Runner/RunnerBase.entitlements similarity index 66% rename from cw_monero/example/macos/Runner/DebugProfile.entitlements rename to macos/Runner/RunnerBase.entitlements index dddb8a30c..2003d2c2b 100644 --- a/cw_monero/example/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/RunnerBase.entitlements @@ -4,9 +4,13 @@ com.apple.security.app-sandbox - com.apple.security.cs.allow-jit + com.apple.security.network.client com.apple.security.network.server + keychain-access-groups + + $(AppIdentifierPrefix)${BUNDLE_ID} + diff --git a/macos/monero_libwallet2_api_c.dylib b/macos/monero_libwallet2_api_c.dylib new file mode 120000 index 000000000..b0ccd724f --- /dev/null +++ b/macos/monero_libwallet2_api_c.dylib @@ -0,0 +1 @@ +../scripts/monero_c/release/monero/host-apple-darwin_libwallet2_api_c.dylib \ No newline at end of file diff --git a/macos/wownero_libwallet2_api_c.dylib b/macos/wownero_libwallet2_api_c.dylib new file mode 120000 index 000000000..6b79a4f03 --- /dev/null +++ b/macos/wownero_libwallet2_api_c.dylib @@ -0,0 +1 @@ +../scripts/monero_c/release/wownero/host-apple-darwin_libwallet2_api_c.dylib \ No newline at end of file diff --git a/model_generator.sh b/model_generator.sh index 24dac675f..58ce9b5d0 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -7,6 +7,7 @@ cd cw_nano; flutter pub get; flutter packages pub run build_runner build --delet cd cw_bitcoin_cash; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_solana; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_tron; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. +cd cw_wownero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd .. cd cw_polygon; flutter pub get; cd .. cd cw_ethereum; flutter pub get; cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index e00527d9f..0ce331d57 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -163,6 +163,7 @@ flutter: - assets/polygon_node_list.yml - assets/solana_node_list.yml - assets/tron_node_list.yml + - assets/wownero_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ac82c9e0f..d39b175dd 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -585,6 +585,7 @@ "seedtype": "Seedtype", "seedtype_legacy": "Legacy (25 words)", "seedtype_polyseed": "Polyseed (16 words)", + "seedtype_wownero": "Wownero (14 words)", "select_backup_file": "Select backup file", "select_buy_provider_notice": "Select a buy provider above. You can skip this screen by setting your default buy provider in app settings.", "select_destination": "Please select destination for the backup file.", diff --git a/run-android.sh b/run-android.sh index bdacef392..880d86b6f 100755 --- a/run-android.sh +++ b/run-android.sh @@ -4,8 +4,7 @@ get_current_branch() { if git rev-parse --git-dir > /dev/null 2>&1; then branch=$(git rev-parse --abbrev-ref HEAD) - branch=${branch//[-]/_} # Replace all dashes with underscores - echo "$branch" + echo "$branch" | tr '-' '_' else echo "Error: Not a git repository." return 1 @@ -16,6 +15,7 @@ get_current_branch() { update_app_properties() { local branch=$1 local file_path="./android/app.properties" + sed -i "s/^id=.*/id=com.cakewallet.$branch/" "$file_path" sed -i "s/^name=.*/name=$branch-Cake Wallet/" "$file_path" } @@ -27,4 +27,4 @@ if [[ $? -eq 0 ]]; then fi # run the app -flutter run \ No newline at end of file +flutter run diff --git a/scripts/android/app_config.sh b/scripts/android/app_config.sh index e2cbd72da..5fe5e503d 100755 --- a/scripts/android/app_config.sh +++ b/scripts/android/app_config.sh @@ -8,5 +8,5 @@ fi ./app_properties.sh ./app_icon.sh ./pubspec_gen.sh -./manifest.sh +./manifest.sh true #force overwrite manifest ./inject_app_details.sh diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index 5093d231d..ec70f02a6 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -10,6 +10,6 @@ DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh - $DIR/build_haven.sh ;; + $DIR/build_haven_all.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_monero.sh b/scripts/android/build_monero.sh deleted file mode 100755 index fb596e452..000000000 --- a/scripts/android/build_monero.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh - -. ./config.sh -MONERO_BRANCH=release-v0.18.3.2-android -MONERO_SRC_DIR=${WORKDIR}/monero - -git clone https://github.com/cake-tech/monero.git ${MONERO_SRC_DIR} --branch ${MONERO_BRANCH} -cd $MONERO_SRC_DIR -git submodule update --init --force - -for arch in "aarch" "aarch64" "i686" "x86_64" -do -FLAGS="" -PREFIX=${WORKDIR}/prefix_${arch} -DEST_LIB_DIR=${PREFIX}/lib/monero -DEST_INCLUDE_DIR=${PREFIX}/include/monero -export CMAKE_INCLUDE_PATH="${PREFIX}/include" -export CMAKE_LIBRARY_PATH="${PREFIX}/lib" -ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" -PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" - -mkdir -p $DEST_LIB_DIR -mkdir -p $DEST_INCLUDE_DIR - -case $arch in - "aarch" ) - CLANG=arm-linux-androideabi-clang - CXXLANG=arm-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-armv7" - ARCH="armv7-a" - ARCH_ABI="armeabi-v7a" - FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; - "aarch64" ) - CLANG=aarch64-linux-androideabi-clang - CXXLANG=aarch64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-armv8" - ARCH="armv8-a" - ARCH_ABI="arm64-v8a";; - "i686" ) - CLANG=i686-linux-androideabi-clang - CXXLANG=i686-linux-androideabi-clang++ - BUILD_64=OFF - TAG="android-x86" - ARCH="i686" - ARCH_ABI="x86";; - "x86_64" ) - CLANG=x86_64-linux-androideabi-clang - CXXLANG=x86_64-linux-androideabi-clang++ - BUILD_64=ON - TAG="android-x86_64" - ARCH="x86-64" - ARCH_ABI="x86_64";; -esac - -cd $MONERO_SRC_DIR -rm -rf ./build/release -mkdir -p ./build/release -cd ./build/release -CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} -D MANUAL_SUBMODULES=1 $FLAGS ../.. - -make wallet_api -j$THREADS -find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; - -cp -r ./lib/* $DEST_LIB_DIR -cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR -done diff --git a/scripts/android/build_monero_all.sh b/scripts/android/build_monero_all.sh index 69ec37b5f..261ebd560 100755 --- a/scripts/android/build_monero_all.sh +++ b/scripts/android/build_monero_all.sh @@ -1,9 +1,57 @@ #!/bin/bash -./build_iconv.sh -./build_boost.sh -./build_openssl.sh -./build_sodium.sh -./build_unbound.sh -./build_zmq.sh -./build_monero.sh +# Usage: env USE_DOCKER= ./build_all.sh + +set -x -e + +cd "$(dirname "$0")" + +NPROC="-j$(nproc)" + +if [[ "x$(uname)" == "xDarwin" ]]; +then + USE_DOCKER="ON" + NPROC="-j1" +fi + +../prepare_moneroc.sh + +if [[ ! "x$RUNNER_OS" == "x" ]]; +then + REMOVE_CACHES=ON +fi + +# NOTE: -j1 is intentional. Otherwise you will run into weird behaviour on macos +if [[ ! "x$USE_DOCKER" == "x" ]]; +then + for COIN in monero wownero; + do + pushd ../monero_c + docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc g++ libtinfo5 gperf; ./build_single.sh ${COIN} x86_64-linux-android $NPROC" + # docker run --platform linux/amd64 -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc g++ libtinfo5 gperf; ./build_single.sh ${COIN} i686-linux-android $NPROC" + docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc g++ libtinfo5 gperf; ./build_single.sh ${COIN} armv7a-linux-androideabi $NPROC" + docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc g++ libtinfo5 gperf; ./build_single.sh ${COIN} aarch64-linux-android $NPROC" + popd + done +else + for COIN in monero wownero; + do + pushd ../monero_c + env -i ./build_single.sh ${COIN} x86_64-linux-android $NPROC + [[ ! "x$REMOVE_CACHES" == "x" ]] && rm -rf ${COIN}/contrib/depends/x86_64-linux-android + # ./build_single.sh ${COIN} i686-linux-android $NPROC + # [[ ! "x$REMOVE_CACHES" == "x" ]] && rm -rf ${COIN}/contrib/depends/i686-linux-android + env -i ./build_single.sh ${COIN} armv7a-linux-androideabi $NPROC + [[ ! "x$REMOVE_CACHES" == "x" ]] && rm -rf ${COIN}/contrib/depends/armv7a-linux-androideabi + env -i ./build_single.sh ${COIN} aarch64-linux-android $NPROC + [[ ! "x$REMOVE_CACHES" == "x" ]] && rm -rf ${COIN}/contrib/depends/aarch64-linux-android + + popd + unxz -f ../monero_c/release/${COIN}/x86_64-linux-android_libwallet2_api_c.so.xz + + unxz -f ../monero_c/release/${COIN}/armv7a-linux-androideabi_libwallet2_api_c.so.xz + + unxz -f ../monero_c/release/${COIN}/aarch64-linux-android_libwallet2_api_c.so.xz + [[ ! "x$REMOVE_CACHES" == "x" ]] && rm -rf ${COIN}/contrib/depends/{built,sources} + done +fi diff --git a/scripts/android/build_openssl.sh b/scripts/android/build_openssl.sh index aa668e6bf..33788c8b9 100755 --- a/scripts/android/build_openssl.sh +++ b/scripts/android/build_openssl.sh @@ -1,6 +1,6 @@ #!/bin/sh -set -e +set -e -x . ./config.sh OPENSSL_FILENAME=openssl-1.1.1q.tar.gz @@ -26,7 +26,6 @@ do PREFIX=$WORKDIR/prefix_${arch} TOOLCHAIN=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64 PATH="${TOOLCHAIN}/bin:${ORIGINAL_PATH}" - case $arch in "aarch") X_ARCH="android-arm";; "aarch64") X_ARCH="android-arm64";; diff --git a/scripts/android/build_unbound.sh b/scripts/android/build_unbound.sh index 8786b0f2b..afe848a41 100755 --- a/scripts/android/build_unbound.sh +++ b/scripts/android/build_unbound.sh @@ -14,7 +14,7 @@ PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" cd $WORKDIR rm -rf $EXPAT_SRC_DIR -git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} +git clone https://github.com/libexpat/libexpat.git --depth=1 -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} cd $EXPAT_SRC_DIR test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 cd $EXPAT_SRC_DIR/expat @@ -49,7 +49,7 @@ PATH="${TOOLCHAIN_BIN_PATH}:${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" echo $PATH cd $WORKDIR rm -rf $UNBOUND_SRC_DIR -git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_SRC_DIR} +git clone https://github.com/NLnetLabs/unbound.git --depth=1 -b ${UNBOUND_VERSION} ${UNBOUND_SRC_DIR} cd $UNBOUND_SRC_DIR test `git rev-parse HEAD` = ${UNBOUND_HASH} || exit 1 diff --git a/scripts/android/copy_monero_deps.sh b/scripts/android/copy_monero_deps.sh index d59e9d7f0..ed8181e6b 100755 --- a/scripts/android/copy_monero_deps.sh +++ b/scripts/android/copy_monero_deps.sh @@ -41,5 +41,4 @@ done mkdir -p ${CW_HAVEN_EXTERNAL_DIR}/include mkdir -p ${CW_MONERO_EXTERNAL_DIR}/include -cp $CW_EXRTERNAL_DIR/x86/include/monero/wallet2_api.h ${CW_MONERO_EXTERNAL_DIR}/include cp $CW_EXRTERNAL_DIR/x86/include/haven/wallet2_api.h ${CW_HAVEN_EXTERNAL_DIR}/include diff --git a/scripts/android/init_boost.sh b/scripts/android/init_boost.sh index 13120c910..7579acdd7 100755 --- a/scripts/android/init_boost.sh +++ b/scripts/android/init_boost.sh @@ -17,6 +17,6 @@ echo $BOOST_SHA256 $BOOST_FILE_PATH | sha256sum -c - || exit 1 cd $WORKDIR rm -rf $BOOST_SRC_DIR rm -rf $PREFIX/include/boost -tar -xvf $BOOST_FILE_PATH -C $WORKDIR +tar -xf $BOOST_FILE_PATH -C $WORKDIR cd $BOOST_SRC_DIR -./bootstrap.sh --prefix=${PREFIX} +./bootstrap.sh --prefix=${PREFIX} --with-toolset=gcc diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 3fa9ef921..2957b91e3 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -6,6 +6,7 @@ if [ -z "$APP_ANDROID_TYPE" ]; then fi cd ../.. +set -x sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml sed -i "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml sed -i "0,/__APP_SCHEME__/s/__APP_SCHEME__/${APP_ANDROID_SCHEME}/" ./android/app/src/main/AndroidManifest.xml diff --git a/scripts/android/install_ndk.sh b/scripts/android/install_ndk.sh index bee72abad..ea131eb39 100755 --- a/scripts/android/install_ndk.sh +++ b/scripts/android/install_ndk.sh @@ -8,9 +8,9 @@ TOOLCHAIN_x86_DIR=${TOOLCHAIN_DIR}_i686 TOOLCHAIN_x86_64_DIR=${TOOLCHAIN_DIR}_x86_64 ANDROID_NDK_SHA256="3f541adbd0330a9205ba12697f6d04ec90752c53d6b622101a2a8a856e816589" -curl https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} -echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 -unzip $ANDROID_NDK_ZIP -d $WORKDIR + curl https://dl.google.com/android/repository/android-ndk-r17c-linux-x86_64.zip -o ${ANDROID_NDK_ZIP} + echo $ANDROID_NDK_SHA256 $ANDROID_NDK_ZIP | sha256sum -c || exit 1 + unzip $ANDROID_NDK_ZIP -d $WORKDIR ${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch arm64 --api $API --install-dir ${TOOLCHAIN_A64_DIR} --stl=libc++ ${ANDROID_NDK_ROOT}/build/tools/make_standalone_toolchain.py --arch arm --api $API --install-dir ${TOOLCHAIN_A32_DIR} --stl=libc++ diff --git a/scripts/android/manifest.sh b/scripts/android/manifest.sh index 95459606d..dab24d5c0 100755 --- a/scripts/android/manifest.sh +++ b/scripts/android/manifest.sh @@ -1,5 +1,10 @@ #!/bin/bash cd ../.. -cp -rf ./android/app/src/main/AndroidManifestBase.xml ./android/app/src/main/AndroidManifest.xml -cd scripts/android \ No newline at end of file + +if [ "$1" = true ]; then + cp -rf ./android/app/src/main/AndroidManifestBase.xml ./android/app/src/main/AndroidManifest.xml +else + cp -n ./android/app/src/main/AndroidManifestBase.xml ./android/app/src/main/AndroidManifest.xml +fi +cd scripts/android diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index bc7985506..468f548f3 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,10 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero" + if [ "$CW_WITH_HAVEN" = true ];then + CONFIG_ARGS="$CONFIG_ARGS --haven" + fi ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/docker/.gitignore b/scripts/docker/.gitignore index ea1472ec1..c39e9d9f7 100644 --- a/scripts/docker/.gitignore +++ b/scripts/docker/.gitignore @@ -1 +1,2 @@ output/ +cache/ \ No newline at end of file diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile old mode 100644 new mode 100755 index eef09a323..a352cdc71 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -4,23 +4,59 @@ LABEL authors="konsti" ENV MONERO_BRANCH=release-v0.18.2.2-android RUN apt-get update && \ echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections && \ - apt-get install -y dialog apt-utils curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang + apt-get install -y dialog apt-utils curl unzip automake build-essential file pkg-config git python libtool libtinfo5 cmake clang bison ccache RUN mkdir /opt/android/ -COPY . /opt/android/cakewallet/ - WORKDIR /opt/android/cakewallet/ +# build_all.sh +# build_boost.sh +# build_haven.sh +# build_haven_all.sh +# build_iconv.sh +# build_monero.sh +# build_openssl.sh +# build_sodium.sh +# build_unbound.sh +# build_zmq.sh +# config.sh +# copy_haven_deps.sh +# copy_monero_deps.sh +# docker-compose.yml +# entrypoint.sh +# finish_boost.sh +# init_boost.sh +# install_ndk.sh +COPY config.sh /opt/android/cakewallet/ +COPY install_ndk.sh /opt/android/cakewallet/ RUN ./install_ndk.sh +COPY build_iconv.sh /opt/android/cakewallet/ RUN ./build_iconv.sh + +COPY build_boost.sh /opt/android/cakewallet/ +COPY init_boost.sh /opt/android/cakewallet/ +COPY finish_boost.sh /opt/android/cakewallet/ RUN ./build_boost.sh + +COPY build_openssl.sh /opt/android/cakewallet/ RUN ./build_openssl.sh + +COPY build_sodium.sh /opt/android/cakewallet/ RUN ./build_sodium.sh + +COPY build_unbound.sh /opt/android/cakewallet/ RUN ./build_unbound.sh + +COPY build_zmq.sh /opt/android/cakewallet/ RUN ./build_zmq.sh +COPY entrypoint.sh /opt/android/cakewallet/ +COPY build_monero.sh /opt/android/cakewallet/ +COPY copy_monero_deps.sh /opt/android/cakewallet/ +COPY build_haven.sh /opt/android/cakewallet/ +COPY copy_haven_deps.sh /opt/android/cakewallet/ ENTRYPOINT ["./entrypoint.sh"] diff --git a/scripts/docker/build_all.sh b/scripts/docker/build_all.sh old mode 100644 new mode 100755 index 0acb7fcde..a4163c3f4 --- a/scripts/docker/build_all.sh +++ b/scripts/docker/build_all.sh @@ -1 +1,17 @@ -#!/bin/sh if [ -z "$APP_ANDROID_TYPE" ]; then echo "Please set APP_ANDROID_TYPE" exit 1 fi DIR=$(dirname "$0") case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh $DIR/build_haven.sh ;; "haven") $DIR/build_haven_all.sh ;; esac \ No newline at end of file +#!/bin/sh + +set -x -e + +if [ -z "$APP_ANDROID_TYPE" ]; then + echo "Please set APP_ANDROID_TYPE" + exit 1 +fi + +DIR=$(dirname "$0") + +case $APP_ANDROID_TYPE in + "monero.com") $DIR/build_monero_all.sh ;; + "cakewallet") $DIR/build_monero_all.sh + $DIR/build_haven.sh ;; + "haven") $DIR/build_haven_all.sh ;; +esac diff --git a/scripts/docker/build_boost.sh b/scripts/docker/build_boost.sh old mode 100644 new mode 100755 index 2c98afab5..97333bbee --- a/scripts/docker/build_boost.sh +++ b/scripts/docker/build_boost.sh @@ -1,5 +1,5 @@ #!/bin/bash - +set -x -e . ./config.sh BOOST_SRC_DIR=$WORKDIR/boost_1_72_0 BOOST_FILENAME=boost_1_72_0.tar.bz2 diff --git a/scripts/docker/build_haven.sh b/scripts/docker/build_haven.sh old mode 100644 new mode 100755 index 7927c5102..1cfb16265 --- a/scripts/docker/build_haven.sh +++ b/scripts/docker/build_haven.sh @@ -1 +1,71 @@ -#!/bin/sh . ./config.sh HAVEN_VERSION=tags/v3.0.7 HAVEN_SRC_DIR=${WORKDIR}/haven git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} git checkout ${HAVEN_VERSION} cd $HAVEN_SRC_DIR git submodule init git submodule update for arch in "aarch" "aarch64" "i686" "x86_64" do FLAGS="" PREFIX=${WORKDIR}/prefix_${arch} DEST_LIB_DIR=${PREFIX}/lib/haven DEST_INCLUDE_DIR=${PREFIX}/include/haven export CMAKE_INCLUDE_PATH="${PREFIX}/include" export CMAKE_LIBRARY_PATH="${PREFIX}/lib" ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" mkdir -p $DEST_LIB_DIR mkdir -p $DEST_INCLUDE_DIR case $arch in "aarch" ) CLANG=arm-linux-androideabi-clang CXXLANG=arm-linux-androideabi-clang++ BUILD_64=OFF TAG="android-armv7" ARCH="armv7-a" ARCH_ABI="armeabi-v7a" FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; "aarch64" ) CLANG=aarch64-linux-androideabi-clang CXXLANG=aarch64-linux-androideabi-clang++ BUILD_64=ON TAG="android-armv8" ARCH="armv8-a" ARCH_ABI="arm64-v8a";; "i686" ) CLANG=i686-linux-androideabi-clang CXXLANG=i686-linux-androideabi-clang++ BUILD_64=OFF TAG="android-x86" ARCH="i686" ARCH_ABI="x86";; "x86_64" ) CLANG=x86_64-linux-androideabi-clang CXXLANG=x86_64-linux-androideabi-clang++ BUILD_64=ON TAG="android-x86_64" ARCH="x86-64" ARCH_ABI="x86_64";; esac cd $HAVEN_SRC_DIR rm -rf ./build/release mkdir -p ./build/release cd ./build/release CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. make wallet_api -j$THREADS find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; cp -r ./lib/* $DEST_LIB_DIR cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR done \ No newline at end of file +#!/bin/sh +set -x -e + +. ./config.sh +HAVEN_VERSION=tags/v3.0.7 +HAVEN_SRC_DIR=${WORKDIR}/haven + +git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} +cd $HAVEN_SRC_DIR +git checkout ${HAVEN_VERSION} +git submodule init +git submodule update + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +FLAGS="" +PREFIX=${WORKDIR}/prefix_${arch} +DEST_LIB_DIR=${PREFIX}/lib/haven +DEST_INCLUDE_DIR=${PREFIX}/include/haven +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" +ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" +PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +case $arch in + "aarch" ) + CLANG=arm-linux-androideabi-clang + CXXLANG=arm-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-armv7" + ARCH="armv7-a" + ARCH_ABI="armeabi-v7a" + FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; + "aarch64" ) + CLANG=aarch64-linux-androideabi-clang + CXXLANG=aarch64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-armv8" + ARCH="armv8-a" + ARCH_ABI="arm64-v8a";; + "i686" ) + CLANG=i686-linux-androideabi-clang + CXXLANG=i686-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-x86" + ARCH="i686" + ARCH_ABI="x86";; + "x86_64" ) + CLANG=x86_64-linux-androideabi-clang + CXXLANG=x86_64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-x86_64" + ARCH="x86-64" + ARCH_ABI="x86_64";; +esac + +cd $HAVEN_SRC_DIR +rm -rf ./build/release +mkdir -p ./build/release +cd ./build/release +CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. + +make wallet_api -j$THREADS +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; + +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR +done diff --git a/scripts/docker/build_haven_all.sh b/scripts/docker/build_haven_all.sh old mode 100644 new mode 100755 index 4b33ad077..ce8eb3f0e --- a/scripts/docker/build_haven_all.sh +++ b/scripts/docker/build_haven_all.sh @@ -1 +1,9 @@ -#!/bin/bash ./build_iconv.sh ./build_boost.sh ./build_openssl.sh ./build_sodium.sh ./build_zmq.sh ./build_haven.sh \ No newline at end of file +#!/bin/bash +set -x -e + +./build_iconv.sh +./build_boost.sh +./build_openssl.sh +./build_sodium.sh +./build_zmq.sh +./build_haven.sh diff --git a/scripts/docker/build_iconv.sh b/scripts/docker/build_iconv.sh old mode 100644 new mode 100755 index 9edac26b3..e55686fec --- a/scripts/docker/build_iconv.sh +++ b/scripts/docker/build_iconv.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e . ./config.sh export ICONV_FILENAME=libiconv-1.16.tar.gz diff --git a/scripts/docker/build_monero.sh b/scripts/docker/build_monero.sh old mode 100644 new mode 100755 index d663f5288..04162f0f8 --- a/scripts/docker/build_monero.sh +++ b/scripts/docker/build_monero.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e . ./config.sh diff --git a/scripts/docker/build_openssl.sh b/scripts/docker/build_openssl.sh old mode 100644 new mode 100755 index 685d0a1be..233e64a7c --- a/scripts/docker/build_openssl.sh +++ b/scripts/docker/build_openssl.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e set -e diff --git a/scripts/docker/build_sodium.sh b/scripts/docker/build_sodium.sh old mode 100644 new mode 100755 index a934d641b..c911814d9 --- a/scripts/docker/build_sodium.sh +++ b/scripts/docker/build_sodium.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e . ./config.sh SODIUM_SRC_DIR=${WORKDIR}/libsodium diff --git a/scripts/docker/build_unbound.sh b/scripts/docker/build_unbound.sh old mode 100644 new mode 100755 index 8786b0f2b..2d1efdea2 --- a/scripts/docker/build_unbound.sh +++ b/scripts/docker/build_unbound.sh @@ -1,7 +1,7 @@ #!/bin/bash +set -x -e . ./config.sh - EXPAT_VERSION=R_2_4_8 EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" EXPAT_SRC_DIR=$WORKDIR/libexpat diff --git a/scripts/docker/build_zmq.sh b/scripts/docker/build_zmq.sh old mode 100644 new mode 100755 index bbff9e41b..19bb99172 --- a/scripts/docker/build_zmq.sh +++ b/scripts/docker/build_zmq.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e . ./config.sh ZMQ_SRC_DIR=$WORKDIR/libzmq diff --git a/scripts/docker/config.sh b/scripts/docker/config.sh old mode 100644 new mode 100755 index c5067f2c3..a9b691688 --- a/scripts/docker/config.sh +++ b/scripts/docker/config.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e export API=21 export WORKDIR=/opt/android diff --git a/scripts/docker/copy_haven_deps.sh b/scripts/docker/copy_haven_deps.sh old mode 100644 new mode 100755 index d59e9d7f0..cef644701 --- a/scripts/docker/copy_haven_deps.sh +++ b/scripts/docker/copy_haven_deps.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x WORKDIR=/opt/android CW_DIR=${WORKDIR}/cake_wallet diff --git a/scripts/docker/copy_monero_deps.sh b/scripts/docker/copy_monero_deps.sh old mode 100644 new mode 100755 index e4392186c..1c2394c0d --- a/scripts/docker/copy_monero_deps.sh +++ b/scripts/docker/copy_monero_deps.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x WORKDIR=/opt/android CW_EXRTERNAL_DIR=${WORKDIR}/output/android diff --git a/scripts/docker/docker-compose.yml b/scripts/docker/docker-compose.yml old mode 100644 new mode 100755 index eaeea0f5b..00f24ce2e --- a/scripts/docker/docker-compose.yml +++ b/scripts/docker/docker-compose.yml @@ -7,3 +7,5 @@ services: MONERO_BRANCH: release-v0.18.2.2-android volumes: - ./output:/opt/android/output + - ./cache/dotcache:/root/.cache + - ./cache/dotccache:/root/.ccache diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh old mode 100644 new mode 100755 index e4bdc017c..14f02a1f8 --- a/scripts/docker/entrypoint.sh +++ b/scripts/docker/entrypoint.sh @@ -1,4 +1,11 @@ #!/bin/bash +set -x -e + +ls /opt/android + +rm -rf monero haven ./build_monero.sh +./build_haven.sh ./copy_monero_deps.sh +./copy_haven_deps.sh diff --git a/scripts/docker/finish_boost.sh b/scripts/docker/finish_boost.sh old mode 100644 new mode 100755 index e3f195276..774c65d77 --- a/scripts/docker/finish_boost.sh +++ b/scripts/docker/finish_boost.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e ARCH=$1 PREFIX=$2 diff --git a/scripts/docker/init_boost.sh b/scripts/docker/init_boost.sh old mode 100644 new mode 100755 index ffb7a1416..068647e1f --- a/scripts/docker/init_boost.sh +++ b/scripts/docker/init_boost.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -x -e + ARCH=$1 PREFIX=$2 @@ -17,6 +19,6 @@ echo $BOOST_SHA256 $BOOST_FILE_PATH | sha256sum -c - || exit 1 cd $WORKDIR rm -rf $BOOST_SRC_DIR rm -rf $PREFIX/include/boost -tar -xvf $BOOST_FILE_PATH -C $WORKDIR +tar -xf $BOOST_FILE_PATH -C $WORKDIR cd $BOOST_SRC_DIR -./bootstrap.sh --prefix=${PREFIX} +./bootstrap.sh --prefix=${PREFIX} --with-toolset=gcc diff --git a/scripts/docker/install_ndk.sh b/scripts/docker/install_ndk.sh old mode 100644 new mode 100755 index 5f97751e3..94373954c --- a/scripts/docker/install_ndk.sh +++ b/scripts/docker/install_ndk.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -x -e . ./config.sh TOOLCHAIN_DIR=${WORKDIR}/toolchain diff --git a/scripts/gen_android_manifest.sh b/scripts/gen_android_manifest.sh new file mode 100755 index 000000000..d6f7a130d --- /dev/null +++ b/scripts/gen_android_manifest.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +ANDROID_SCRIPTS_DIR=`pwd`/android +if [ ! -d $ANDROID_SCRIPTS_DIR ]; then + echo "no android scripts directory found at ${ANDROID_SCRIPTS_DIR}" + exit 0 +fi + +cd $ANDROID_SCRIPTS_DIR +./manifest.sh diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index ab7fbd422..67375c914 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -9,8 +9,10 @@ if [ -z "$APP_IOS_TYPE" ]; then echo "Please set APP_IOS_TYPE" exit 1 fi - -cd ../.. # go to root +./gen_framework.sh +cd .. # go to scipts +./gen_android_manifest.sh +cd .. # go to root cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${APP_IOS_NAME}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${APP_IOS_BUNDLE_ID}" ./ios/Runner/Info.plist @@ -20,6 +22,7 @@ cp -rf ./ios/Runner/InfoBase.plist ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLName string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes array" ./ios/Runner/Info.plist /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:1:CFBundleURLSchemes: string ${APP_IOS_TYPE}" ./ios/Runner/Info.plist +sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_IOS_BUNDLE_ID}/g" ./ios/Runner.xcodeproj/project.pbxproj CONFIG_ARGS="" @@ -28,7 +31,10 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero" + if [ "$CW_WITH_HAVEN" = true ];then + CONFIG_ARGS="$CONFIG_ARGS --haven" + fi ;; $HAVEN) diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 974e44bc4..7d52d42bb 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.15.2" -MONERO_COM_BUILD_NUMBER=90 +MONERO_COM_VERSION="1.16.0" +MONERO_COM_BUILD_NUMBER=91 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.18.2" -CAKEWALLET_BUILD_NUMBER=250 +CAKEWALLET_VERSION="4.19.0" +CAKEWALLET_BUILD_NUMBER=251 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/ios/build_monero_all.sh b/scripts/ios/build_monero_all.sh index 2b61f6db0..aec6d86f3 100755 --- a/scripts/ios/build_monero_all.sh +++ b/scripts/ios/build_monero_all.sh @@ -1,10 +1,27 @@ #!/bin/sh . ./config.sh -./install_missing_headers.sh -./build_openssl.sh -./build_boost.sh -./build_sodium.sh -./build_zmq.sh -./build_unbound.sh -./build_monero.sh \ No newline at end of file +# ./install_missing_headers.sh +# ./build_openssl.sh +# ./build_boost.sh +# ./build_sodium.sh +# ./build_zmq.sh +# ./build_unbound.sh + +set -x -e + +cd "$(dirname "$0")" + +NPROC="-j$(sysctl -n hw.logicalcpu)" + +../prepare_moneroc.sh + +for COIN in monero wownero; +do + pushd ../monero_c + ./build_single.sh ${COIN} host-apple-ios $NPROC + popd +done + +unxz -f ../monero_c/release/monero/host-apple-ios_libwallet2_api_c.dylib.xz +unxz -f ../monero_c/release/wownero/host-apple-ios_libwallet2_api_c.dylib.xz diff --git a/scripts/ios/build_openssl.sh b/scripts/ios/build_openssl.sh index 57a59500b..d03054cd3 100755 --- a/scripts/ios/build_openssl.sh +++ b/scripts/ios/build_openssl.sh @@ -12,6 +12,8 @@ git clone $OPEN_SSL_URL $OPEN_SSL_DIR_PATH cd $OPEN_SSL_DIR_PATH ./build-libssl.sh --version=1.1.1q --targets="ios-cross-arm64" --deprecated -mv ${OPEN_SSL_DIR_PATH}/include/* $EXTERNAL_IOS_INCLUDE_DIR +# copy and then remove because mv is not working when there is subdirectories +cp -R ${OPEN_SSL_DIR_PATH}/include/* $EXTERNAL_IOS_INCLUDE_DIR +rm -rf ${OPEN_SSL_DIR_PATH}/include/ mv ${OPEN_SSL_DIR_PATH}/lib/libcrypto-iOS.a ${EXTERNAL_IOS_LIB_DIR}/libcrypto.a mv ${OPEN_SSL_DIR_PATH}/lib/libssl-iOS.a ${EXTERNAL_IOS_LIB_DIR}/libssl.a \ No newline at end of file diff --git a/scripts/ios/gen_framework.sh b/scripts/ios/gen_framework.sh new file mode 100755 index 000000000..950a7afe5 --- /dev/null +++ b/scripts/ios/gen_framework.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Assume we are in scripts/ios +IOS_DIR="$(pwd)/../../ios" +DYLIB_NAME="monero_libwallet2_api_c.dylib" +DYLIB_LINK_PATH="${IOS_DIR}/${DYLIB_NAME}" +FRWK_DIR="${IOS_DIR}/MoneroWallet.framework" + +if [ ! -f $DYLIB_LINK_PATH ]; then + echo "Dylib is not found by the link: ${DYLIB_LINK_PATH}" + exit 0 +fi + +cd $FRWK_DIR # go to iOS framework dir +lipo -create $DYLIB_LINK_PATH -output MoneroWallet + +echo "Generated ${FRWK_DIR}" +# also generate for wownero +IOS_DIR="$(pwd)/../../ios" +DYLIB_NAME="wownero_libwallet2_api_c.dylib" +DYLIB_LINK_PATH="${IOS_DIR}/${DYLIB_NAME}" +FRWK_DIR="${IOS_DIR}/WowneroWallet.framework" + +if [ ! -f $DYLIB_LINK_PATH ]; then + echo "Dylib is not found by the link: ${DYLIB_LINK_PATH}" + exit 0 +fi + +cd $FRWK_DIR # go to iOS framework dir +lipo -create $DYLIB_LINK_PATH -output WowneroWallet + +echo "Generated ${FRWK_DIR}" \ No newline at end of file diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index a1143bb12..b8785a9be 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -9,7 +9,9 @@ if [ -z "$APP_MACOS_TYPE" ]; then exit 1 fi -cd ../.. # go to root +cd .. # go to scipts +./gen_android_manifest.sh +cd .. # go to root cp -rf ./macos/Runner/InfoBase.plist ./macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${APP_MACOS_NAME}" ./macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleName ${APP_MACOS_NAME}" ./macos/Runner/Info.plist @@ -20,9 +22,11 @@ cp -rf ./macos/Runner/InfoBase.plist ./macos/Runner/Info.plist # Fill entitlements Bundle ID cp -rf ./macos/Runner/DebugProfileBase.entitlements ./macos/Runner/DebugProfile.entitlements cp -rf ./macos/Runner/ReleaseBase.entitlements ./macos/Runner/Release.entitlements +cp -rf ./macos/Runner/RunnerBase.entitlements ./macos/Runner/Runner.entitlements cp -rf ./macos/Runner/Configs/AppInfoBase.xcconfig ./macos/Runner/Configs/AppInfo.xcconfig sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile.entitlements sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements +sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Runner.entitlements sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" @@ -31,7 +35,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml @@ -40,4 +44,4 @@ flutter pub run tool/generate_pubspec.dart flutter pub get flutter packages pub run tool/configure.dart $CONFIG_ARGS cd $DIR -$DIR/app_icon.sh \ No newline at end of file +$DIR/app_icon.sh diff --git a/scripts/macos/build_monero_all.sh b/scripts/macos/build_monero_all.sh index f7e55909b..9f6130066 100755 --- a/scripts/macos/build_monero_all.sh +++ b/scripts/macos/build_monero_all.sh @@ -1,20 +1,59 @@ #!/bin/sh +set -x -e -ARCH=`uname -m` +cd "$(dirname "$0")" -. ./config.sh +NPROC="-j$(sysctl -n hw.logicalcpu)" +MONERO_LIBS="" +WOWNERO_LIBS="" +MONEROC_RELEASE_DIR="../monero_c/release/monero" +WOWNEROC_RELEASE_DIR="../monero_c/release/wownero" -case $ARCH in - arm64) - ./build_openssl_arm64.sh - ./build_boost_arm64.sh;; - x86_64) - ./build_openssl_x86_64.sh - ./build_boost_x86_64.sh;; -esac +../prepare_moneroc.sh -./build_zmq.sh -./build_expat.sh -./build_unbound.sh -./build_sodium.sh -./build_monero.sh \ No newline at end of file +# NOTE: -j1 is intentional. Otherwise you will run into weird behaviour on macos +if [[ ! "x$USE_DOCKER" == "x" ]]; +then + for COIN in monero wownero; + do + pushd ../monero_c + echo "unsupported!" + exit 1 + popd + done +else + if [[ "x$1" == "xuniversal" ]]; then + ARCHS=(arm64 x86_64) + else + ARCHS=$(uname -m) + fi + for COIN in monero wownero; + do + for ARCH in "${ARCHS[@]}"; + do + if [[ "$ARCH" == "arm64" ]]; then + export HOMEBREW_PREFIX=/opt/homebrew + HOST="aarch64-host-apple-darwin" + else + export HOMEBREW_PREFIX=/usr/local + HOST="${ARCH}-host-apple-darwin" + fi + + MONERO_LIBS=" -arch ${ARCH} ${MONEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" + WOWNERO_LIBS=" -arch ${ARCH} ${WOWNEROC_RELEASE_DIR}/${HOST}_libwallet2_api_c.dylib" + + if [[ ! $(uname -m) == $ARCH ]]; then + PRC="arch -${ARCH}" + fi + + pushd ../monero_c + $PRC ./build_single.sh ${COIN} ${HOST} $NPROC + unxz -f ./release/${COIN}/${HOST}_libwallet2_api_c.dylib.xz + + popd + done + done +fi + +lipo -create ${MONERO_LIBS} -output "${MONEROC_RELEASE_DIR}/host-apple-darwin_libwallet2_api_c.dylib" +lipo -create ${WOWNERO_LIBS} -output "${WOWNEROC_RELEASE_DIR}/host-apple-darwin_libwallet2_api_c.dylib" diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh new file mode 100755 index 000000000..27d839ef4 --- /dev/null +++ b/scripts/prepare_moneroc.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -x -e + +cd "$(dirname "$0")" + +if [[ ! -d "monero_c" ]]; +then + git clone https://github.com/mrcyjanek/monero_c --branch rewrite-wip + cd monero_c + git checkout eaa7bdb8be3479418445ddb18bf33d453f64afcf + git reset --hard + git submodule update --init --force --recursive + ./apply_patches.sh monero + ./apply_patches.sh wownero +else + cd monero_c +fi + +if [[ ! -f "monero/.patch-applied" ]]; +then + ./apply_patches.sh monero +fi + +if [[ ! -f "wownero/.patch-applied" ]]; +then + ./apply_patches.sh wownero +fi +cd .. + +echo "monero_c source prepared". diff --git a/scripts/windows/build_all.sh b/scripts/windows/build_all.sh new file mode 100755 index 000000000..e77f6edb5 --- /dev/null +++ b/scripts/windows/build_all.sh @@ -0,0 +1,37 @@ +set -x -e + +cd "$(dirname "$0")" + +if [[ ! "x$(uname)" == "xLinux" ]]; +then + echo "Only Linux hosts can build windows (yes, i know)"; + exit 1 +fi + +../prepare_moneroc.sh + +# export USE_DOCKER="ON" + +pushd ../monero_c + set +e + command -v sudo && export SUDO=sudo + set -e + NPROC="-j$(nproc)" + if [[ ! "x$USE_DOCKER" == "x" ]]; + then + for COIN in monero wownero; + do + $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 gperf libtinfo5; ./build_single.sh ${COIN} x86_64-w64-mingw32 $NPROC" + # $SUDO docker run --platform linux/amd64 -v$HOME/.cache/ccache:/root/.ccache -v$PWD:$PWD -w $PWD --rm -it git.mrcyjanek.net/mrcyjanek/debian:buster bash -c "git config --global --add safe.directory '*'; apt update; apt install -y ccache gcc-mingw-w64-i686 g++-mingw-w64-i686 gperf libtinfo5; ./build_single.sh ${COIN} i686-w64-mingw32 $NPROC" + done + else + for COIN in monero wownero; + do + $SUDO ./build_single.sh ${COIN} x86_64-w64-mingw32 $NPROC + # $SUDO ./build_single.sh ${COIN} i686-w64-mingw32 $NPROC + done + fi +popd + +$SUDO unxz -f ../monero_c/release/monero/*.dll.xz +$SUDO unxz -f ../monero_c/release/wownero/*.dll.xz diff --git a/scripts/windows/build_exe_installer.iss b/scripts/windows/build_exe_installer.iss new file mode 100644 index 000000000..4315f7d27 --- /dev/null +++ b/scripts/windows/build_exe_installer.iss @@ -0,0 +1,44 @@ +#define MyAppName "Cake Wallet" +#define MyAppVersion "0.0.1" +#define MyAppPublisher "Cake Labs LLC" +#define MyAppURL "https://cakewallet.com/" +#define MyAppExeName "CakeWallet.exe" + +[Setup] +AppId=com.cakewallet.cakewallet +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\CakeWallet +DisableProgramGroupPage=yes +LicenseFile=..\..\LICENSE.md +; Uncomment the following line to run in non administrative install mode (install for current user only.) +; PrivilegesRequired=lowest +OutputDir=..\..\ +OutputBaseFilename=cakewallet_setup +SetupIconFile=..\..\windows\runner\resources\app_icon.ico +Compression=lzma +SolidCompression=yes +WizardStyle=modern + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + diff --git a/scripts/windows/cakewallet.sh b/scripts/windows/cakewallet.sh new file mode 100755 index 000000000..61e526a73 --- /dev/null +++ b/scripts/windows/cakewallet.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# This (wrapper) script should be run in wsl with installed (windows "side") flutter +# and available cmd.exe in PATH +# Assume that we are in scripts/windows dir +CW_ROOT=`pwd`/../.. +cd $CW_ROOT +cmd.exe /c cakewallet.bat $1 \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 133f12e52..fcfd676dc 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -9,6 +9,7 @@ const nanoOutputPath = 'lib/nano/nano.dart'; const polygonOutputPath = 'lib/polygon/polygon.dart'; const solanaOutputPath = 'lib/solana/solana.dart'; const tronOutputPath = 'lib/tron/tron.dart'; +const wowneroOutputPath = 'lib/wownero/wownero.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -26,6 +27,7 @@ Future main(List args) async { final hasPolygon = args.contains('${prefix}polygon'); final hasSolana = args.contains('${prefix}solana'); final hasTron = args.contains('${prefix}tron'); + final hasWownero = args.contains('${prefix}wownero'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -37,6 +39,7 @@ Future main(List args) async { await generatePolygon(hasPolygon); await generateSolana(hasSolana); await generateTron(hasTron); + await generateWownero(hasWownero); // await generateBanano(hasEthereum); await generatePubspec( @@ -51,6 +54,7 @@ Future main(List args) async { hasPolygon: hasPolygon, hasSolana: hasSolana, hasTron: hasTron, + hasWownero: hasWownero, ); await generateWalletTypes( hasMonero: hasMonero, @@ -63,6 +67,7 @@ Future main(List args) async { hasPolygon: hasPolygon, hasSolana: hasSolana, hasTron: hasTron, + hasWownero: hasWownero, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -239,7 +244,6 @@ Future generateMonero(bool hasImplementation) async { const moneroCommonHeaders = """ import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_coins_info.dart'; -import 'package:cw_monero/monero_unspent.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -256,6 +260,7 @@ import 'package:polyseed/polyseed.dart';"""; import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_monero/monero_unspent.dart'; import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -352,6 +357,8 @@ abstract class Monero { List getUnspents(Object wallet); Future updateUnspents(Object wallet); + Future getCurrentHeight(); + WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ required String name, required String spendKey, @@ -414,6 +421,187 @@ abstract class MoneroAccountList { await outputFile.writeAsString(output); } +Future generateWownero(bool hasImplementation) async { + final outputFile = File(wowneroOutputPath); + const wowneroCommonHeaders = """ +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; +import 'package:polyseed/polyseed.dart';"""; + const wowneroCWHeaders = """ +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/wownero_amount_format.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_wownero/wownero_unspent.dart'; +import 'package:cw_wownero/wownero_wallet_service.dart'; +import 'package:cw_wownero/wownero_wallet.dart'; +import 'package:cw_wownero/wownero_transaction_info.dart'; +import 'package:cw_wownero/wownero_transaction_creation_credentials.dart'; +import 'package:cw_core/account.dart' as wownero_account; +import 'package:cw_wownero/api/wallet.dart' as wownero_wallet_api; +import 'package:cw_wownero/mnemonics/english.dart'; +import 'package:cw_wownero/mnemonics/chinese_simplified.dart'; +import 'package:cw_wownero/mnemonics/dutch.dart'; +import 'package:cw_wownero/mnemonics/german.dart'; +import 'package:cw_wownero/mnemonics/japanese.dart'; +import 'package:cw_wownero/mnemonics/russian.dart'; +import 'package:cw_wownero/mnemonics/spanish.dart'; +import 'package:cw_wownero/mnemonics/portuguese.dart'; +import 'package:cw_wownero/mnemonics/french.dart'; +import 'package:cw_wownero/mnemonics/italian.dart'; +import 'package:cw_wownero/pending_wownero_transaction.dart'; +"""; + const wowneroCwPart = "part 'cw_wownero.dart';"; + const wowneroContent = """ +class Account { + Account({required this.id, required this.label, this.balance}); + final int id; + final String label; + final String? balance; +} + +class Subaddress { + Subaddress({ + required this.id, + required this.label, + required this.address}); + final int id; + final String label; + final String address; +} + +class WowneroBalance extends Balance { + WowneroBalance({required this.fullBalance, required this.unlockedBalance}) + : formattedFullBalance = wownero!.formatterWowneroAmountToString(amount: fullBalance), + formattedUnlockedBalance = + wownero!.formatterWowneroAmountToString(amount: unlockedBalance), + super(unlockedBalance, fullBalance); + + WowneroBalance.fromString( + {required this.formattedFullBalance, + required this.formattedUnlockedBalance}) + : fullBalance = wownero!.formatterWowneroParseAmount(amount: formattedFullBalance), + unlockedBalance = wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance), + super(wownero!.formatterWowneroParseAmount(amount: formattedUnlockedBalance), + wownero!.formatterWowneroParseAmount(amount: formattedFullBalance)); + + final int fullBalance; + final int unlockedBalance; + final String formattedFullBalance; + final String formattedUnlockedBalance; + + @override + String get formattedAvailableBalance => formattedUnlockedBalance; + + @override + String get formattedAdditionalBalance => formattedFullBalance; +} + +abstract class WowneroWalletDetails { + @observable + late Account account; + + @observable + late WowneroBalance balance; +} + +abstract class Wownero { + WowneroAccountList getAccountList(Object wallet); + + WowneroSubaddressList getSubaddressList(Object wallet); + + TransactionHistoryBase getTransactionHistory(Object wallet); + + WowneroWalletDetails getWowneroWalletDetails(Object wallet); + + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); + + String getSubaddressLabel(Object wallet, int accountIndex, int addressIndex); + + int getHeightByDate({required DateTime date}); + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority getWowneroTransactionPrioritySlow(); + TransactionPriority getWowneroTransactionPriorityAutomatic(); + TransactionPriority deserializeWowneroTransactionPriority({required int raw}); + List getTransactionPriorities(); + List getWowneroWordList(String language); + + List getUnspents(Object wallet); + Future updateUnspents(Object wallet); + + Future getCurrentHeight(); + + WalletCredentials createWowneroRestoreWalletFromKeysCredentials({ + required String name, + required String spendKey, + required String viewKey, + required String address, + required String password, + required String language, + required int height}); + WalletCredentials createWowneroRestoreWalletFromSeedCredentials({required String name, required String password, required int height, required String mnemonic}); + WalletCredentials createWowneroNewWalletCredentials({required String name, required String language, required bool isPolyseed, String password}); + Map getKeys(Object wallet); + Object createWowneroTransactionCreationCredentials({required List outputs, required TransactionPriority priority}); + Object createWowneroTransactionCreationCredentialsRaw({required List outputs, required TransactionPriority priority}); + String formatterWowneroAmountToString({required int amount}); + double formatterWowneroAmountToDouble({required int amount}); + int formatterWowneroParseAmount({required String amount}); + Account getCurrentAccount(Object wallet); + void setCurrentAccount(Object wallet, int id, String label, String? balance); + void onStartup(); + int getTransactionInfoAccountId(TransactionInfo tx); + WalletService createWowneroWalletService(Box walletInfoSource, Box unspentCoinSource); + Map pendingTransactionInfo(Object transaction); +} + +abstract class WowneroSubaddressList { + ObservableList get subaddresses; + void update(Object wallet, {required int accountIndex}); + void refresh(Object wallet, {required int accountIndex}); + List getAll(Object wallet); + Future addSubaddress(Object wallet, {required int accountIndex, required String label}); + Future setLabelSubaddress(Object wallet, + {required int accountIndex, required int addressIndex, required String label}); +} + +abstract class WowneroAccountList { + ObservableList get accounts; + void update(Object wallet); + void refresh(Object wallet); + List getAll(Object wallet); + Future addAccount(Object wallet, {required String label}); + Future setLabelAccount(Object wallet, {required int accountIndex, required String label}); +} + """; + + const wowneroEmptyDefinition = 'Wownero? wownero;\n'; + const wowneroCWDefinition = 'Wownero? wownero = CWWownero();\n'; + + final output = '$wowneroCommonHeaders\n' + + (hasImplementation ? '$wowneroCWHeaders\n' : '\n') + + (hasImplementation ? '$wowneroCwPart\n\n' : '\n') + + (hasImplementation ? wowneroCWDefinition : wowneroEmptyDefinition) + + '\n' + + wowneroContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generateHaven(bool hasImplementation) async { final outputFile = File(havenOutputPath); const havenCommonHeaders = """ @@ -1154,18 +1342,20 @@ abstract class Tron { await outputFile.writeAsString(output); } -Future generatePubspec( - {required bool hasMonero, - required bool hasBitcoin, - required bool hasHaven, - required bool hasEthereum, - required bool hasNano, - required bool hasBanano, - required bool hasBitcoinCash, - required bool hasFlutterSecureStorage, - required bool hasPolygon, - required bool hasSolana, - required bool hasTron}) async { +Future generatePubspec({ + required bool hasMonero, + required bool hasBitcoin, + required bool hasHaven, + required bool hasEthereum, + required bool hasNano, + required bool hasBanano, + required bool hasBitcoinCash, + required bool hasFlutterSecureStorage, + required bool hasPolygon, + required bool hasSolana, + required bool hasTron, + required bool hasWownero, +}) async { const cwCore = """ cw_core: path: ./cw_core @@ -1191,8 +1381,8 @@ Future generatePubspec( git: url: https://github.com/cake-tech/flutter_secure_storage.git path: flutter_secure_storage - ref: cake-8.0.0 - version: 8.0.0 + ref: cake-8.1.0 + version: 8.1.0 """; const cwEthereum = """ cw_ethereum: @@ -1226,15 +1416,21 @@ Future generatePubspec( cw_tron: path: ./cw_tron """; + const cwWownero = """ + cw_wownero: + path: ./cw_wownero + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); - final dependenciesIndex = - inputLines.indexWhere((line) => line.toLowerCase().contains('dependencies:')); + final dependenciesIndex = inputLines.indexWhere((line) => Platform.isWindows + // On Windows it could contains `\r` (Carriage Return). It could be fixed in newer dart versions. + ? line.toLowerCase() == 'dependencies:\r' || line.toLowerCase() == 'dependencies:' + : line.toLowerCase() == 'dependencies:'); var output = cwCore; if (hasMonero) { - output += '\n$cwMonero\n$cwSharedExternal'; + output += '\n$cwMonero'; } if (hasBitcoin) { @@ -1269,10 +1465,8 @@ Future generatePubspec( output += '\n$cwTron'; } - if (hasHaven && !hasMonero) { + if (hasHaven) { output += '\n$cwSharedExternal\n$cwHaven'; - } else if (hasHaven) { - output += '\n$cwHaven'; } if (hasFlutterSecureStorage) { @@ -1283,6 +1477,10 @@ Future generatePubspec( output += '\n$cwEVM'; } + if (hasWownero) { + output += '\n$cwWownero'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -1295,17 +1493,19 @@ Future generatePubspec( await outputFile.writeAsString(outputContent); } -Future generateWalletTypes( - {required bool hasMonero, - required bool hasBitcoin, - required bool hasHaven, - required bool hasEthereum, - required bool hasNano, - required bool hasBanano, - required bool hasBitcoinCash, - required bool hasPolygon, - required bool hasSolana, - required bool hasTron}) async { +Future generateWalletTypes({ + required bool hasMonero, + required bool hasBitcoin, + required bool hasHaven, + required bool hasEthereum, + required bool hasNano, + required bool hasBanano, + required bool hasBitcoinCash, + required bool hasPolygon, + required bool hasSolana, + required bool hasTron, + required bool hasWownero, +}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -1356,6 +1556,10 @@ Future generateWalletTypes( outputContent += '\tWalletType.banano,\n'; } + if (hasWownero) { + outputContent += '\tWalletType.wownero,\n'; + } + if (hasHaven) { outputContent += '\tWalletType.haven,\n'; } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 72a019636..42379021f 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -16,6 +16,7 @@ const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; const nanoConfigPath = 'tool/.nano-secrets-config.json'; const nanoOutputPath = 'cw_nano/lib/.secrets.g.dart'; + Future main(List args) async => importSecretsConfig(); Future importSecretsConfig() async { diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 000000000..7d1c03451 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,123 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(cake_wallet LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "CakeWallet") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libwallet2_api_c.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "monero_libwallet2_api_c.dll" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../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}/../scripts/monero_c/release/monero/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}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libpolyseed.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libpolyseed.dll" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libssp-0.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libssp-0.dll" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libstdc++-6.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libstdc++-6.dll" + COMPONENT Runtime) + +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/x86_64-w64-mingw32_libwinpthread-1.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwinpthread-1.dll" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..903f4899d --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..323f53c9f --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + FlutterLocalAuthenticationPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLocalAuthenticationPluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..d6d9b0a49 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus + flutter_local_authentication + flutter_secure_storage_windows + permission_handler_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + sp_scanner +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..394917c05 --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 000000000..0a899f86e --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.cakewallet.cake_wallet" "\0" + VALUE "FileDescription", "Cake Wallet" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "Cake Wallet" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 Cake Wallet. All rights reserved." "\0" + VALUE "OriginalFilename", "Cake Wallet.exe" "\0" + VALUE "ProductName", "Cake Wallet" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..955ee3038 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 000000000..a7eecbf9c --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"Cake Wallet", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..242fb9cb5f35992d02996c591a2bc69a08b5ac94 GIT binary patch literal 17470 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 + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 000000000..b2b08734d --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 000000000..60608d0fe --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 000000000..e901dde68 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From 73492ad8655d8f78b3d351d654fe90ec364cada4 Mon Sep 17 00:00:00 2001 From: Matthew Fosse Date: Thu, 4 Jul 2024 21:44:08 +0200 Subject: [PATCH 20/20] update nano default node (#1408) * update nano default node * fix node indicator * Update pr_test_build.yml * Update pr_test_build.yml * update default nano node for new wallets * support extra args on tool script * remove nano secrets from node.dart --------- Co-authored-by: Omar Hatem --- .github/workflows/pr_test_build.yml | 3 + assets/nano_node_list.yml | 5 +- cw_core/lib/node.dart | 19 +--- cw_nano/lib/nano_client.dart | 14 +-- lib/entities/default_settings_migration.dart | 26 +++++- lib/main.dart | 2 +- tool/generate_secrets_config.dart | 91 +++++++------------- tool/utils/secret_key.dart | 3 + 8 files changed, 75 insertions(+), 88 deletions(-) diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 99a45287f..788d02126 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -121,6 +121,8 @@ jobs: touch lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart + touch cw_core/lib/.secrets.g.dart + touch cw_nano/lib/.secrets.g.dart touch cw_tron/lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart @@ -165,6 +167,7 @@ jobs: echo "const CSRFToken = '${{ secrets.CSRF_TOKEN }}';" >> lib/.secrets.g.dart echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart + echo "const nanoNowNodesApiKey = '${{ secrets.NANO_NOW_NODES_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart echo "const tronNowNodesApiKey = '${{ secrets.TRON_NOW_NODES_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart diff --git a/assets/nano_node_list.yml b/assets/nano_node_list.yml index 2e4d1ec3c..be550177e 100644 --- a/assets/nano_node_list.yml +++ b/assets/nano_node_list.yml @@ -1,7 +1,10 @@ +- + uri: nano.nownodes.io + useSSL: true + is_default: true - uri: rpc.nano.to useSSL: true - is_default: true - uri: node.nautilus.io path: /api diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 3bbbf38de..a82479c86 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -6,6 +6,7 @@ import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/io_client.dart' as ioc; + // import 'package:tor/tor.dart'; part 'node.g.dart'; @@ -148,7 +149,6 @@ class Node extends HiveObject with Keyable { return requestMoneroNode(); case WalletType.nano: case WalletType.banano: - return requestNanoNode(); case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: @@ -203,23 +203,6 @@ class Node extends HiveObject with Keyable { } } - Future requestNanoNode() async { - http.Response response = await http.post( - uri, - headers: {'Content-type': 'application/json'}, - body: json.encode( - { - "action": "block_count", - }, - ), - ); - if (response.statusCode == 200) { - return true; - } else { - return false; - } - } - Future requestNodeWithProxy() async { if (!isValidProxyAddress /* && !Tor.instance.enabled*/) { return false; diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index 3b388e5e8..8d8bef13d 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -10,7 +10,7 @@ import 'package:nanodart/nanodart.dart'; import 'package:cw_core/node.dart'; import 'package:nanoutil/nanoutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:cw_nano/.secrets.g.dart' as secrets; +import 'package:cw_nano/.secrets.g.dart' as nano_secrets; class NanoClient { static const Map CAKE_HEADERS = { @@ -54,12 +54,14 @@ class NanoClient { } Map getHeaders() { - if (_node!.uri == "https://rpc.nano.to") { - return CAKE_HEADERS..addAll({ - "key": secrets.nano2ApiKey, - }); + final headers = Map.from(CAKE_HEADERS); + if (_node!.uri.host == "rpc.nano.to") { + headers["key"] = nano_secrets.nano2ApiKey; } - return CAKE_HEADERS; + if (_node!.uri.host == "nano.nownodes.io") { + headers["api-key"] = nano_secrets.nanoNowNodesApiKey; + } + return headers; } Future getBalance(String address) async { diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 71a971a9a..144ca456d 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -34,7 +34,7 @@ const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; -const nanoDefaultNodeUri = 'rpc.nano.to'; +const nanoDefaultNodeUri = 'nano.nownodes.io'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; const tronDefaultNodeUri = 'trx.nownodes.io'; @@ -241,6 +241,9 @@ Future defaultSettingsMigration( case 38: await fixBtcDerivationPaths(walletInfoSource); break; + case 39: + await changeDefaultNanoNode(nodes, sharedPreferences); + break; default: break; } @@ -836,6 +839,25 @@ Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { } } +Future changeDefaultNanoNode( + Box nodeSource, SharedPreferences sharedPreferences) async { + const oldNanoNodeUriPattern = 'rpc.nano.to'; + final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentNanoNode = nodeSource.values.firstWhere((node) => node.key == currentNanoNodeId); + + final newCakeWalletNode = Node( + uri: nanoDefaultNodeUri, + type: WalletType.nano, + useSSL: true, + ); + + await nodeSource.add(newCakeWalletNode); + + if (currentNanoNode.uri.toString().contains(oldNanoNodeUriPattern)) { + await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + } +} + Future changeDefaultBitcoinNode( Box nodeSource, SharedPreferences sharedPreferences) async { const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; @@ -1225,4 +1247,4 @@ Future replaceTronDefaultNode({ // If it's not, we switch user to the new default node: NowNodes await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 8539ac803..014d5f011 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -203,7 +203,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 38, + initialMigrationVersion: 39, ); } diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index cab41ca69..735e8e359 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'utils/secret_key.dart'; import 'utils/utils.dart'; -const configPath = 'tool/.secrets-config.json'; +const baseConfigPath = 'tool/.secrets-config.json'; +const coreConfigPath = 'tool/.core-secrets-config.json'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; const nanoConfigPath = 'tool/.nano-secrets-config.json'; @@ -11,6 +12,23 @@ const tronConfigPath = 'tool/.tron-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); +Future writeConfig( + File configFile, + List newSecrets, { + Map? existingSecrets, +}) async { + final secrets = existingSecrets ?? {}; + newSecrets.forEach((sec) { + if (secrets[sec.name] != null) { + return; + } + secrets[sec.name] = sec.generate(); + }); + String secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + await configFile.writeAsString(secretsJson); + secrets.clear(); +} + Future generateSecretsConfig(List args) async { final extraInfo = args.fold({}, (Map acc, String arg) { final parts = arg.split('='); @@ -19,7 +37,8 @@ Future generateSecretsConfig(List args) async { return acc; }); - final configFile = File(configPath); + final baseConfigFile = File(baseConfigPath); + final coreConfigFile = File(coreConfigPath); final evmChainsConfigFile = File(evmChainsConfigPath); final solanaConfigFile = File(solanaConfigPath); final nanoConfigFile = File(nanoConfigPath); @@ -32,70 +51,22 @@ Future generateSecretsConfig(List args) async { if (key.contains('--')) { return true; } - return false; }); - if (configFile.existsSync()) { + if (baseConfigFile.existsSync()) { if (extraInfo['--force'] == 1) { - await configFile.delete(); + await baseConfigFile.delete(); } else { return; } } - - // base: - SecretKey.base.forEach((sec) { - if (secrets[sec.name] != null) { - return; - } - secrets[sec.name] = sec.generate(); - }); - var secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await configFile.writeAsString(secretsJson); - secrets.clear(); - - // evm chains: - SecretKey.evmChainsSecrets.forEach((sec) { - if (secrets[sec.name] != null) { - return; - } - secrets[sec.name] = sec.generate(); - }); - secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await evmChainsConfigFile.writeAsString(secretsJson); - secrets.clear(); - - // solana: - SecretKey.solanaSecrets.forEach((sec) { - if (secrets[sec.name] != null) { - return; - } - secrets[sec.name] = sec.generate(); - }); - secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await solanaConfigFile.writeAsString(secretsJson); - secrets.clear(); - - // nano: - SecretKey.nanoSecrets.forEach((sec) { - if (secrets[sec.name] != null) { - return; - } - secrets[sec.name] = sec.generate(); - }); - secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await nanoConfigFile.writeAsString(secretsJson); - secrets.clear(); - - SecretKey.tronSecrets.forEach((sec) { - if (secrets[sec.name] != null) { - return; - } - - secrets[sec.name] = sec.generate(); - }); - secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await tronConfigFile.writeAsString(secretsJson); - secrets.clear(); + + await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets); + + await writeConfig(coreConfigFile, SecretKey.coreSecrets); + await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets); + await writeConfig(solanaConfigFile, SecretKey.solanaSecrets); + await writeConfig(nanoConfigFile, SecretKey.nanoSecrets); + await writeConfig(tronConfigFile, SecretKey.tronSecrets); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 7261478a6..ed22d152a 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -45,6 +45,8 @@ class SecretKey { SecretKey('authorization', () => ''), ]; + static final coreSecrets = []; + static final evmChainsSecrets = [ SecretKey('etherScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''), @@ -57,6 +59,7 @@ class SecretKey { static final nanoSecrets = [ SecretKey('nano2ApiKey', () => ''), + SecretKey('nanoNowNodesApiKey', () => ''), ]; static final tronSecrets = [