From 423f6a225c4dc7e4a60ea6cf586ba8bd21c2c4c9 Mon Sep 17 00:00:00 2001 From: Rafael Saes Date: Wed, 16 Aug 2023 19:22:20 -0300 Subject: [PATCH] Merge branch 'main' into CW-396-additional-themes --- .github/workflows/pr_test_build.yml | 11 + .gitignore | 4 + PRIVACY.md | 27 +- README.md | 18 +- android/app/build.gradle | 4 +- android/app/src/main/AndroidManifestBase.xml | 3 +- android/gradle.properties | 2 +- assets/ethereum_server_list.yml | 10 + assets/images/aave_icon.png | Bin 0 -> 13265 bytes assets/images/arb_icon.png | Bin 0 -> 9896 bytes assets/images/bat_icon.png | Bin 0 -> 4335 bytes assets/images/bttbsc_icon.png | Bin 10730 -> 0 bytes assets/images/cake_icon.png | Bin 0 -> 9429 bytes assets/images/china.png | Bin 582 -> 0 bytes assets/images/comp_icon.png | Bin 0 -> 5969 bytes assets/images/cro_icon.png | Bin 0 -> 4274 bytes assets/images/dydx_icon.png | Bin 0 -> 8718 bytes assets/images/ens_icon.png | Bin 0 -> 7087 bytes assets/images/france.png | Bin 1025 -> 0 bytes assets/images/frax_icon.png | Bin 0 -> 5520 bytes assets/images/ftm_icon.png | Bin 0 -> 5846 bytes assets/images/germany.png | Bin 576 -> 0 bytes assets/images/grt_icon.png | Bin 0 -> 5865 bytes assets/images/gtc_icon.png | Bin 0 -> 6569 bytes assets/images/gusd_icon.png | Bin 0 -> 6486 bytes assets/images/holland.png | Bin 596 -> 0 bytes assets/images/home_screen_settings_icon.png | Bin 0 -> 394 bytes assets/images/india.png | Bin 683 -> 0 bytes assets/images/italy.png | Bin 1030 -> 0 bytes assets/images/japan.png | Bin 570 -> 0 bytes assets/images/ldo_icon.png | Bin 0 -> 9033 bytes assets/images/live_support.png | Bin 0 -> 1776 bytes assets/images/more_links.png | Bin 0 -> 1889 bytes assets/images/nexo_icon.png | Bin 0 -> 4600 bytes assets/images/pepe_icon.png | Bin 0 -> 15111 bytes assets/images/poland.png | Bin 538 -> 0 bytes assets/images/portugal.png | Bin 773 -> 0 bytes assets/images/russia.png | Bin 557 -> 0 bytes assets/images/shib_icon.png | Bin 0 -> 7996 bytes assets/images/south_korea.png | Bin 680 -> 0 bytes assets/images/spain.png | Bin 518 -> 0 bytes assets/images/steth_icon.png | Bin 0 -> 5116 bytes assets/images/storj_icon.png | Bin 0 -> 6569 bytes assets/images/tusd_icon.png | Bin 0 -> 5414 bytes assets/images/usa.png | Bin 838 -> 0 bytes assets/images/usdcsol_icon.png | Bin 6923 -> 0 bytes assets/images/wallet_guides.png | Bin 0 -> 1760 bytes assets/images/wbtc_icon.png | Bin 0 -> 8448 bytes assets/images/weth_icon.png | Bin 0 -> 7127 bytes assets/images/zaddr_icon.png | Bin 6158 -> 0 bytes assets/images/zrx_icon.png | Bin 0 -> 4950 bytes assets/text/Monerocom_Release_Notes.txt | 5 +- assets/text/Release_Notes.txt | 6 +- configure_cake_wallet_android.sh | 10 + cw_bitcoin/lib/bitcoin_wallet_service.dart | 29 +- cw_bitcoin/lib/electrum.dart | 88 +- .../lib/electrum_transaction_history.dart | 9 +- cw_bitcoin/lib/electrum_transaction_info.dart | 7 +- cw_bitcoin/lib/electrum_wallet.dart | 24 + cw_bitcoin/lib/litecoin_wallet_service.dart | 29 +- cw_bitcoin/pubspec.lock | 2 +- cw_bitcoin/pubspec.yaml | 2 +- cw_core/lib/cake_hive.dart | 4 + cw_core/lib/crypto_currency.dart | 54 +- cw_core/lib/currency_for_wallet_type.dart | 2 + cw_core/lib/erc20_token.dart | 66 + cw_core/lib/hive_type_ids.dart | 13 + cw_core/lib/node.dart | 55 +- cw_core/lib/set_app_secure_native.dart | 10 +- cw_core/lib/unspent_coins_info.dart | 5 +- cw_core/lib/wallet_base.dart | 2 + cw_core/lib/wallet_info.dart | 8 +- cw_core/lib/wallet_service.dart | 2 + cw_core/lib/wallet_type.dart | 20 +- cw_core/pubspec.lock | 2 +- cw_ethereum/.gitignore | 30 + cw_ethereum/.metadata | 10 + cw_ethereum/CHANGELOG.md | 3 + cw_ethereum/LICENSE | 1 + cw_ethereum/README.md | 39 + cw_ethereum/analysis_options.yaml | 4 + cw_ethereum/lib/cw_ethereum.dart | 7 + cw_ethereum/lib/default_erc20_tokens.dart | 309 +++ cw_ethereum/lib/erc20_balance.dart | 47 + cw_ethereum/lib/ethereum_client.dart | 230 ++ cw_ethereum/lib/ethereum_exceptions.dart | 11 + cw_ethereum/lib/ethereum_formatter.dart | 25 + cw_ethereum/lib/ethereum_mnemonics.dart | 2058 +++++++++++++++++ .../lib/ethereum_transaction_credentials.dart | 17 + .../lib/ethereum_transaction_history.dart | 77 + .../lib/ethereum_transaction_info.dart | 74 + .../lib/ethereum_transaction_model.dart | 47 + .../lib/ethereum_transaction_priority.dart | 52 + cw_ethereum/lib/ethereum_wallet.dart | 486 ++++ .../lib/ethereum_wallet_addresses.dart | 33 + .../ethereum_wallet_creation_credentials.dart | 23 + cw_ethereum/lib/ethereum_wallet_service.dart | 108 + cw_ethereum/lib/file.dart | 39 + .../lib/pending_ethereum_transaction.dart | 42 + cw_ethereum/pubspec.yaml | 68 + cw_ethereum/test/cw_ethereum_test.dart | 12 + cw_haven/lib/api/signatures.dart | 2 +- cw_haven/lib/api/types.dart | 2 +- cw_haven/lib/api/wallet.dart | 17 +- cw_haven/lib/haven_wallet.dart | 29 +- cw_haven/lib/haven_wallet_service.dart | 20 + cw_monero/example/pubspec.lock | 2 +- cw_monero/ios/Classes/monero_api.cpp | 11 +- cw_monero/lib/api/signatures.dart | 2 +- cw_monero/lib/api/types.dart | 2 +- cw_monero/lib/api/wallet.dart | 17 +- cw_monero/lib/monero_transaction_info.dart | 13 +- cw_monero/lib/monero_wallet.dart | 63 +- cw_monero/lib/monero_wallet_service.dart | 20 + cw_monero/pubspec.lock | 2 +- howto-build-android.md | 12 +- ios/Podfile.lock | 22 +- ios/Runner.xcodeproj/project.pbxproj | 5 + ios/Runner/AppDelegate.swift | 10 + ios/Runner/InfoBase.plist | 5 + lib/anonpay/anonpay_invoice_info.dart | 3 +- lib/bitcoin/cw_bitcoin.dart | 2 +- lib/buy/onramper/onramper_buy_provider.dart | 5 +- lib/buy/order.dart | 7 +- lib/core/address_validator.dart | 92 +- lib/core/auth_service.dart | 33 +- lib/core/backup_service.dart | 359 +-- lib/core/fiat_conversion_service.dart | 17 +- lib/core/key_service.dart | 7 + lib/core/seed_validator.dart | 3 + .../socks_proxy_node_address_validator.dart | 10 + lib/core/wallet_loading_service.dart | 91 +- lib/di.dart | 112 +- lib/entities/background_tasks.dart | 164 ++ lib/entities/cake_2fa_preset_options.dart | 35 + lib/entities/contact.dart | 7 +- lib/entities/default_settings_migration.dart | 46 +- lib/entities/get_encryption_key.dart | 17 +- lib/entities/load_current_wallet.dart | 5 +- lib/entities/main_actions.dart | 10 +- lib/entities/node_list.dart | 16 + lib/entities/openalias_record.dart | 2 +- lib/entities/parse_address_from_domain.dart | 9 +- lib/entities/parsed_address.dart | 28 +- lib/entities/preferences_key.dart | 35 +- lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/sort_balance_types.dart | 19 + lib/entities/template.dart | 26 +- lib/entities/transaction_description.dart | 3 +- lib/ethereum/cw_ethereum.dart | 126 + lib/exchange/exchange_template.dart | 3 +- lib/exchange/trade.dart | 5 +- lib/main.dart | 217 +- lib/reactions/fiat_rate_update.dart | 33 +- lib/reactions/on_current_wallet_change.dart | 16 +- lib/router.dart | 60 +- lib/routes.dart | 6 + lib/src/screens/backup/backup_page.dart | 37 +- .../screens/contact/contact_list_page.dart | 223 +- lib/src/screens/dashboard/dashboard_page.dart | 339 +-- .../desktop_wallet_selection_dropdown.dart | 25 +- .../screens/dashboard/edit_token_page.dart | 310 +++ .../screens/dashboard/home_settings_page.dart | 166 ++ .../dashboard/widgets/address_page.dart | 139 +- .../dashboard/widgets/balance_page.dart | 355 +-- .../dashboard/widgets/menu_widget.dart | 40 +- .../present_receive_option_picker.dart | 2 +- lib/src/screens/exchange/exchange_page.dart | 496 ++-- .../exchange/widgets/exchange_card.dart | 21 +- .../monero_accounts/widgets/account_tile.dart | 57 +- .../new_wallet/new_wallet_type_page.dart | 7 +- .../nodes/node_create_or_edit_page.dart | 51 +- lib/src/screens/nodes/widgets/node_form.dart | 58 +- .../screens/nodes/widgets/node_indicator.dart | 4 +- .../screens/nodes/widgets/node_list_row.dart | 32 +- .../screens/receive/anonpay_invoice_page.dart | 5 +- .../screens/receive/anonpay_receive_page.dart | 20 +- .../screens/restore/restore_options_page.dart | 2 - .../restore/restore_wallet_options_page.dart | 85 - .../restore/widgets/restore_button.dart | 24 +- lib/src/screens/root/root.dart | 20 +- lib/src/screens/seed/pre_seed_page.dart | 25 +- lib/src/screens/seed/wallet_seed_page.dart | 6 +- .../widgets/seed_language_picker.dart | 20 +- lib/src/screens/send/send_page.dart | 268 ++- lib/src/screens/send/send_template_page.dart | 369 +-- .../widgets/extract_address_from_parsed.dart | 3 +- .../widgets/prefix_currency_icon_widget.dart | 70 +- lib/src/screens/send/widgets/send_card.dart | 686 +++--- .../send/widgets/send_template_card.dart | 263 +++ .../settings/connection_sync_page.dart | 153 +- .../screens/settings/manage_nodes_page.dart | 84 + .../screens/settings/other_settings_page.dart | 46 +- lib/src/screens/settings/privacy_page.dart | 10 +- .../settings/security_backup_page.dart | 23 +- .../widgets/settings_cell_with_arrow.dart | 7 +- .../widgets/settings_choices_cell.dart | 8 +- .../widgets/settings_switcher_cell.dart | 19 +- .../screens/setup_2fa/modify_2fa_page.dart | 177 +- lib/src/screens/setup_2fa/setup_2fa.dart | 3 +- .../setup_2fa/setup_2fa_enter_code_page.dart | 50 +- .../screens/setup_2fa/setup_2fa_qr_page.dart | 20 +- lib/src/screens/support/support_page.dart | 96 +- .../support/widgets/support_tiles.dart | 70 + .../support_chat/support_chat_page.dart | 38 + .../support_chat/widgets/chatwoot_widget.dart | 62 + .../support_other_links_page.dart | 56 + lib/src/screens/wallet/wallet_edit_page.dart | 177 ++ .../screens/wallet_keys/wallet_keys_page.dart | 19 +- .../screens/wallet_list/wallet_list_page.dart | 250 +- lib/src/widgets/address_text_field.dart | 73 +- lib/src/widgets/checkbox_widget.dart | 59 +- lib/src/widgets/nav_bar.dart | 1 + lib/src/widgets/picker.dart | 114 +- lib/src/widgets/standard_list.dart | 26 +- lib/src/widgets/template_tile.dart | 84 +- lib/store/settings_store.dart | 282 ++- lib/store/templates/send_template_store.dart | 34 +- lib/utils/clipboard_util.dart | 15 + lib/utils/exception_handler.dart | 8 +- lib/utils/responsive_layout_util.dart | 22 +- .../contact_list/contact_list_view_model.dart | 27 +- .../dashboard/balance_view_model.dart | 71 +- .../dashboard/dashboard_view_model.dart | 13 + .../dashboard/home_settings_view_model.dart | 121 + .../dashboard/transaction_list_item.dart | 8 + .../exchange/exchange_trade_view_model.dart | 5 +- .../exchange/exchange_view_model.dart | 238 +- .../node_create_or_edit_view_model.dart | 66 +- .../node_list/node_list_view_model.dart | 3 + .../restore/restore_from_qr_vm.dart | 4 + lib/view_model/send/output.dart | 25 +- .../send/send_template_view_model.dart | 110 +- lib/view_model/send/send_view_model.dart | 143 +- lib/view_model/send/template_view_model.dart | 88 + lib/view_model/set_up_2fa_viewmodel.dart | 262 +++ .../settings/choices_list_item.dart | 14 +- .../settings/privacy_settings_view_model.dart | 19 +- .../security_settings_view_model.dart | 4 + lib/view_model/settings/sync_mode.dart | 15 + lib/view_model/support_view_model.dart | 29 +- .../transaction_details_view_model.dart | 206 +- .../wallet_address_list_view_model.dart | 31 + lib/view_model/wallet_creation_vm.dart | 3 + lib/view_model/wallet_keys_view_model.dart | 8 +- .../wallet_list/wallet_edit_view_model.dart | 55 + .../wallet_list/wallet_list_view_model.dart | 30 +- lib/view_model/wallet_new_vm.dart | 3 + lib/view_model/wallet_restore_view_model.dart | 6 + .../.plugin_symlinks/connectivity_plus_linux | 1 + .../.plugin_symlinks/device_info_plus | 1 + .../ephemeral/.plugin_symlinks/devicelocale | 1 + .../flutter_secure_storage_linux | 1 + .../.plugin_symlinks/path_provider_linux | 1 + .../.plugin_symlinks/platform_device_id_linux | 1 + .../.plugin_symlinks/share_plus_linux | 1 + .../.plugin_symlinks/shared_preferences_linux | 1 + .../.plugin_symlinks/url_launcher_linux | 1 + linux/flutter/generated_plugin_registrant.cc | 27 + linux/flutter/generated_plugin_registrant.h | 15 + linux/flutter/generated_plugins.cmake | 27 + macos/Podfile.lock | 4 +- model_generator.sh | 1 + pubspec_base.yaml | 7 +- res/values/strings_ar.arb | 49 +- res/values/strings_bg.arb | 49 +- res/values/strings_cs.arb | 49 +- res/values/strings_de.arb | 53 +- res/values/strings_en.arb | 51 +- res/values/strings_es.arb | 51 +- res/values/strings_fr.arb | 53 +- res/values/strings_ha.arb | 49 +- res/values/strings_hi.arb | 51 +- res/values/strings_hr.arb | 51 +- res/values/strings_id.arb | 49 +- res/values/strings_it.arb | 51 +- res/values/strings_ja.arb | 51 +- res/values/strings_ko.arb | 51 +- res/values/strings_my.arb | 49 +- res/values/strings_nl.arb | 51 +- res/values/strings_pl.arb | 51 +- res/values/strings_pt.arb | 51 +- res/values/strings_ru.arb | 51 +- res/values/strings_th.arb | 49 +- res/values/strings_tr.arb | 50 +- res/values/strings_uk.arb | 51 +- res/values/strings_ur.arb | 49 +- res/values/strings_yo.arb | 50 +- res/values/strings_zh.arb | 40 +- screenshot-android.jpg | Bin 36830 -> 0 bytes screenshot-ios.jpg | Bin 297466 -> 0 bytes scripts/android/app_env.sh | 8 +- scripts/android/inject_app_details.sh | 4 +- scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/ios/app_env.sh | 8 +- scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 4 +- tool/append_translation.dart | 66 + tool/configure.dart | 112 +- tool/generate_secrets_config.dart | 20 +- tool/import_secrets_config.dart | 22 +- tool/update_secrets.dart | 3 +- tool/utils/secret_key.dart | 5 + 304 files changed, 12544 insertions(+), 3248 deletions(-) create mode 100644 assets/ethereum_server_list.yml create mode 100644 assets/images/aave_icon.png create mode 100644 assets/images/arb_icon.png create mode 100644 assets/images/bat_icon.png delete mode 100644 assets/images/bttbsc_icon.png create mode 100644 assets/images/cake_icon.png delete mode 100644 assets/images/china.png create mode 100644 assets/images/comp_icon.png create mode 100644 assets/images/cro_icon.png create mode 100644 assets/images/dydx_icon.png create mode 100644 assets/images/ens_icon.png delete mode 100644 assets/images/france.png create mode 100644 assets/images/frax_icon.png create mode 100644 assets/images/ftm_icon.png delete mode 100644 assets/images/germany.png create mode 100644 assets/images/grt_icon.png create mode 100644 assets/images/gtc_icon.png create mode 100644 assets/images/gusd_icon.png delete mode 100644 assets/images/holland.png create mode 100644 assets/images/home_screen_settings_icon.png delete mode 100644 assets/images/india.png delete mode 100644 assets/images/italy.png delete mode 100644 assets/images/japan.png create mode 100644 assets/images/ldo_icon.png create mode 100644 assets/images/live_support.png create mode 100644 assets/images/more_links.png create mode 100644 assets/images/nexo_icon.png create mode 100644 assets/images/pepe_icon.png delete mode 100644 assets/images/poland.png delete mode 100644 assets/images/portugal.png delete mode 100644 assets/images/russia.png create mode 100644 assets/images/shib_icon.png delete mode 100644 assets/images/south_korea.png delete mode 100644 assets/images/spain.png create mode 100644 assets/images/steth_icon.png create mode 100644 assets/images/storj_icon.png create mode 100644 assets/images/tusd_icon.png delete mode 100644 assets/images/usa.png delete mode 100644 assets/images/usdcsol_icon.png create mode 100644 assets/images/wallet_guides.png create mode 100644 assets/images/wbtc_icon.png create mode 100644 assets/images/weth_icon.png delete mode 100644 assets/images/zaddr_icon.png create mode 100644 assets/images/zrx_icon.png create mode 100644 configure_cake_wallet_android.sh create mode 100644 cw_core/lib/cake_hive.dart create mode 100644 cw_core/lib/erc20_token.dart create mode 100644 cw_core/lib/hive_type_ids.dart create mode 100644 cw_ethereum/.gitignore create mode 100644 cw_ethereum/.metadata create mode 100644 cw_ethereum/CHANGELOG.md create mode 100644 cw_ethereum/LICENSE create mode 100644 cw_ethereum/README.md create mode 100644 cw_ethereum/analysis_options.yaml create mode 100644 cw_ethereum/lib/cw_ethereum.dart create mode 100644 cw_ethereum/lib/default_erc20_tokens.dart create mode 100644 cw_ethereum/lib/erc20_balance.dart create mode 100644 cw_ethereum/lib/ethereum_client.dart create mode 100644 cw_ethereum/lib/ethereum_exceptions.dart create mode 100644 cw_ethereum/lib/ethereum_formatter.dart create mode 100644 cw_ethereum/lib/ethereum_mnemonics.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_history.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_info.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_model.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_priority.dart create mode 100644 cw_ethereum/lib/ethereum_wallet.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_addresses.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_creation_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_service.dart create mode 100644 cw_ethereum/lib/file.dart create mode 100644 cw_ethereum/lib/pending_ethereum_transaction.dart create mode 100644 cw_ethereum/pubspec.yaml create mode 100644 cw_ethereum/test/cw_ethereum_test.dart create mode 100644 lib/core/socks_proxy_node_address_validator.dart create mode 100644 lib/entities/background_tasks.dart create mode 100644 lib/entities/cake_2fa_preset_options.dart create mode 100644 lib/entities/sort_balance_types.dart create mode 100644 lib/ethereum/cw_ethereum.dart create mode 100644 lib/src/screens/dashboard/edit_token_page.dart create mode 100644 lib/src/screens/dashboard/home_settings_page.dart delete mode 100644 lib/src/screens/restore/restore_wallet_options_page.dart create mode 100644 lib/src/screens/send/widgets/send_template_card.dart create mode 100644 lib/src/screens/settings/manage_nodes_page.dart create mode 100644 lib/src/screens/support/widgets/support_tiles.dart create mode 100644 lib/src/screens/support_chat/support_chat_page.dart create mode 100644 lib/src/screens/support_chat/widgets/chatwoot_widget.dart create mode 100644 lib/src/screens/support_other_links/support_other_links_page.dart create mode 100644 lib/src/screens/wallet/wallet_edit_page.dart create mode 100644 lib/utils/clipboard_util.dart create mode 100644 lib/view_model/dashboard/home_settings_view_model.dart create mode 100644 lib/view_model/send/template_view_model.dart create mode 100644 lib/view_model/settings/sync_mode.dart create mode 100644 lib/view_model/wallet_list/wallet_edit_view_model.dart create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/connectivity_plus_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/device_info_plus create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/devicelocale create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/flutter_secure_storage_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/path_provider_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/platform_device_id_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/share_plus_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/shared_preferences_linux create mode 120000 linux/flutter/ephemeral/.plugin_symlinks/url_launcher_linux create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake delete mode 100644 screenshot-android.jpg delete mode 100644 screenshot-ios.jpg create mode 100644 tool/append_translation.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 4ca762c12..47378eef5 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -13,6 +13,13 @@ jobs: KEY_PASS: test@cake_wallet steps: + - 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" + - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: @@ -85,12 +92,14 @@ jobs: cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs - name: Add secrets run: | cd /opt/android/cake_wallet touch lib/.secrets.g.dart + touch cw_ethereum/lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart @@ -117,6 +126,8 @@ jobs: echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart + echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties diff --git a/.gitignore b/.gitignore index 9fb7fd204..6fd8f33d6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml @@ -89,7 +90,9 @@ android/key.properties **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.ethereum-secrets-config.json **/lib/.secrets.g.dart +**/cw_ethereum/lib/.secrets.g.dart vendor/ @@ -120,6 +123,7 @@ cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart +lib/ethereum/ethereum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/PRIVACY.md b/PRIVACY.md index d740dcba8..88f180c5e 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ Privacy Policy -Last modified: July 21, 2022 +Last modified: August 9, 2023 Introduction ============ @@ -13,7 +13,7 @@ Introduction - On this App. - In email, text, and other electronic messages between you and this App. It does not apply to information collected by: - - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries)]; or + - Us offline or through any other means, including on any other App operated by Company or any third party (including our affiliates and subsidiaries); or - Any third party (including our affiliates and subsidiaries), including through any application or content (including advertising) that may link to or be accessible from or on the App. Please read this policy carefully to understand our policies and practices regarding your information and how we will treat it. If you do not agree with our policies and practices, you have the choice to not use the App. By accessing or using this App, you agree to this privacy policy. This policy may change from time to time. Your continued use of this App after we make changes is deemed to be acceptance of those changes, so please check the policy periodically for updates. @@ -25,7 +25,7 @@ Definitions - "Node" means a server on a supported cryptocurrency network which transmits data to your App for processing and synchronization, and to which your Device transmits transactions which you would like to submit to the supported cryptocurrency networks. This includes full nodes, Electrum servers, and lightning network nodes. - "Cake Labs Nodes" refers to the set of cryptocurrency nodes operated and maintained by Cake Labs LLC. - "Service" refers to the App. - - "Third-party Service" refers to any service integrated into the App. This includes but is not limited to ChangeNOW, Wyre, MoonPay, and BlockBuy. + - "Third-party Service" refers to any service integrated into the App. This includes but is not limited to ChangeNOW, Onramper, and MoonPay. - "Usage Data" refers to data collected automatically about your usage of an App. - "You" means the individual, group, corporation, or any other entity accessing or using the Service. @@ -40,26 +40,29 @@ Information We Never Receive Nor Collect Information We May Receive But Do Not Retain -------------------------------------------- - We receive but do NOT store information from and about users of our App, including: + We may receive but do NOT store information from and about users of our App, including: - The device IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to supported cryptocurrency networks. We receive this information: - - Automatically as you use the App. + - Automatically as you use the App, unless you turn certain features off in your App privacy settings. - This data is provided by connecting to the Nodes and price API maintained by Cake Labs. You have the right to choose not to provide synchronization data to Cake Labs by choosing a different Node. We provide a list of Nodes in the app that include our own and third party Nodes, or you can use your own Node (which we recommend). + This data is provided by connecting to the Nodes and price API maintained by Cake Labs. You have the right to choose not to provide synchronization data to Cake Labs by choosing a different Node. We provide a list of Nodes in the app that include our own and third party Nodes, or you can use your own Node (which we recommend). You have the right to choose not to connect to our Fiat API service by disabling this Fiat API in App privacy settings. - Personal Data sent through the Cake Labs Nodes is limited to your device's IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to the supported cryptocurrency networks. Personal Data received by Cake Labs in this manner is not stored for any length of time, and thus Cake Labs is both unwilling to and incapable of sharing this data, or using it for any purpose beyond ensuring your appropriate connection to our Nodes. + Personal Data that may be sent through the Cake Labs Nodes is limited to your device's IP address, the block height to which your wallet is synchronized, and any transactions or channels which you use our Node to submit to the supported cryptocurrency networks. Personal Data received by Cake Labs in this manner is not stored for any length of time, and thus Cake Labs is incapable of sharing this data and will not use it for any purpose beyond ensuring your appropriate connection to our Nodes. If you decide to use a Node offered by any third party, some of which we include in our Apps, said third party will receive this Personal Data instead of Cake Labs. We take no responsibility for the actions of any third-party Node offered within the Application. We recommend connecting to your own Node to limit third party sharing of your Personal Information. + If you use our Fiat API service, you will share your IP address and the cryptocurrency and fiat currency exchange pair for which your wallet requests a spot price quote. You can disable this Fiat API in App privacy settings. + Information We May Collect About You and How We Collect It ---------------------------------------------------------- We collect several types of information from and about users of our App, including information: - - By which you may be personally identified, such as name, e-mail address, or and a/any other identifier by which you may be contacted online or offline ("personal information" or "Personal Data”), ONLY when you provide it to us; + - By which you may be personally identified, such as name, e-mail address, or and a/any other identifier by which you may be contacted online or offline ("personal information" or "Personal Data”); + - Device data and error log data; We collect this information: - - Directly from you when you provide it to us. + - Directly from you ONLY when you provide it to us. - Personal information is received by Cake Labs ONLY in the event that you choose to provide it to us by voluntarily contacting Cake Labs regarding support, questions or suggestions. + Personal information is received by Cake Labs ONLY in the event that you choose to provide it to us by voluntarily contacting Cake Labs regarding support, questions or suggestions. You may optionally send us Error reports to help us improve the App. These Error reports contain error logs and basic device data. You can review and make modifications to these Error reports before sending them to us, or you may choose not to send them to us at all. How We Use Your Information --------------------------- @@ -112,7 +115,9 @@ Data Security Links to Other Websites ----------------------- - The App may contain links to other websites that are not operated by us. If you click on a third-party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. + The App may contain links to other websites that are not operated by us. If you click on a Third-Party Service link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. + + The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. Changes to Our Privacy Policy ----------------------------- diff --git a/README.md b/README.md index 317ad91b7..7b739f980 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Cake Wallet for Android and iOS +# Cake Wallet for Mobile and Desktop ## Open Source Multi-Currency Wallet @@ -7,6 +7,7 @@ * Website: https://cakewallet.com * App Store (iOS / MacOS): https://cakewallet.com/ios * Google Play: https://cakewallet.com/gp +* F-Droid: https://fdroid.cakelabs.com * APK: https://github.com/cake-tech/cake_wallet/releases * Linux: https://github.com/cake-tech/cake_wallet/releases @@ -17,9 +18,8 @@ * Completely noncustodial. *Your keys, your coins.* * Built-in exchange for dozens of pairs * Easily pay cryptocurrency invoices with fixed rate exchanges -* Buy cryptocurrency (BTC/LTC/XMR) with credit/debit/bank +* Buy cryptocurrency (BTC/LTC/XMR/ETH) with credit/debit/bank * Sell cryptocurrency by bank transfer -* Purchase gift cards at a discount using only an email with [Cake Pay](https://cakepay.com), available in-app * Scan QR codes for easy cryptocurrency transfers * Create several wallets * Select your own custom nodes/servers @@ -32,6 +32,7 @@ * Convenient exchange and sending templates for recurring payments * Create donation links and invoices in the receive screen * Robust privacy settings (eg: Tor-only connections) +* Robust security settings (eg: Cake 2FA) ### Monero Specific Features @@ -40,13 +41,19 @@ * Specify restore height for faster syncing * Specify multiple recipients for batch sending * Optionally set Monero nodes as trusted for faster syncing +* Specify a proxy for Monero nodes, compatible with Tor and i2p ### Bitcoin Specific Features * Bitcoin coin control (specify specific outputs to spend) * Automatically generate new addresses * Specify multiple recipients for batch sending -* Sell BTC for USD + +### Ethereum Specific Features + +* Store ETH and all ERc-20 tokens +* Add custom tokens by contract address +* Enable or disable Etherscan for transaction history ### Litecoin Specific Features @@ -69,6 +76,7 @@ * Website: https://monero.com * App Store (iOS): https://apps.apple.com/app/id1601990386 * Google Play: https://play.google.com/store/apps/details?id=com.monero.app +* F-Droid: https://fdroid.cakelabs.com * APK: https://github.com/cake-tech/cake_wallet/releases # Support @@ -123,7 +131,7 @@ Edit the applicable `strings_XX.arb` file in `res/values/` and open a pull reque 2. Edit the strings in this file, replacing XXX below with the translation for each string. -`"welcome" : "Welcome to",` -> `"welcome" : "XXX",` +`"welcome": "Welcome to",` -> `"welcome": "XXX",` 3. For strings where there is a variable, denoted by a $ symbol and braces, such as ${status}, the string in braces should not be translated. For example, when editing line 106: diff --git a/android/app/build.gradle b/android/app/build.gradle index 00cef6393..946c53697 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,8 +45,8 @@ android { defaultConfig { applicationId appProperties['id'] - minSdkVersion 21 - targetSdkVersion 31 + minSdkVersion 24 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 64adea1e7..b40aeb7c8 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,6 +14,8 @@ android:icon="@mipmap/ic_launcher" android:allowBackup="false" android:fullBackupContent="false" + android:versionCode="__versionCode__" + android:versionName="__versionName__" android:requestLegacyExternalStorage="true"> %Q(kw%H%sF0c3V@Ho9=p6Khao#Q`8M@Pr@@PYPI+H2^^ zgPD=`+y=7kp}o+1J=M8OSK2Q~puIWmsHvw(M+b+o9NM0ty+7;zz|@P5j)m*wK~MK3 zgO85x)~kown$P^L)*xpaET2QxRd!#ATw%~+Y_-dRXwc-N5z3>YCz^jmB1#yT3x)483!k!;fB!yA`DLAAEq3>a>O9`2 zy!$HL=E!r*3JbMzUh~;Ogdt%t3Xle8F!zX8A& zcSAlpglmFvX>6CBwNzs*8(V?af);I>*U~qUF!Ypl}0C z0T>sE=SZR|q_er7?FL;o+tQe3$i?O7Gwffd*{G_2ura;^Kd}C!>z#)+Gp3F7O?$^~ zxYIj*XVlM$ukmGk?QlAn>=J5?sXkkQ$^m%{*H1%U`hh9ZZ6os>FSKe|kJ#Rqg&j|X zxWtcT?WSn{QWyBPub%Ec%5>~t>^;yHqr@pbo0rzCITNgHf75r!yY)|AlF4uME6+t? zA!~}GzSjs~+rOiZZB6Hi^^l@GxpM5V#~i=}9StR+2_B08YL5i#_FKNyA4-zqHZ6{u zr#ZHhE*kvQugbhSmQ;T^L*l3sHrX+4H5a*`s<5NAE3$rb!|UK(>+>=dY6-()BPrY- z`+5`2o{8k;A<1VX3=abTicH=;IJ3iEM?G9)D?ZJp>YLhNPe!{^98H+!mpR*qvS8(4<7^+3#EOlpc zJ)Cgq3~DESHYvNoNsVWLwTj&?Ow7<&`q|sb@SJ@S0Y6nGLy99n@PZ>YKDAL-Ey0J{ z`x-nd72xbjg0j{C$!xBmmG(Fc{QOVH(}F#>!eP{|wDPPlGxpu_Xt9vA!@mMaZW?1Vt3dSi(AUuYpLO@IGvH{y;Bp@Vnyp%P?AX$Q`xoi9MpWJ6wOczp&?uAop02fX-Z9)-`wY4JkYR=r_lU z_Q%^MGr?7UxHrw#kJoM#?j*JXpo{gSCo|@0f>zjFleD`E{g|~g1Dh$%FX6J{iJi^u zXvH}P;spcyX$`M0>DwaMk0v%}CnU#D`#qSTAGbB22kfmPg(7q(lG9Kkl5bM)=o@+c zbzy-du*jrh-xNx9WV2 zagwuV#K6$i_iP(F*#~1uH$L~sAuf5~6qKF6xO{U2h-Yn&Bs~q#PsSL;y7)d&*tys6 zMD?MZ6@17MN|G~uk2%!M7F?0p6ycFTz1aj`ENIeM<9PMeGEvX{W6UJ)B!r=XO5#PH zpMIM*TpDoK!xJQzZIR(L@HR$aN4`!M@nq1kNRhK{p@foZ5gk~=w-V@jgV4y0I6aXE z11-A2pVNK?El0n0*X2~{ns0~iG#ycugd4N{ga4KQmS#22DD;Xj5bPzt;w^-2;>dE) zg_Ne~RG2{f55M}lF3A)bLV`~2_;1S>#Y#RS6CMoRvXE-uF*XTvm8(=fB42_pW(RY6 z(v_iM7bpulJ9i|;h{+Z^->7%~?k6-VCa$Mt)ny3pzdYA^pXzDLHWDa@d@>m1RXTOA z^rDoyFrX(HGPMp`FzG`bzFlJf5?t8MZcBO9Ae^;*J|uE2eUe15sdow8&Dd6-+VJV` zu+ee$sE!?H^O8JuFM%GNQOL%}7 zSiR;o!{HdR;pI&+icEZ}-%rrD<<^PbsZNl7zk_SIlN5PXz3LC%B#cEALf)LH>Jjf* zwpY{B&Z*g#dJ$MF4oEsf=tGjb;LjnSM3+}`&E)@xcdvG!BmShc+@|s`WuZh&$ev8)~8|Ca7z9S3*RxMxcX@X!7HT4 zeA^&Io|79=nrSClu$Q6x6-gcxKQPCvx&#|{l&(^C8=rVEN4L12yo+E0iTnAGGd3fao_O~i8h1>ydZY}6=@ z>%RE>-NOp>74&#ea9END_xLjQ@-w3Z4!46MUD&-DcG?Q2ykh%Sd&}9)xwH0(vgdHU z`hMtDu;(Ieq+~#DL(6I{ei$Hm-4XEy3WVvDI>8HNsP2DL!G|i5-nY;`sAfe{v2le10Hl!MG<0{zOzL1G@X31R~gXoq;ps&1niexT;>_q z7szBSl$9XWa*<0XQ3gYo_8ULgHxRKE)7kQu=ck29y2XxSqd~@d%n^FtzM|uoG;)>8 zl_z^!E_HQi%wE$;GA>WG^u2 z8WcAE|C|hP`(E&@^~`BCZi~fW6N%ji8&OcNegAlz%0=@hmonJf8J!|-3v7dw zb_wbT*OjlwRA5)OaI#A+O+MZpp%i;Pi2amoUb!o2e$ZPE`QLUAH zQXmDTg)-{wt9trgTm?ey2!cBD-#8Q^nuNadx_xuLTu5n+D2ruJRJ2LetFVk>2=dtB zyPLo@^xQ>uSVI3F2Ed6q(*Ec`8;Q(2$vr?sfYH7&xHr+CkQIbMa#2XX5ZW zKk>{SyrPtEM~H4f25?`X#I`VeYm)R7C&qHJzB2+j@PpP<4!Y|nTj@_>Dk4gOTtT1i zzQwEI*IzE4?LQk66?tYL^D^VcvlcaNNU`Ay%aI=Y+W?MMdC}?j<>sko&u;SdA&8Kb_ zaF^x{_=_M8SaeL1-&vq~;_iLXwz%@Mod$xu3Zf~9(~HI!3!tY!6088H)hN1*K9^-D z8T2^d>Dv@Uz6oKaT90v0Ve^}v9AfvMcBkBd~C}Y}@Q3KgqV*5Gf z_Nar1o<&ArB}*(Jl3oo7sBj|Q@EDQ>^9d#SWpgY7YG8I=u05%pGUy;YXTDpaB^>vc z+degmX>L%%t5#JJ(qGmCvEdsy|zb*H>B(Kb!Q?6`mssVA)c5P2nh zmzY)isqHWw^wv|iW63P8Hg-db4&V~nCAaq`w;cHa)&wSimwf6 zQKSs7pOK}c1awZCO#6AfFvW`2=+I^VU8Zh@$OGb%*k?d9YEf>vKiNtIW}aJ9cMX|U zEHB08{)lb}jm0o6Q*6aCe0BTg#UGa3Kw>(dwtEYrnZ6%Ki~iD}%n4{tyDtrT9i9u- zmIFFh_v&#dqRFHD#&Q#)sb`t+`2(AOd&C-R!&&l#?k-k!5!iu{ZR7hN{tm94+qiog zz|Y0`qTB(u*To`F)np;)Q|ui^W!e}5#k(laf!Ju2C| zZAHR*OrA^qlIHij_lCoAX*ajC4s8rNu$j*N{#T{U1oftklXYt5sFUL>|FPa8hUm?6 z-j`zSr2o9ijSjSnx4CvSupt zW4TL_JK)qWe5zn?r6qR^EAHC2t-z4ctG{}m`|iJ_uG%-;1f$h>WICSe%PFV+$@A?` zKeB{guUFw0m`SGYMZ_?JG#{$fS(xku&VQ^gw(Dnmt6|PL&|uya6YrOhpb&Z!iw9`f zK2~D&qfVCxZgb6If<`0jiDs9DtPXOW)0O56(YC9y_V356E_A{d<-(MJ+lGlZAacGt ztholCoJsmCk2A?_BmMiywad`fjFD(X*ZYl`y-Q9x^u} zwvz#p>;>5b^6{0YYYG_28Si-K>TSU9rw^FFZ;DQaO->gy>3=%2`_TCc*fZ84ZolJ) zkLbW_akYWS_(h{T;u$79;>PE@m7c{O*qk9W`p+iWH(vW!(5cljorgwhtKBkS&v+u) zr`0gdv1jz1UOHpj(+qNx;pOUda06D~omaZG&*n~lnzhfcQtSk7eP)Ub@>TSyk2RJ4 zKx=%j1sU46*rRi4RSlzXEj;;JL7zy)OHnuvZ%SQ=@RF07>B@mkj`vs%21$QGd&)jA(D=k&TG!GGO) z_iw1(Y2P|CwV3plJjH3ZUp$X7uEA;bHu>w^O1@$CHl&k|n2Ea4IMBBcy#&~fb@*!c z9phDRV1}Pha%QnoU5KxqW;#aw9x$Aw%WMl`%jh`mk)%iLi{nc6_juEnW#?ot)vHY= zUShf8m86rf&OTsS1q{GB&C~AVgOyrTCky4~xvA3& za)_H^?jnSoK4+F8Uiel^n6CnXM58wUk!2c<;2{|eo{dA_%Vl$)LTG$B7j+~RA!-&I zBqBg}J{jlGKwh+_%#Z&BEyhYPfkEKMatH;$QROa1y;pPPoi!G5Q!TQ94zp*f4`aeJ zn(Y6Xvqfi4yD{#5zID5I%FnN!H&oC$$=Ru2FB|XL)^C3sQtApvW65aj`;%pOhs;!h z8oqyVdk4Ansm$QEE1cdYjV($&`6^E(9b12FT`6BNcOk`COh;SYJl?1AKc*nz`5AM_ zE!Bt)c$76ZH)wEp3b;$Q8?cY5$O1D$1F|ofg$~)y8deVDPcN^u!}A)3CQWi+@vD-$ z#LfAeJ(8#!(D|FQGYvv+0Y%3nCU-zesLRKxD(@QTRYLsI!qpM9Qa$95^k@-X4ovd+ zYEYHT_5W_(MuTSx8A?;1*WihpQbzKS(>hCK|fj?(E3S6 zY2#00>-;pznajk*HPa%I#-~AI1;4iavx{7Yf`b2bLM^DH!skaqO4AD~3wjljUFZ|W z_EY^O%r1X*1nl#ea)EXWbN>@KBFXpEg04^J73+XP*~<66ezrYSg81ffBQ0Xnk!FjExGP>*3r}h9=S(>S4HTE7v`OT*Uz z6L-+Y(-O2PO>4R? zjn25?@CdJAVa*ohw?)_L}U<6x~QeA7Y|!#y;@+(z{DR zdS?&THEtv2PG>H_5od>Zy&Q;v-%DX6p2D({q|wj59<}1&3gHvjJ{<};^^1HzMxi;b z=_m=Lq?cEzJi=tfn&>21DnkCyWM&bOlkmRjHxZ;<^V3llQ@5#{3iF-w9Y)y2;FR5N}Ra)DUvMLZ{J1 zD2~VeY*W=_WM{1%DqiSqCuYLwcW_mIVE=qM9dO{+>=m^wK(lDzP&?fAa8J%wP{>8$0_O)-6j){ zls)8kYfWlZ@p_}j!(wlZg%Hqj$AOq{FcwYhg$;n>t7mDE0+rwyul--!={S|;fP=7P z*gK;2{)+nm#JSDM<-1zGT9aO#o6`CMOKXqy=u!P?jpJJLHkfSvJ%I|4;&}zn>bqjy zh%X@2WZ=ckw;G1Kih$a<`o_+)B!C(HQATh2*Bh~L&C7sV> z+6tHScnWtdQc_P4?b3kGQN!o@x3I>YCHKEmmux#fowc2q-?W|hWY(sOv=YzeOGs*~ zeJ8&5CgJcaVz@2ncq8pby4&*C7e0?iNz0zYHxwgS*s+SMH*MpQz!@+5;b3oxt%<2# z#ytx!S6G21h&I~9ShyiA)Az5Y`*X4gkC`m(eegh@twOGLv4@*o`2N6*XHZLWB6onn zF=-G%fQW$KG+sqKezM+-pU=U+wG0f-a5irNMhWfW7+louttq{jW-SdAnF>KYx>aMd ztM12kKxz{qWVQ&F3H?{YG)PKQ0Sh9oP{h-#gt;NrHJ&tnP|CJpfoG$$*SL7}Ba}Vz zOz{JsnJ;|*^oA;@i;Bt}kBGisg$D96cmu zVh1{?B6=Q}<T=XsPF8e&jAZaw$&~VlsQYlwRr=4u=IoEc zGOkhe>W9d?>+e8&(kgZ-R;Hge?|zWNJFtfXPBa1K(fMBbPF2|P2Z1l!B92z=ZtsfI z1?riaA-oqEJoBS6a?4H^?c%D^o6H_Z&ja0)Faqi`f*OrHfEcC3IB^eozRj zL!a@LHO2|7tZuH0iUxJR#VBiVgk=&X6ZiX*>)Sk8PB*Ydeq@XN9kbkXE;W->{ai79 z^`7joBQ&vaePE&?S6@(7tik`d_Xh94NKSBgx<^-*s{Nz1iMgIEr0F)_#2kf1nDPEi zV2%0~rZU)ducNQ0?HwBfp)B%KHTu@oL?8Fz#f`;b8Znej`=RE#xBeE%I%es`Yjwd+ zE6;)_Tlu_?3n~7DQo5YM7fEo?IOpbnI7#qs|~Mx+2v)YA)I& zbcQ>s2h}D>q(H`65JE7q43{QAbI&vKrAaIRM0#i6iV5;YK0G``Xt32zk)H6i4V=m6 z4`^t2viMc4nd>|%3t`%Px-rA>Dzdhbu%GYLaOX4>3EC-O$2%_Y+o>MsJL3R0%EXq^EK({B-ZpPJH?{q)zwayN4$lGEd1` zK!U-Uz%jQ^w>xDqZe{~X(6LyZ)|*ux_BtzEcY^_UHkTxIE@xw(r#k((tlWa+@x-ea zZ9l|;#~rcLZJ^4XKYh*_0w`T4nNMS+Aa!)!|IwzqdORGJQFEaHT6>fnIbr|I1YU3r zbv6j(bUXJVIVLh?=C^C;J&&DFd5Tw|9<{?Wn+Y*q?o~1A8FsH*MJeNFrjct}7G5iE zt&AbXMR_&CV9#Dv3)c|wP-_vyHdU+vCZNwo5G*OsZqCEQB zp{&Fx!@S#kFX-^+@G+r;;u5bAY9DY(kl|AvzlHmkfE!jY-frAbklNbTkI}dY?sMwc z%Ne(Qo+`b}R_`4RM`cg&m0H)_jBo8x{HE(Vk{nVuv1nkaQ7WPfZh>gQ(pwljVC9oS z^@~LIi<=`2PBPW6MIT$kZU+&v>v}!A<$8%zbaxx87fO2t=(>ZJgbPGW`Xc)7TN-9G z@}^n_*2WpJuB3azqtQC-$Ydx7;0s6C3 z5l<+%HW%Kk1y|``VK9k4g&<@q%QHXsCx5?jo0>N3oc+ ztJDBeb^1T^_@cy?-l+-&D@`G`IH$`^r)kVg2I;Ary>buG0-3Pe{;WC0TIyeTaKJU3 zuQya5k<1=<<4TAp=FM7F|KEp18jw*{RA22+9^*Vp%BIbJb$e9;R79qf&i@>f{uu+9@$&t~-nX$$ zg?g$e{q>5@bJIq3Om$5t)rfXZo&L`5MFT*Ge~%RJ#vAn#?paVmb-aQwOww~ePlEBL zQz9zN*CS(2|3S9zLxGuJSm^9^zLY!>@0!r|w<%#dVEA?zZv?|fq<;!`QQDhBv8gA0 z=^$mn8#xEq{Q&RjckV~iQfpkud(uG**Bpz+5<8aw&T%;{8t8GYMebK!B|U8vHX5zK z_oegX8)Sb1N!!7Dlkl$!$9gh+Vsc^j1y(f~J!Iw7)wf7OUm_Si*|d}Y+*@?Zh%Pi# zKsq$*j?B2yws3z*G%pt_>^^?m{~KR1K|PBmk3d1GO)hCV)M~%kMPkf$ptTiTDgQ95 zXX`1}%ABU=yOQX?B?xygnpE#}Z4{~gqQ1Tq68?H{_8{L{{$6gz2i}0b(Z(8rYw;k8 zalV4wG4+y$q~M3hPeX=ix^nBw5%zLd7$X)>3HJgPDwKkNvh`%}e{i_AL5!<`lnr_v z?7zPHI^T&NJ5K2;1N`YSfLp1K^;EwFSHDg*@F|we66>BBlOY^sHH$RxkNe6PugG?R#=wQ&%pW57~J|olNv|??#a-)?AmU>??yu?1ZTKC68)y+o zTcGoE6y`V&0lR>GjaNv5?HGJogGgQBuGA+~R z!uS*c)f3V0`grIW4U(Kr@o_&N|m_K5HwH5%=k8NwKci6*AG?p~Woo{%j9e{!xKRxuJk z0rTrq3rF-Xl1wB}4HXy>CVr%a%JfaQe-F@MHg;2!c)>*@z0X#aN zi3m!oHs3{(NE=B4t-y85JPo21ES2?#g~>?CNYN;x89Bj3K5JY6x=jaW+}A-&$~o=` z*%BN5hVkcR+!|{V^bmOiG+|FM+KASUNk-DBRcVDx#@8%Dbf1THO?ct2Y8>m-rD z)n|wQ@$3E^Bq`_{^^*D{d4=W#6msUQlGP)t)A*P297D!&MvF8FQwoRHqCrN#dRjB? z%`Q-{9jJ@P&(;T8y01jAM>!M$#;~U<zt$)Dd~szO99cn2fy?zu6yC<0E2hNpP;7wx+q)Mh&OJmfSW|oA5+^=iuQy zpSPUeR@eXJDIu2Iab2iYTAJgCX70+29_6s9XMYtV-eeT8Q8Dp5S+z_Sc43+Tb+EAGORpDskut8YD5&V2x*pu3VZRFr@?optGcAPXHdLz zSo;t1niX6uTL+#v$`)D>R>TpUbnQDM6aK|l8epVlanww-W(QulFFps4krIz3EGPxU zi6c?#qHGI5hzIq}^x)j?hFfkojL=2HAhQumD6(?n) z%MdyH}0BNoj??E=}~ zQ)Ms1sL?a{RmHCg2S{wmk`x?&G)Psw4LMbqSSoO5DH5sqJ>hZtDeW&x2wvUyO%nZC zkA7(*GU~``<%-jLgUQI36U|6$Ogi#gFp2lWii2!HY*S;6_fY-x-->h)Wga01WdA@b zvgq^{j4^$PaPlTkPXog0cG=1~NU=;kdE10zf|;-|;I(^gtmZpSZ}Xgz)BP}WxRq%N__pn;5xL#t7FdKt#fMG*_{0*jhAt|Q`#STwuYFPCTKol zUDv-N_lqE1%vL%tf2!f!l>1}^ML=u1S>qWu(fp$Gw|k8i<}>5DFwpk6bP;S*-f-Q! zpW1Al8CV%tvIIGEBO@j^y&K0*r$f`b5p}DA4PL{%5xnQ13LAz;nHwagu}BB@TB_~4 zGHkiDT1^;sdv~1A`tM>|jT6ItQyyVTM9Sug6C3Tw{u!BIaj^7jqfyE&q9$gNXvV$% z%Z*LpQL42WJ^V!l99G?NGC9r*xgiR7;jCzUXFR zqLla z_pS?&=*V5gZp#?OFr6ImW03HU{ZG5%(i`SaBfMM32KJC;)F5N;dmnzN@`O;o9Q{E< zh4jwoKRTKe`%nvM6aVMrUFy-jfaH1gOC6Qaah4bvwfPY?09V_0#d^aVWXG28LBF49 z;J`Wpy0%>`y+=OQ$Zdx{-X~Qqr>*_0eQR@P*v1~0zztvs*N^zyRKcF6FVJ>pf+r`H z&w~Bnk9C%(;&hfNI=_3d<1Z~94jC`uh$rBIvx38O1>a2E)YzNoJP z{NX(6#>@^vSY@-!WP5ThVJ{5OF4$;Nro#8{z`99)T3#qiN@THnbFs{X(AS^ds2368 z|H%KkT4KJPl9wP#X{qvo>@bI<%(Dykm-ZX^yVmHB%( z`6kYOs$q6c=eM1@$bMX7UVeW1(!2t5 zHgg=yc38UfgOi!Rw$4R6dSi+>fPX_PH|=A6A1=L`TWWo$U5~bv%0^3F?$o4N3o>0( zq`4$O`DKUoZ#2?qQl?i=&b$Tk99;=0y&oxqIOWb|@ov3me*1rAA5i{<@1+RE*;7^^ zwhJB*(gOJo-O8Z_a`Y&#*4`AOjWDg);9l$KmP9m z(0zMrrO$Y$_VMp*GkX#fYgawWU%T^vW!ImYS8^xi48FI&$!kWIiliWk{xKk18R|cfl`fJGeMCRlb(v%Bl&B<|WF#q!VhSrIb zX9ftG3tCigQ}}uL%m>HpPn5jG2P~rfn1n&(HPEj#%-poj|MlQFS2D@8Oi>`S;fri% z^TJ67`|LS~vV`h1fq{;CP{?AJnxc7R$-ff`Tj!eR=XeP5Jd;}T-GBzMy2}tZ3>C5k zfqyh6auuN+k4`SZOU*zM5aL-}+a6~k+!0Ho%IVP9C`(#&%1fF*p(>6R*;@|yU4f^z zi-V{tSxGYQ93~%0A0FSv-u1hQXgqFrANTSeaxk^``(Jz_!;+8?l^H3G|`5j>AxIPek$8Evz_r*jWe+0b!3DPea-f{X1(Sze$wkZ$d;Ih zK87)yq9g{n#@p(F89LOPR_<2IM$)t#! zFI#LK)Mt_;I`r@n2poSbXC=sXJlJ7JW5+765oRK#%lx=l^p2+xd$Hf1M&;0r8Z`B6|K4e2etbr$Hb`wSWL)*$G;LKaH)#WlV} z$71X|UiToR#qbxLQbCk7;jkX97FTC z_Le|>i;RC-ayDe@pM^k6ozP7AaF_pI;%s0)dF*@_EzQ6(A864t7QAjyOY9P_Ibak% zB+U5BV}864>(7s&!8SRM3FqU5R{jcTY^qSreu4z;DA@kz`*hHV zqoOGXaJ%!Hb=XeqE$aO9$+Ze=yC@gBQ|U8ryn*-Y<@Bpmlm$0(Vsx!7$H_~ch>G%8 z%Wx;*y(|Aj8ZyvXN_JEqq>OnIpImYcL@Dqm5T0xgfI@?>!*w|haLB#+qM9ByYXRSJ zM&a_adp8%ZIbm`0LoqhPxqA96TBelMB$_crleVQAQDXnXff^mC_CcCULnG_0TY(Kq z)k;eLtBVm6t)a0u2aIZf!|_VcCe!5M(=y)J*;SL1b_T4mzBS0P(ZF`jKQ?VxlX=p5 zg0`8=4s`?C0XdIoLVD-3Z6K+~stYSxBgVB2@AU7{ZCs6T%HG4FtoD_^_{u+{5>6?! zz|*GCX-mQ8bi=i>k1G%vN?(A*Jo=PZYKFT5AmNyFi^T%D1Kufzj@ZhwQA`S3w6Cfl zW_t_}u<+u@&e6GCEBn-Pv(YJWU-|sQh2;f~9_>qy`+x=EhgGYKq8hdJ21i@E|1~8T z#s0zWT$V7Tk~Uv{6pex#p$|@WCvjg(NnmTx0N*8kBhryE$}*&H(_cQAoE33eZEaW^r(&=Oj9)2)VRULB^beeg}>IFFYtTUGgV;vlgKCIu6uT+4yx|-N?in zJu;^pKns7%iH-1d@)je?Pm=h5>!$-oEBz*;?a=H-^}VqB_8-l0&H{UK?NIt7Q>b6( zL(yYa><%M}6hF7oQ<@b~eW1ihdL{jPX{&uaJe4I~dC>RcFVwG01&>s&feJfxjeJ(v zNl0rIdG=6nFL;qiMa3M}9o;&Y=v>f((=D@#T-P$mH7Yl;kG+>Pt;A%hF4-u1v|79T zqYa_|>VIj!vGD{aycUR5L_N|z7J?RpE|yo$M(dV>Xlgk~di)3947et=rkr-yfqv!8nHF~3`i1Dpl1 z#rdl+{wlXHG2d8zc-`Q-p(>}?@C?W`J=9_ceDc5OctPWmA(Hc6&B#p{QMGAhmz4dQ zc1wVI;vVLC4kfCU|BZ;(D!+npfuK6&<{1vK+O34=Onk@V=CS zr}|p#YDhLE-SG-gm__hXUm8nqkpAdv2T6*#bLdANvv~Z*@QZgEq-_>E5^LtMQYjG3 z=_SQGO>9T(SFFO19q~;EbvwiwV(yGuR<#lZmrdSfdxubZsAIbc(==z@4pEXcYKi0e zZ16L4by4iS-V`lAIt7@1L+%kKr4NmLwG>dvR2_h#dZeHs+vrHgZAPay7GT3iaex7* z7U|DG$HVl9hwFKIoUJdHI&)@Y<>b#n0Q|hRw^kWu{I#Qe-UB*}P}esWW_G_K8Fo~= zfPpJL!_bfB*mh literal 0 HcmV?d00001 diff --git a/assets/images/arb_icon.png b/assets/images/arb_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9cc4798fa606acb264acdf2de2ef0c209835df4 GIT binary patch literal 9896 zcmZWvWn7d`xLsoD?(Xgm>F#bsx*MgtLs&vmx5Be(dfW~R6BX|ed zNlr@+0BA|We6az-`{>@PW*-0mOv3*f0wBMT3;-1tiHkgEqE_;+x4#nFCQUJ>$F_gR{K2xt8aI@W9g zAPA*+7q01%beNusv^b#g(R`2G$%selZ6z zm!qKil~?B0=6HrqD;_zuI^!k=NI5yhk29mg6-k*1L@XFPYTNNzZQtBG;mO#WrhuZL zjGzj2U%6DZQ_v9z{OA<5oRhobzRbD;zLu(8vP46}Xnu{wUJK^_g~L(5(h3yJwP)ZG zh|h}51t!B-nPSi@P1Nu`*F85VJ(~bbf&P0dRd&Bq-$|-jmtEI-xz>)~E~xlekNX?u zm@rBTd|P@rLF+*H!r{vQUXvlm3lgu^wy;G?#^i|e`A&|oeN+EgoL#=?(8r*w0kJP= zU@cP`a&W|)DXw{}iqdqg97FT?n|B*}9^id+r8By7`A61EdztX z*6xYjD|(Q#i>I7|0%4AqMe^d$1pLgw7RJOXIhIs|kT|04ytC!7Eo1oM(`w|>>V{bO zs>UWHu%d4g=6`u_G4T)UIpCCz-VC2;cnCaDVYvW!1<17G(v053k)z=x6DgM?{@c1( z6r1&(@t1h;#stz0q0a3&xLVba{0!6IGClAEng5XtAV3Z9RGI6yRUC^A=$+;Srak?* zqF~aYNlVdMPnWdg35CIIL@rzDH1Ln>V9{${s`wO2Gjz7I#F4RJ5KofieXrPw} zg`W2bMXO98sZ;9=orpA|rKROv2^kp~wz6T?lC90YEth#GdD1RxdrzJ~h6boVF2JKR zCPogstAY7WZKm)}DMX}}O=WbD-n~@^1fmb%$o#8TLjnwBZB9wjvqijD_ zt2Qc3V=u**Jo_OZan;4@TaL(l3^ET z@g-o~Zrgu%CEi{_t_7-xB=(-GDXSW^@}@UvRd+*{u2lX5w!LQ&Sc!<~TNOuF80`@? z`@i4^?|t&kkHSJKhz!-g57e_392=JajaZ9tb&dpD8dMD>#*%SuzL4qf^TbBzHWNG# z%KpOlZzPg}C5DH6VG6t(8c;n8Syc8eG2{z#n>|SnxJAV2e-&x8>Wf;z&ZZ-Im8 z8rh+0DNiawWM0&L^f1fiTP!<P=cD8jQsMTof)NPV;daJ!bHIeSf24VCNFf zlN$CS-Fi=>XsF3XuJ?&Gl^!81nWkMB+q5H`c;47k)_tBV_~GO5^g&(Ade^4^y6BA= z+&qLw!-~_6a5ZYMS^FqZDDw^XIpCaVIeeV&%v(XBh{V;47p@}2f}~Bd&Whjk&4XBl zSw4BLdS=VDz%8TjpK(d+)HgX_136 zsSlvI{ctoKl>&s@#@)u8^9Sa&kL{+X{n=^)G|_t0W^EhgU0bTfxFgr*1E0R&Z~8Zo zoYq>p)6nR(-!39zcD-lRpWyVIDd3Ax(R%-F6u-LRYvW)X<8w>Ic<(R=4hDMb>-X0L z!IeF1CQ9egWsf|O2=?d^#;h*TDSe-;IyC;Q!%0;i?)boSdpaGxknR7(0yNQWjk!Rw zPnzMk`TE})Xl{ueg!9CN$;k_76s~SiU8-+7S5dEqr>*Fiw;trv4vi?7LLINQ^)iV( z?S=}pScu-|`On}KO8ct2k6-EEN8}l=+s2Mr=SYIgoSEW20cnnF&hPY=k5ZiZ)FX5R z`<;mTt?2rjxGrxH@%?$4M#~fCTb$2Wm{JOyKYXrQN2SupQ&IO`^GTGw;$vl z^w;L$6Fs|;sqMPyJE?((mrkF*$5OG+of%s9ACFshL0)za`vNT<7Cnfyy1J&Z!fE}H zEcqFI{ZA#`e`m_+M?r}g<5%2iHphLVXjtgkwzSPse3@C1F&gj6J9T;s%{zykhT7f+ z+!~5NUgkIhpTQ73o()+HaL;6<@Mrdw4nIbD`H4*EB|S4Vd$F&-AD@VfEq2VH6Y1VH z|ME`-X3O^W5*ij(`fV}QXDo&h&^;XvQDAzUN_^7Bj|kXk)8+DvrPB0crmvTKQBW^} zjvA*sZ?Cc!#^6ar?v{+jTeJ9phx-HPC6*|2z#P7=!BJPOv40%OqS zGmF#k1FCYwLW|kwc2k&c{LY#E0k6ea5vCH7z*EuCN6Hd1(0ugs!)>S4P@`zL7L9S0 zwNZ#{r-1#L9?>Fs zT&nrSk!FH5t08}foP}&RomOQT2rC*9#??&mrC}$}-%e>5)NZ;y-FH1!ivkbtUV~o4 zk{iiw)Eb|6;wY#GpbzpXG|ZejmBhlr?JJiyB(^La+(Rz zvRk`phi`^$_|D?d_1`C<;SYEZ*x`2_GYv0Z_i_jpm=&whE9{JXA;xJM=AG8j4iU?G zbYDDfM31#h^TdG$`YiBWmDP2V(}sTnf-hl4Nn^KU0vNg$Uq%Hf7w}10-AdFr)zzn< z2k#Ox_{zE8)*e@*6f8T^acQm!^Y1Q@gSd^VP2OqxZXY%elU`-dXaWTdcCFxu_` z4r?Utj}{zbX}i9PkF`tA&6!v~Y9|{7qfrqRRhSn}kBOL-?^{XnZ}?%g+IcF3=UzQ_eSvEW|NKGFBcUUT$d!pVlNG-)*#CVx)msRu`OskeB;Z+OHywLo0<4*y-WG zm-mxQG=$0V=ay_u=r?^ZW6>#oTZZzBFWD~ZV2qmh)5;is8$d)coFV}P1_mX`3M{?ef{loY0C4`R}^~tq-(7W71= zTj4gN5*Rov7Vb&OsDWW^^wm1QFV7uGski+dN~aMZb)H!}*@UXf?)bhfMkO*&M5e!N zH1Wm=m(D@ppmjpUz(DXWv@kC6QhNcchkDW_L{_1)UL7G3NW#Z7CqT#_FP(Dg0pW>R z=K!c26+=`FZqay9xoAg6s~%v_tEwq3>!P}u1_>%~)Cw7{#%Hg02ifwY^yDLSrAZ;( z;RV)tt8^71t`t|0CqLm;N%&l0sr^~(2}L^&uxHb!&C2eEVC*yEKrN$HS@+Yhdna1> zQVQqy4!wX2BfADiYP|=pC;la0`t;I%rOhZ^1v=>Upfr&5-Z|3ymJ{B--H0XanQ8-nQZ_HsOq+U&odGW_0#%es~%bit9rV zt&HDLNB9nTT9K`sHqwUCym?>9b=~1-=_O=0|7R|bAB2mMjf-@_{BX*b{`cG2lzXJG zPdp#AFznS+&T5J;UlRC=7Rl=khWw+$WE}hU_*fN(9T*yh zVr$#wnH@?*^k=-JgH}O5Z($(H_&7Hwws<*0!c$=jP6Up#4E7hPa zYwIfeq|0A`cECMbqSRw-p1Ji0`L~gz1*fPU64v8frt>4=fq@)F*z*Tvig)nXEd^_Y(?sc7%7Kp=eQ)(~Dl1^}D40 z<>4VQrSX$?vWsE`sM{>wlt4Ci*TGE690n0%IC~8(qOJOKK^b!%z2RLetoSs`x?&W$54*jm|(kUSUlwW8xaR+5Q zQ!z&83l(M~r?k!$@f-FfgQc929xUy5?CJq8-(8-5!{rt z%{a5aMN;w+kd+tQm`_w+ZFGqvD}1dsTFDrRm5(?$I9kH)>A!l#iwA+(FVGpNa+IY9&gHREm|hKm z`lMbRnWsZgkMIB1we>OADUb+YucqF@qz>nVjfS2f%r+~NaZ6K#V`|ALMMM-rz}bSI zc~ENtM@MTPd9hf!HKi ziS>C>Mb{KRIdDWxP^)Uv>)0WZ17ZIEV%)Lv5fj;{4Zs)`zW2!=FW0G8$;ekbPCCzX zP$-w}>BJ^ncH^=B3=}1#L3(GULG>*dV^uS9&leGKE<>4Tf)V{;+Zh3f$3GAs3jy3n z`&(#~!u6DWX4cT0$aqJ4F1_MXpZ!x{X)Pq_sfxbpZa7+KZ?UPCZr>sz5&K)DDw%%H zAQ2H@#E>8--yLb3#E#~UgmE#zdDYu@62J-a*$JJG9kK(WDj|r;G1|3XA!8S5v({SO zu5{G(lnwxOWcmSiV=1k0N(-N-M)ghZS zZVbj%6e{oArysZTX_>2{P^#r92j(7Ga(D&)0i*f3G|Ib*4Gy>kdW#|C!pU-XGdK(k z^=%fQl9*oP&8=(ODELW&%%rkC1Vp~+mZ|ea4dGRA*+dd!_D=;m$#jFu~U0c z!6t8$&oea;w+x>reu}tMbv=4qMh4f8n8kTE!NFjPobS-inKssp)q9U@!{4B7TM3crw};?knZuPc9hu^vH6c4B8WfOrs1u0Z5VTItPQd@NarXXsN8b z3=z&vgtx1HX%HEJ9rlPH#?QIFvv!cD?FHQi-fUwjDyz$+{~_0e{kv`0J)55hWO_f= zwEEi$t%mw=V9J-b1R#g~HCunAjM4hDW*Z=grZHB77*N%Lj9sPIlbqW~2evcwkD<{Z z$>eRoV*qJ-F~!VF2ef4$Q={#!&>$M#RCGk+X_v%UiT;)iyC{E%=(g5@i}K%~E&?6{ zcobDtnW{%sC77?d(kVl!>Rb`-mXB@P=0d`vP38rUvKu<^du68O6Agg;JQt3in^nmi zH}+nV93mI`$ExNqSx0_tYGZe@RmKzQN3N*Z6LqCh_*PcyGg~NCRHq7wy zZ-bHS$S|jWy90&N*eTIKHjD1Sm`8fmuVrJ7|8%zi&Y9s*Sy2o+EKvd5pwG^oShcV% zp97}F?E5Ue5jh)w*A>5RB=~}b?zrW47e8NDDuEjQV;#vB=vz>jxMVbzx|3HdcJnMU zS!T5#ZzyWu_xCnv5&2T)bvDtXSbj9zoc*wHQtK*y?NT$Pf0H{Ow+Vn-6)VQ|1*wyn za5>K}@GIx)z339r=x^3ea4Ip46eGqC1#6H|=0(MjWKM3AeWq0h0=bbw#`yLSc=2HU-USZYfWHLkh*+bxqZ%-y$>; z&O!g1Q-iGYLm%l*{iQdh575MkN&b(xX?zP8$r!dR+Qh0WHE9L%(*G(gRlrD+n2Ekk z6D_vKUU6%*$8X1p4MxU%{ze6&iqSwKZ3l@X35AM7{ReiCu|d_124;7*t-yh1e(FmM z+%z^G_Mr=io0XhqWvG_fa`Dp{wtH8Sy0wsPQNY~;%6i>4$Er?Vwz}@f7 z5fq_ml^&HnBN_dvT%GKuNL0u>>zo;$Z?GlLXJ=>cHoQx~yMSYP$5^UEx-cpcjTPa? z;6^Vn@T&rle`rV8Si#7o(fRxPvPA7gzJ~2$ah1|W?hIIQyZXwFd!umKJtW@g8PG_#4@iStz11b%G<59pVQx<44{PU@ywZ_>R~AXJUlk|$ zsVC{E792$Qxj=M#O6G|fUbjqg`Ze>%5pb9Ij%mc=`Gkjl&#qs+!Q_2@4ZZYq;Y{bu zpF6IB+vUxSN8L13llw#ii=6(RI$=2lU9e5K>dDA9enP9xZ0)HH<3}C5Me)M(rIVO5 ztS4e5gAs&E=yW_~n5Q4aWp#{MBC`EPVu40;T*Ip$^mDwJc*dBDeDk_CAkty1^#s44 z1K=`CJ)oO;L;M}J@+5^^kUyxk)f{Y*1B*$+r5`*}oE|o(9(W+5r725!YN)RtUJqF_ z!8<|Tqzi;dVm0*$tTGytdD{4hxUO^)Yk z2U*0K=!c2FAcLjj@8-aPpmROe$PslbkKMKr?(#2u_nI%c#=6Fc;}QC>Ra-qV=H7%D^RQvfj=B2 zI9_ODDpPXGR(R5pacW|j8M~Ee5aEgO`EYoys7e3pmNcrS{Y?t`b-cN{Vgu&NkJe3B ztDJu2WC!vvF-dttyssV`8nc3jP7~SJ`@XzDcs^3}N3J^F4YQP|k0a}&mKe<~zmP*8 z9wfSk;FzDMO9?F|ju+|W)T&Y>*ZwN$Fauo)W3W`lx$;gTcrxJ$YUG|_l$riX6(K9q z4H&>R1*sIJ_gJ-uyBKd^OFuP7;w^?v7j?9{%2=Uh5#hhy3IbEo#pd_{+_z|Hs1%nB z4cBc9`CAGoB|ndfOKv0d3bX|tbaLVjjR2z%zEIY@K zXX;!`RLo+#d+0IXVw?7VC*h?HBq}~pU~ZmXY=)G}kItpFv{s?Xq%P1i8YkmyO)6S2Jo7*%s(grCFjQvl3Zq@yOh;@RKR+xX6hPIq;Z5zzu& zQ+H$@`L`DB56BW;z0!c z=$XUe$E_Iy&RuDzZ$%SFim+v&-KljFPHD;V@>Yy>8E1GzAVQTrC?sVtg7YL|<}fN+ z?i(mHfWU}!%jEmPYp%zi&s_4$3AW|t!~5{V!V4}mh}{K^ZkHkgStT-yE4U`to%SWi z{A-%81TSsx<~iI=Z{u_3ut|$PTZsR*SHAGrCsr> zgPK^8k>r<ga=A z^P~l(`Ho-$@|iuwpetF%m+M?2tG zRy3EEhK4Ddr$5Ig*c_fw)`r$`X}ZTt8##z{G-6#`jI9nmhTQnIOa;pk^ahlNwpvyh zkenz}zNi$JP8Gf3ElL;&EJ4#;fUKp`qb4zPd%{00pkQaef4K zn~*Iznn;8%eS)>jI@Xmn#Jo!1wk{unQ*c^;EeagHrP-U&(|8Uxul-y4OV(zVco4_q z->6Q6>~He>W4~9^?r;+QEE<}j-tg`%1u)uXe2-&r6Xj~)j~W^)wOQNXK*GMWn+7cC zUA>jQ-P2v#qP(XiIdM=tm=#O+kY2^>^!@sa>~dJg+)npixA=C%D>UuZ zYpYGlzVtPI5-{l6E)aF2;Q9#@gQkBy@y{DUNiJm~NKfi1uK(7}wm~8&IlUh3|G#($ zM5?}_7Dr@y2o<=p3<RWl?|*RNwRjxU`eml4=BBx+nmWJ7?hC{AEo+3P8IRXrW`c$C#wPEg zFjemCUQ+{XL_Xsa&6D`pJD;>OMA(exaOZV15!z(%M}Kem!M66@vuJ3iyMMVf_)LZb z<-Uh@hMLd*KP<;S-jqdX@{+CxuR+Z`kuXj`(Bomi&5dD_o`q#z8sW1)quY#hY}{&= zY{eXd-G+K)!&)T1n%yXtjSpmg3G?*iKj{bt?G!uhVx=^_>gQ=Zv%(r(Bc%y~k+I)rV z8=O3Tfl<&Oad&+K_-zt+%8KOz^o~Rp4WIe-bYU!7P-4Hj)*BKxLYBoCi1>OV^YzwJ z26p$7UT`-+pKMbGzkJw+S9!NlSOdT;%@7S@@MJ414C zP3N6M)Wid+Ar_WrBqVwm`$FI?e_lqporOiuxO#2A5!PHAh+ z_a}=dKJ|P{%-UC`!0B|F4zfGA-(w$s-vj;k9ishZYk|#@ap*qJM8*QjTLHk*M@$Xp zspv?hxX^r0#9LcEc4S1WKgF<(D;{A<9{CC^tlz1>%$1ygzb*r)DQPP<%3DYO4~pL@ A-T(jq literal 0 HcmV?d00001 diff --git a/assets/images/bat_icon.png b/assets/images/bat_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d4f6ff49298cc08d4fd13a43433ba58756480c4f GIT binary patch literal 4335 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 Ju0=V={}1^yB;Wu5 literal 0 HcmV?d00001 diff --git a/assets/images/bttbsc_icon.png b/assets/images/bttbsc_icon.png deleted file mode 100644 index 253c01d1c54568ea68fa704e9afe352dc011205e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10730 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 literal 0 HcmV?d00001 diff --git a/assets/images/china.png b/assets/images/china.png deleted file mode 100644 index 0771ff1e07effc44dffd64d6ebca28b5e45267ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmV-M0=fN(P)xt80WRCnH7hL4$kV0-BRF{ z8*gl;5P`<)gJT2k17Ck6b4wz~&L5F?&g%>NPk>#wv8Ee3{`SvC?Z1uh8cWRQg@QSm1UQsnm)%z zX8G!z1ZX6Q&Q)6Rus;bEXIqwpFXz`x8ua-6@_2XY{5glok9esfUL3q2y@E0kX?zh# zuI6$vEy+`|Fl8w)!1UxvQP{%=G;x2)Y9J4vjxZINT%j}#Ir2KH(WD*ak0i0(P~sCPWb3$8YUu5Dwdaf?%SU4>~cw U;{cWMU;qFB07*qoM6N<$g1!9u#Q*>R diff --git a/assets/images/comp_icon.png b/assets/images/comp_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..620e4a4f785e59186117437c95eac547e39be320 GIT binary patch literal 5969 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 literal 0 HcmV?d00001 diff --git a/assets/images/cro_icon.png b/assets/images/cro_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db6367005fee6d025b3bc6ae9d2922090f01fd12 GIT binary patch literal 4274 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 literal 0 HcmV?d00001 diff --git a/assets/images/ens_icon.png b/assets/images/ens_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fb9f8ba56f2f1a025065c7c19ca4e784fac6fa8d GIT binary patch literal 7087 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 Z{SP-GO9HIaK002ovPDHLkV1gE)t?U2* literal 0 HcmV?d00001 diff --git a/assets/images/france.png b/assets/images/france.png deleted file mode 100644 index 57b98bc32615823e70f8c35a1f744551abeda12a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1025 zcmV+c1pfPpP)Ll+9~YR~W|Md&Ze4nk)n*8$bSp`Ui*`7cN9JF|!y# zR^1d4MKFq>L8u@uCN4shxN)IO7C};}g9E`yEpwJ60?d?iQIl0kr%EkX?1asQ^@3UZzc0)gk(xu3jw?uzIYwcWv_N(;5H;DK(5S z;Z{nat`y2QZs6^}z``gz;;08kYW7D|9mL?d+k_!8ywtKatYRB`a2!M%IX086D< zGqYi1!X2J+Ik6iTHb0K!5Q;7~liB(7as3}I)^_&9kIX8U$leSL-j!&3gq zVxu)1fK5jSgGd`9ITw#(zEm2vrCTT|W$$eDSa)|7hY!Q-+-VGv!L3`|0D_srl38n5 ziQLx4h6ucE8~a4Tt8?eD1YDWfD7hpC2G%BRnY(tevM?g%{{HY+NXa3P?LBG@24LFT zVTjnoV)khYkqD-ShbsW~lJCDe+Sysd?%mKthGQ{i3+?ac&w%_X5GERB6OE!mB*zm8 z%mu+^N~hoT9XOy6i?MVZ!_lZ=BL1&Ha3sP3k^Xtl9=v$|JV(jo%Cpv1jYx#G5Frp> z1QddmxR zG7B5*>cX#u!Y=@t!*Bq6$G+GI^1S)s^XFfIXaU!+uSp=#zOUHkV3>uz&+ltB^ZL*s z`G|-gT)NbNwOU4?wIE>425gvT8xgd0C6$`0+`m7LmoFzVJ)K8BpXaHmDb8lIc$~># zxTgm{3=Q#Oy?&>mtfsXnSF1#>TO#0h0@S9`8LM`0xjc7yV&X+^bab?u$z+gDr!g`z vBD2|S{>{ot(l#I$1Yri&l?q|Av$OOE&u)Z%W!&g000000NkvXXu0mjfb2Z>8 diff --git a/assets/images/frax_icon.png b/assets/images/frax_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..81ca4269b5725bdd3847eee0653b39fbb4974937 GIT binary patch literal 5520 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 literal 0 HcmV?d00001 diff --git a/assets/images/germany.png b/assets/images/germany.png deleted file mode 100644 index 2dc720303e39d1bab5f4db8fb5842a01a0ddd2d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 576 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

@d?xT!E$9ILz9A*Vu6n=KY*(E? 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 literal 0 HcmV?d00001 diff --git a/assets/images/india.png b/assets/images/india.png deleted file mode 100644 index e6002f5466205f895c5fca6896ac1efa396babf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 683 zcmV;c0#yBpP)^SIT?N<2J(gJ4Y^`sNQoU= zh8Wpe8rf9m=eI(*_1(l76!`&YzzlJ2G@lX^lX-q=a-i|&uMSq5+sKURxOsgXH?EB* zY&aM2JE2P8Y)ay=GBeQ+6$7va_NCNiTsu3pJ>*=ZU9Aq4WiP7&7+ z#31jUzxjbXxeHxutM9k*;&C=%>v*98Lc=j~+$gbjUV3<(6tFdD^X|+ne{6>a&3%NSC$?J1bNc%rK2ah2Vf;<9{$rw&_$ z3~bj|x44SZqqvQAwXcz8y&~^u^(3&!r=Y(bKqobRzkC>BqQQ$1V3B8O%_kdPHs zVRA3DIj2RGo-~f`lK~FIU`~)H>KDMc$yo#*uoiOzOVhMQME<@*anbHQ_YX>26>ly! R@X`PP002ovPDHLkV1g1%DUARC diff --git a/assets/images/italy.png b/assets/images/italy.png deleted file mode 100644 index 616fa97d8a9cbc94519330f02626bd855976b474..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1030 zcmV+h1o``kP)D)$dOmbsPuq&rt@0K4O^;7YK+``G!5Ci!pSl9e%B%O=f1;avDvAk^H!%mK2(p$ ztb#kDeuSw2G0paBFzLmAK>B|$fBcl1uD9ZY!>?&8J^f_C;=q(m3SMI!*1p|gt{V5{ zD<%U7Gu^6bwih=+Z7Wa-g|&18j~TlM9~>D}?|1@u-Qvh!am!}b-C$+Rj}SjdV5Xaz zW_#(wzyE(rgO3h23Ycl}(q&5< zy&rg<{NR#t&f-Y2RVi}k^l|CqgC4qUmFS9NKTUfBIN$6n!j5qtfvIhRb`M?VT{I)O z>D?9&e#MJK2P>z$w+{+#n&I8_4lfy?X{@uj>q0+Peb7hYd|=xh-WFUq&x=K<7&(xv zi$8}A7hfR|_tVPRXC4!DGmtC^5`M{xNn>p$eAsg77^(g65Y}hIi8+1s7T5 zC1Y*sN1iovqX+U&TzCPUSKd^)nVyF(_P;>P@GkXxL1N?PFIn64#fF+uUyvK`#JwL6 zlGe*_Xz+eJzH}qD_0(IlYA}XWxlh?^p7hhIi z{pReJpkjb(+V0UuO|ftgL?aw(!ZI`rbpgEFZX{0BhqcpvIK1?c9$zf8{u^Jp;deG} zHSxG$ck9@9HVMw{eHy;7vOJ8)P!o~4#-T3YMmu2Fd%Zz8=`aHJk20>7R&i}@O|Md^ zguj)`n0_mSsd|f&a_$SV&NeYF!2*{30ya@F#e5tbw*dseeDu@kKrWk2slUJBU#51#9vcv}@&Et;07*qoM6N<$f)0i1 An*aa+ diff --git a/assets/images/japan.png b/assets/images/japan.png deleted file mode 100644 index 2295c0dfad26a26eb8dad0a34344df8a0d891238..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 570 zcmV-A0>%A_P)Z{vJV@bc1z{M*&2>AIaTRb81S7G&_(LQR#t|CR3>eE493fS0{>qU* z6=f_zE>7vdp}_b%Cd9$rbt9mHCK4@N+JWO?9Xp+F|8}?X@HClh%j3(+YyxgnnHko@ zI>K68e3N6~`%S`FRTfg*g?ddG2oJv@Jvn&`r_-*T%gy3`-*>9fb+CW;O8@`>07*qo IM6N<$g7azjO#lD@ diff --git a/assets/images/ldo_icon.png b/assets/images/ldo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d614bd48be646e716ffe1585b20481f009284aea GIT binary patch literal 9033 zcmV-PBevX$P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKABHl?vK~#8N?R{&M zT-9~vu4h;EQ_q%K0t6Bg$QFYcc?iM=;;}u(vl1t>oaD!>tay#b6VH&WV6watn}>fG zoQbgkljTLmGfq7AcqSQRLMCQnFeIR{k+3Bo5*Q>Q2_dN^wI2QMuIhT-`M&eGed_k@ z>grZk^)1Y|`s;JfKIh&#ckg|k_tveLyv>mR^M{uqxCFcc+yq_@t^<2P=5>HwU^kcn zi99$8j)4>4FnA8^2akbA4NqqN)9uWA+n}W#Apc(;P9UI>w}Ur;*O@epodg#XC6G>W z1bi8M0o==U=0DzEM%a>QIR}8zmm}B#-U{9VZUJpeZZ@XCgWyi^NpLruVF+Q%pd}n2 z|BXNE6wmu*M1KqX5ZFg~1aTVt2Wa(sQ0=psPyfNV7DpG6c7XghzV>bezXSdz$T7W@ zz`4Vt;CC{g{>-aN5ur}5cs9cfA~z( zR}D>f02u$n2p$HvD#1IjH~{_uoZxAlYz~^{0Qqk|oW)xHONg@7zk}KFki=qlWj=L# z&ZHZPrZ_Pfb z6MR=E8-s>AK>jZt`cnjdtW(iL+^dKei;36#snC3If9Buc(J&o0^cMNQ zLu(+3#HL&=P?t|Zx2)`dp(`S^97ZHo*0PxO@2z~@^Q-X*hUMb0it_|{3-?frO$Dc~X;|WjD!iB88s7lWaodwNgr+xv<)l zi99w#*H0>)F9I&w0Z6)?>p$3cz3lB?Cz(naA`h)TCd=Rie>2i8 zhbLNMC?GfE{}V>{7ln&*fc&=}#BATg4An!|#O^b?jC(~H?pP;3>bp)-==yo#N|*7N zBry!c5k7jMS0>6S7Q*F0KcNVW}7K37@8?*84xX>}ENi%1Y9s0(Dj# z#-|fxlOnHR9BoTuXv<5zIY_T9L$N}(y+}`~oor*TN;WM7? zm8aKjS1msVqx)n&TEAryEW;5VAL^5QC8f%=2>!^}WZsy!1LVK`;5E?PBT9R%K(zGT zm)2^tZ6*hXG9P5?wISgPTNVX?E*;mTQuCcXxSr~TCXz~%*=AR)pFU-pU;AL3^ zysOs|H+B-0DLJ<4J@Ufx4Qkd;SbbS={MS7Ih5;}JcEJ(;ZepdJoNB3|fHwI{W23q; zzaAhiUxN0w`&x`J(V5F++e&$6{d?7}}jA;9(kmhI>TUM_XW zo{2v60(6>|!5zj{b)hZ?zzO*bA0Ojt32?9ftO%}53&>U zfhTgS<#e&Tm4*W1VPms7QC|-b^X>$<`m?79lU;v4-7WvP{(5zbmxc}p%JLAlcr^qt ztY#AO2}#IP6PHL4s*2GG0-qOA(pG%vc23Oc0Qvv%0B4qes8i8L+$+gT>)tQNGd+^V z63}+_GYPN1HXV&N*!7=UwiR9f zmAdN>Z5-2#lN|r&Af4dvr`F23Qa2o?Wt@OEn{z@~(*g3|ec)EKl3!KLCE|>s!!M*d zW#{@|k`lXhuK7b-{xsDvfUaH>Eq)eqgJ?uj@=X46br%7m8J{0f{;INJ4b18QQ-8s) zu&J+WwFKDp@9WzkN3+W%g|0tD83EGxAT&Vgy?}7)p}>>9 zz$*^LtPbF1V!sYW-8fTm(PP3Z%GvHK<@uFYIkSGYavKH-Ifns++Q{fJ9D~os42FUy zi|b{u(xrz2{Q*(ujb2vxrQ1-{8Uj9R!bl_Lc~VSdQSW0#lt}thA8O&(UcmH@U~RVJ;60UPa%fyx$3T`wuoq_NkAm;tP(Nd(zhdeHHUM@dg zb+t0S-sUBOA;GvS*YvyiklFuiOriYKwr_V%-+E7t4j}Hh6CjP*H`)AVW_u6 z3GBwp@s#XZw?lG#z7H3P23j}qUrLz$l`w3u3sAj)PZ+u?v~itIV?uZ?mX=-RO{fKw z+Zyz|tfawT7a7tnMSSd?>zGRWm1 zcL4SB&kfyWqE5F%b3gM|%F#;8tO`=bwpFNdfc#&5mHWrPK?O&dT=ETfTq@76-lli^ zRO=5Tikmo(t3be3&vk--I<#tT2bbarnLb!)%A=VVOwKk zAGm}n@KtOPe;XHCa4aD&V9h_4NaF%(b-_B1TS6sJFA$9`*drjqC7_-NYO5CE1O=Se zgG1OmgR?P>;|Ahc^aOlJwKG>3z zaDa3Q6G<3c_XZpmV(LpIdGrWHI0LO6ToPKc>~pbAl9R0ZjELid#?}hH0|0#eYE<9A z3F!Iu-nBbqB$<&!rTY0kOLq4~uxgw)Ou&c<8VqrsOv#PG=@4l;LNbYXwXwNUQ1b&g z$nSk%Z_|3uqL*|qvTl0e$M7XH)gd0NA1 z_4r5QCe>ulk-Nk{Yj|r&OX~<69<+>QQ5%$;QVyS@au^H zZ^{2}+T1t*{%=B!ud3EggosMNv}T74Ci$%%eVdmk!-9I8hk@)`A1T;ElB&ChVNuS? ztB6ymJZQi*Vvs2e32kbAFzqCibGfMI2XF%J4?rYooU}jDA^VeCP^(B!2x)7+R^K>) zetAll?uHs)svy0sQht5#*zu8rmUf2zK?hyRY642vXt!H;p@8*`CTMH*!Y->p{@6m~z$bfi z>E-D9uY-*-&cn_H58b_oHmrIw{L{AA<2((+gp$ue9*93>U>MGA^|LXFVWBOp9YNg} zFmn7;!$O|J0-U9cUSW6oYAL{F+1o@E%I4YuVsf3*T*SzSF!%M|AOqq1Osyk_UfMpE|Nd9_rtbfvWzhn6{%G_w9aqY(HNPY$Gs`3eT`rnBxtVlf6s`E; z$7#3{dI1k}6i==w4$6*+ebUR8kMV)SS48n^sF}BsN{$u@Pj#;qjHzvH3E7+)ldI$7 z>cvhKOafU9llX-bpP<63mc20Y9K6%mnU2Y}6`owVCMoIe4z*gr_@b^wgJGn&F_wSq z{;wf+I~W)IfvS6?k-*<~=8hq*un>;8lcc9{9-nUbsm+4XZ9n%(}giSyET?loy2$8gJ@fDREYJWL1R4;%f4PA@H#+A1Z< zWqajJ@4IBMbFF%{3!m|~P%Rhba*~qM(jn)uq^0>5q*^+_*d(o}PdpFs5KIANb-FBT zGev1j=w+%4cK-=D^Qw`4={@&zX`32GkH()foacu%4v*3SoFeXJXoh(@?_?(32{o)fc5B ziDOZux>Pmz{6bqfFMUIA%8CmIBvvk>d?cJ;CszUNU-h*v5jmOX=FZ=r>ywvCtJH1S zBnRjkpaRJ*nUXEBA=wlglML3}9E*8E6VxE5Z6;LY4BpbFr{%T_??`&GsiOc+Nk^ih zMvvMx<+gN13Yg36n(CEj&aTnr)rT)Rm$B!(sMm}w2KYGD{^8~FT))Wf=MKqBdydHH z$Rs9M+Bmg9Ct!(HGAUbS2y0q?pBEEFC9MVCWWu}xj^-}wg5<^z`}#DN@t2~-Z|=y; z@-**8&TV+so#z<7FeZC;9g?3ue?*=>la@ncJ<2H}fR0%H@%zspvgj`%t{0rd4Hw$v z&yTK?i88ly(HoR1l7_~&Y*{T=Z0wb6dmDyp-P&q_CfDL6Hke%bW4P5si7F4WblGzeZS_qShOQV00q$v$~5(E7tbC!j7j zY*;2+uIiKSo_0L}n`>MEILvU*;JjpI0M5XNL@8qrkS=Kl;GBW4kX@CS!pvw&%_HjR zp7lggWO#5)4!(X$PV|o{86Bb)*b98DcSLSlyQIBRJRm3D8juQ{f?Kk+KwGetv;A+ttY8b+ zjX1mcE0W{VmzaR@>udG=tWvQgZyxTKpP&wh51&`gkV4&9Pq)7I8iL?t_e>!nZ;o}M zTn$fYEJiUq5%gJzHjz|Fp^}hSM%vZQogkR66v@yi(<+rA8(BIXmt4LmFTZqL_Ut|+ z=g$slWwTHX(797(!SVzn-8C ztFFU@E^vsvI?@S;MvQsQ=NRj-=s!d>7ufnQ8pdp|0Eyk)J>_VvjI1 zG%0(2dRX?qd{o9q=eK`gl@ricHV9!tGr+cvV1bh`|@!) z)juRZ+kH?DA3PxiEbr8?fHDh(Hhf(2xx5_s`7!y~3y0+F*-=Tt>C|rwn9q0XYQvz+ zkDI!a90o=)1fH7AA`J&Xy2%_yb>tk_RFu)>9UIS}^`j1!Mv<f$q4%h`*+H_EEx$t~Cz*sRQBk1~g@7yXvDayoE8u2hC&Is-C&UtuF&JWll z@SeiHmk!J8uN;+$vDxPbl#!#uWAgKTZ^*0r-;{}oDV)QM^QJ!b{3>KR`1vOE%UHRg z$}?m-iAHdtl#q#%eJQo8HyZ|dvH2wPZ^&a2E6!7ckD_7>@I< z0fTY-8Nskt+@`IJ^MD*$KO4pLI%MJc)tD=vV4ioBJ-|^=f2FQT9Kpph3@Ss>HK40- zLmPKP+R~Qy<&2x-Ni{#f$DcShAp2iBAZPk9Kd4lsTq;Tb@l!DT0ZbyzIIp&FtIr=6 z_dm}GRh|jm`q;Qz&JuHBjb{cgsyQ^+?2IcY4p3C z&5vVFz^z<0v$x%SSouI)ou6gh(8&EVM(&oe`ATMJ%jYUh|K7k6+8!Sd#N=huEi{~T zfT9`yl>#hXC4p6WHti_ShF0!|GvW-Hc4Q>`NK1?Z92DFN{X@*wmrku^Uc~x;+*2)}t6+_DS zaYr8dnWQQ2%ERg&M(Tx)qlr*9NA(n51gCVgvEapWCooJIvni=c9#Fm?w~BU+;Kof_ z$sBnVO}?U$1>-7Y+RB}Ln`g`UX%nCAb9H`G%}-m?=EebHm;=P*Ajo(_$qVI+7Ii$F zRZ6oAQM6U4Zx|bf#X%LXgFMbH?|!y>UN;QZPvx1ivytUV>Vi7a`v5a_)&L%a<<-0o zRTRno>(7~YTVZfMK9@*q{YfK1Wop)&=0s6pR)>OOXZ zr**Wk=!OIEqODR~Rw;*^O}UOS+&n)t$1xo=kd;-@$o+CgZjbRpQ#S1LZFxoKShrjl zxf|G(DjMxH9JDcQt$=E3WIy`(Aq3CpRO7%wfDb}q>5Z$OsSK;I*%W7u<)@9z=2eAY zl_3~6RB4C4+{v5uoK9%ZrQ#0VbJQYrkiq!hnJOsVGy%^TTPvXY4r1~>Rfh(WBTyF{ zpfjnvyfCG>oK2BRZj55nGeTpWU|gjGtjaUzwdMRgD-Zcx_ZVMR2l+hcGU%cMJ%Dlm z{56>TE~pPwy9)fC;F~(y81N#q{Gvr#KL}Z+5ad^-9nPj$*8nOW3_%*qJyxY>p*^RH zQ)c7#SSJk1ZQguH9jwU|unOSAs0{~g`>c*?;1Z^hz4i0s2)?f^VQAr|gCQ}G{A9c$ zm$c_aJ&z8Vq8kq~sAwnc8_A?qXk;pF%G%?qjt!w(#vbF#S;mGAXPh&bu0a}oeKv=4 zB8Id=FeDgXX#RBG_l=ExpsE4HDXY>L_n--1LGvMM&QKY_#8ukLo%||eG47;&vb=i?Y0CL> z6IPW8&_AEU8Reh@lHX+RYb(gIGWH1l7vSU(x zL1Ko2f@GC~vH)F0+qep?5t(-4VPu|l1QmDXZk%OI+I1RRre1Og+}t^iA<^yu%+%E) z^6NBh5UBo4WdGHj>;ZP^RAay{Z)4Y}db0*EK1@N1C~K7)WK}uWXg2Q1tkHaV7?J0e zQE?Nxj%2CI8M<=(Ri{J9M$nb0$cBzFb#tu2*kx=w1GubN@qh^%0)8Qq!@x#NfcV|G zDg`0ma)xppx<;##2ji|WZ9G6#p67GU>C4@?bG*$9rM(7M=8)ICrDH<6Qd8p58ybsQ zZHdbXN31RWIIdvV^pz|=IrH7tGpv+J_*wX8Nr9UVBac@!#!)gGHwLw7TUHs;p)WUi zwv3XqjLEAiR|g3OE(!L0Tf2v6W-dN=c^P>eu~6XB!jZYzRnoY%UwV_a6C2W-Mu!|!ds z`A*t06u6|>k^S(Uyg~E>t?9;)y})~V2Gy(9R4pM3DlAMkD_tWRgZZuB~*Tb@RjWT+UW!UR67~{_B?wNRs>hA$nLu!dCJp_{ced%bgq958lQ7 zA-w|O5z`NWoV-o zPS#A~pQUR+*U&)&+dP|Z<34d-f}u;oPn(E6hH+lY_O2nhtbItWd>V|0jIHXz{Av@& zq#`CSPG9XJ)8N{kGqO25p`8Hmv+>V2h7s0EyRN=}>0Y9@bd{NnG9>As6kL(BT;tQqz9XO^04FHwIA76ZL&nelMffMk`_@@IO z>Uz~blV=!A2WgG34DHZ$f-r`C4&g*~+zLP%e{1`gyr-K_VVPxa<3O8y(%7hO%*z3Y z?7zJ060BFZ(WnFY8oez@8uxtH}25ez~T7 zSS=6T7Q}0|s4|3!5{`J9_(xWa_fA~VDNm@lBUEt%C#vLjZND8$y=yv8BgK~QmTCy@ zH#V6!E~;j^XTS7)1RvL_#())M3p$ZsF8gw;q~ImgQZcOCu8O+`cH~vmAE6vezjSh% zyg%>9`5MAHIrP< z7~0Jz=4#oq$;yy%tnZ8!<*K$pS(hGyLYva%6MVYoL)l-xdq5`_hDE6WSz%N+TJ1i)g^I5~X#+?y24nP?N;Eo$oBWU@9 z(iZ17Ha?&k@Z=5r|1gaImKH1=RtFc2+|K(pBKRTL6enOyBPS{u=`Svq)8#Iil%z5^ zUp`sS9KZ*S%z&XWvW@dK#MIoYE*6b~JAJxf= z!=lz2POt&N6JW!K0}Ja2Q(jsI;RFMfE*U}ZP>AtYz4h0wLcfl#4_2U|ZRk3HGPsGS zy|OHi=VeWNR8}O$G4vE6Z>r%5cKg2q<6m^kw^;Pr!wL9{lz)Th`?RDvu#Uk0d9O`I zEA2AkWo69ENFF@{UlH#=fvgXxg6jn8mqgH^^M~oWVpFm#HYr{4Nok7}{Lg5z;fd$L zk7sYbxG_J7hFnGN|J}#mA^JAh&?Vw*u+Bh~B_TyGDN~Zd5;BeXLK0@>54&?psV|>p z`TGDVNYlt|Ll=)eL{eDNalSz;kgg-^_$2zriu+Ct^xnK>w zga&tUKMdb69X9+5b3eZCw-G!7b}7L-s2AMZzb$*y-3!{`s~1giaX7&>Xiwilbi0uMV20v>e5{`hOX>g?aD-bC+ymaI1aE)vi?{c}@HM}*(F{bw z0W9DM{|3QlQLDG7BY4X2{G#ULFn)cT5)DSA9Ka&C>)wwb_&tz4LyEE%2H&>AuV#Ne zd*eL~eeuRZ5J?BHfFtmx((izL;^hj;S_sELzLD!YF#L=+b3_G^b^wdqt}k|?rXN7` zH^C2r>nM*P`1tQXfZs!R|7`ZgFVywrjHN_NIDm`X?k_G!a0B>J@MiETaH)R&o=doc z;7*Vey4~3wU+`bT*a~Pl2XFyr;LmJq0=dOZr?}3fd%?y24HmxOn7^^ew`}vr7WlsX zy-dUC>cz{gh?aJMFyvnN;xYu6fSfbX8TgROdT=$!-h;QQd1I;@v^@tmq((vBfZ;7y v{)`Td&EHhySk9+!$dlO{zQDY<2}J%sI)a8~yD;s900000NkvXXu0mjfja+t0 literal 0 HcmV?d00001 diff --git a/assets/images/live_support.png b/assets/images/live_support.png new file mode 100644 index 0000000000000000000000000000000000000000..89ad61f458cddcb9ee9c17f751c1d545137d6d95 GIT binary patch literal 1776 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 b(vE)tR=vS9SH@o300000NkvXXu0mjfr1x?h literal 0 HcmV?d00001 diff --git a/assets/images/nexo_icon.png b/assets/images/nexo_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..754a3f99579b2bff18bbee75e127db8376b899b5 GIT binary patch literal 4600 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 diff --git a/assets/images/portugal.png b/assets/images/portugal.png deleted file mode 100644 index d0f1a4917e145e0a963b61b9970fb70ce49a57dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 773 zcmV+g1N!`lP);?FrNi2nh!j1rsh_grg_m=1oo}dTIH%iIE$S z5Ca-bjK=(wy-TJS3 zxc!wkr1^Y7GEI=K#{jm7c_J|NVtA*$wB2!g^inH9MA%?|gXKfp>niLb2^Is}!CEn_ zaR?AE;blJ^ln#U7q9BtNjbHB=*byjUa)KX_mjNG2mOOs{k82%|i-4UY7rG>@Uyb$w z+Y*C&<4x_Y1t5s}mGN<5QMtwlR%T$F;T=p(;-%%p6V1D%Cm*V zxr7IcPXp4L?ofeVL%ws18hP^3Jv{mkH9xDczF`zCKGO9I%h40AP z?4h8x+su>cc`U4El--g@>DWt}49s$VSOA|=9_6&xzTZ>^a1&Prp?$xehZE{8y%sA|I!s<+oJ5I2rpKCjLOkUtsFoC@JC=XYp zai-=o?4>!Fm2)!3{$gq56zqh@l}F%5$9+n_0 zIQhIzi+0Z@t-jC+xPtkXf*j9)~uh200000NkvXXu0mjf D=c!zi diff --git a/assets/images/russia.png b/assets/images/russia.png deleted file mode 100644 index 8b4752cb7e128c4e0cd8a360bc4301053dee447d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 557 zcmV+|0@D47P);{zYwzdc;`yp)NKWVx%llkqm(?Q@N%d)~4<738HOqjY@+Au_bQd3cs{EDh(y>gKU zEFrdQvBF*nz!(7MA`_Vk#)cD&9k7hpZhc{c5{5N!u2D*FtZLQp1PYi8oWe9AiT+q1bl&?i=QqDlh8@opeRvvl`ADT)P>orlRUOaPnpw);mMVpza52q@%N00LchS%<(t2fiiHD8CGXr}afNsm_ zHn&JQiq#^q8Ac}%M2xyzqQi#V2VIWbm&6S649qGzIVBvJu}R9=F;Hsb;VC5|#uSve zzrLKkvCV*GBBYWY*%R+@^Rbi`EtJPMeTn_<=*ynp+fQzjO6qsmcZ%N*50VH_99EWf v-e_n8XRl-OZi|F=c8i~{&)Fz3ZD-yWzwWYv!g1)K00000NkvXXu0mjf2_Ns7 diff --git a/assets/images/shib_icon.png b/assets/images/shib_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..97bc94468f435e27783486e243e1f1822bed6ece GIT binary patch literal 7996 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 O0000ugK~#7Ft(8qq z!ax+q|8J(H;zxoT-I?)gXB2Osi7Pkg3Dg_N38*KK8z3jJCT`pr4er3I2e5lLxKbkW zd<`Jb0s+Er(oEmS{QD;JQ9>V{jY)mMIbq^Ghplu1wjqQK0Rz7D-d5lj`;DcjWNF&Uf=%SdBtkeOg!!;vXdfa&2YDE4FS z#t$YWQ23>rz9F7Kl-LtN?IGNhQO2&41KKGU74286XwX?)u!gZ9xXrG`>w&qfed_bJ zt;rhsa&VT&UA?Vb^azHO%tnq$xovx%XP_~BXUP?o-wyn2r_cxwX7iMYauP6>h|Tf9 zGhG$2GZFK;oir#r6xN_jQm%guOO90M7a-yLI#ApV{Mcgt0iD`;yVgy*&;S4c07*qo IM6N<$g2t`kDF6Tf diff --git a/assets/images/steth_icon.png b/assets/images/steth_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6336e414bee81311b8188c9720bad714f819da5e GIT binary patch literal 5116 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA6OlvNFEh>N)IKe^iWPDXTxScx~M3Dc+nj*_hBPM^cGl657!B&jLF zbP*tmx6$!SF7X09BiTxyd8@F z&;W-@Hg>0(fhl-{WcwsF!I*3UMDcrcJlG|2g86fI->fEs&PyK z=3_5iYhv?hk~P3VlJ~pQ!axhWS+Z@Cnm|k@0it*#9S?Jf(!d|sLDwWwIB}Rf0z@%! zeF8o&PSWMEOhO;QUP)@gFo_yqf5}$v)C`z{4Mnbxxa8zuvIr2x*y|%*!W@`_o%9wi}b80lp()Y+n_?6fml2k0jM{w4VS`+|zMam#7Swf=%?>WdlvMqum6E z;&pU9)+H(jq2k?i539LkJJEIxaGd0o?zD2?3#=*GElIT$?Il1I@2=ygU7|J+Alg*- z@@|)GAKFQPD5mBytEX;Y2EgvRr&i7`YO4nLw&XSLw02+$Fhi}JUDQ4TMDY$fe%>Wa zK<80eUa^5v6v8jiM@fWswkbEk!HkK|m*6_Wo+D5OE{$N5|xA5)IZ*Xul` z|1DvvSl&E>SgbqHirGcYHvv)nK^?D@P#Agfh~!U_1rloY;5B&mCudB+lQU@#Ou%xg z&+AJz(K$VTdCUUW;`7JL6!6=PMYA=4o21Fm?!1N1!ViCx{7Uk72?dhp8ho~^zuT2V zT;ul=t~snZMggL5?+hs~VoEAQ(@ntFCGT>lnQ^-WTTacVeO{&j`0g{xrR!DfToWIH z!p%Y^ew!A{$C6Y-k%i6mz{S_Em1K3M?w3&O@00Lcvyq$6j4}afz=Xv5TUWx2qgN$q z_x>nw4|E+LD^q|^(Ks|y14J=34Aq~d@E?`Tmz*!5@cC|t=i-`kM3Z}UcD`PJtMq>* z*+4<1%dUZPj-4PS)&8q8Zv0l}=s z4JmIH0it+69Y5?6&jZ1mt0d=1?v{kI^F_hE;2+$jvw3>OCwWXVNKj0{Mta5%xnwg? zXYF}D+>$KUOX%_QAgcB^$)6>UONy%dG;p1=CLj&YEYPR0ti$4ym^5MnJPSnOIld)R z@PJ-xN0Vw1-MoW^e~0882~9!lXWi;PE4WAQ^$F$fHA>;6;~|5XE26 z(J#kB*X%`--^`r1rQnA_-r^rTsI!apdZE(zF!%!uDc?-5wWI!wjbfH%L&eix#stu$ z8`rF|0?!`81dIYEV0jsc4Q5&ln2IB13jV`C>q5N*aC7<3lo0j1MuOvHY3IkGY61Z7 zAFLuh@IJjhs&`K7?A_)4T2QwMU;qXV5>YjrqqeI1tl-&r#>bWOmn(Izq^@}l3F;;Q zs{atloe~ygqV`WZJPDrC+0`-u#0_dgGisZQlj9W8bGEjGHuJbn77Os~Rhj^u^0nH2$e}y5&(kZ1U{grS=9M#m-9xTSp@Ki;0Iq>NA)(-zd(ddKg8;y-+|3?;tlEi*AXrq!Kd>eM z>YjUAsJmJ$J0IGKH9>U*aI1IXTa7R(WWD(Nh$h5%8_Ox|Bh2>9G9IY%U*gQPgz>L8c}%meM}ycu7X02DmLL(QY^ zxkhpHlj-Ga>xyehXbF8!dBlL1ps0!feH~fM0TTR6LePiM?*)3uac}0D8dsUic#0}?g6~v zQzWdbgH2~t550bu>tV5<#o+oBSg?ty<1Ff&#Zlxtc?iJ&-FIA*t{idCh?mpVUam7j z!Q=%)%T|Yi&Y($yZ2ea@<7($P;AmV&7GMBE?pv?z;{OJ4C?7SK0vt{(UdD@sGqezd9%aW-|4cO-RQ-eCJ?s4sp2KkF+rVwqh#H>_Ka_ZfXB7A! zna6N39L0saSr~3Pw~tEbah#)K|CwHC(k)h2m+z_TC#DZO@7FF)D^!oA`tLdtRu~&a z5MM^e>Dm7=b8d!E1fXCU=s6o$MuoBd)b?>1#eJd*X+{!ExSLHO3ydTKvJ9!F*m3ZIok==Pq?$Kkn&I37`&_(KTc1JIVuTKIs^ z^H5pHNUfr#5YXhL4dq# z9%gW3S7rrH^#bS_IoxC*Q`EwoO z19`5&XZeolu4UE)z@`M#vt5JaGWz9JIrCn>+6G~wHR;%6X)ulb$4TFQgk z&o$mD$@{GQCK?vgV+Lyi5YQ+JY&bPzR9rqQXigz$0fI;@&w^>mhkiU)0-4ZizL{FI z;60Y;3M?p%HYut>8cl$55jz(hr0~mTG(a1ms+ntWm`Jdd1Uot&gm3#XF?$?LY(4i= zw6F%@I((cXS2WnfebC%AgB<}}Eo0DT8L;Uiar>;z!r1_kd{}uO$-%l$3N{_t!?#6% z?dRF>Rf-av@2#$9z!aFkQUlp`X=gLe zkl(h@LLSt9Lc?gtGNESfi~BR%9o3Qmc&9ixWdXYx6j9a}NCTNq!EP!!R3z9@1<$|? zYCYuTf0hOBL609kiH{VP0Rg8*5&#Wg1qcAs%d`qcfdW7qKvw(%YCid3sc{BD;S)O` zXjJxjxOe;*%!mmr34ooo0?r$QBSUBb4+@-sN~nVOpy@~Rn81;Rb|F7Tt)iA2mIR>2 zS^{qwQkAipb%WZEi9yR)JqHB944A-@061=z;6bs~c=4gMKzO*?KYQ~tAJC8{up~f; zLc(t1Tm~5r7Z^1Ad`DMfwU{LVaI8WAEdT-9rUmAR0E5HfEn(&yTN0q$$l1}L77>yS zMX(THum^nyMD0SfWXg?R8h|AM$_=6%541ptj0^!hef%NX{UKTyT(d~8X(}uUz@mlhq_G00FqCki_&3Aj|5S!I}Up^=^8DBY>*)wC!|@TLurBX*t8X7tpNZVI937d zEltBZBZSm`!o8N=y`uoRm~>-`5CmWippa2P5AK~|=yejHt71VDnRU;O0MAN{h1t}m zuqS}q4T~vSRe&jI4DPIh1O$B;1zkmVF9f?;dCNKmrU;<{08_rJ0>PoGok7&`+?m>6 zMR*#52bl^ViU62_o7`z|Py?$Tbb;-sb1Kj5-lCxP)3Rb)d~<{$fLqjwrQoUpTA-?q zB5FUof7wq!O$TLkh4M6rmJ6i;0Mot8tp}VP3>%|HR?#ecRtASQKi$2dYd=lDdnBxn z6a?W2&`Z|VJHaI?f#uNaQQ+D>5?O`c0Rb*mUIyy{4XFXZk0s`9*AXBVPt6SJ3ozIt zWOwf%u>C9*8#J&MmH>{q-*AaafLCrY{VEN9b}!SrLl-{&49_r6c^K5W4Xptn0g*v= z2dx53nWDQmM5aLi`uan5_j+K;*jOXD_6$z|hXW;5ZI|w+U|SXvg^$`N{9J|ON2kwO zNh&a^AwaK-C%eB(1P3Mn5?B^g72UnSH)YRMo>j8pR8<-P68v1ps$v6}0V_wXs_tGu zk0Ps_1#9*VqdEdO93Ww9P4Hd9hEY8MK(Bj)jx3QLYCfR{i!n1lzew%%-MxOd(h$JoXNWh2UxikD0qP+D z=ym7oxU0l$jQa=_>9VD^)7?w$57o)!3;z`5MUbApEU1qFpx3=rN6dlQWXBYs0nh|_ zYO(L`#r}t|{iuD$Pkvu{5PB}fSky}Z(Cc2TBO5=mK~pGDle2ad@tGBL#PbgU?t>w7 zC+peGhj`^h{R9BL?qxc%*Iy_*NY^d%^|RDwYCr1*mTf_#fwg@WNRHPt-;umiGnzpF z(Cad*Xfw%`N!lK0f!R8W_}@x8T6g!7&(!>@B`4@vnMm6RG=~7dg^bxhFT^+kb}D5t z>T$K2!5+l>ExUUuc&5!qdcM;oA*ayiMzaV2*bJf9rSpiLKT(&Kpf``@?9#LV@qT*x zth#&2TcUf+mO4hyM60K9XdVGPdR;b+WR%Z|604zJ6g~xXe6jIO{V1EO;rf~>~5#YSxAX2b|r5TsENP<0ItO7S% enyoqLbp8+A#Qw`SJ(n{80000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGf6951U69E94oEQKA89qrwK~#8N?VSnS zHC0{5AIKst)5=U+G|PBZKwL1%TyhIsAVpF$B9R2l-LOJ*m>gMOmeF^>O*z5(r3d>hBr&|_hOe<;5W6N6>|I{!!6TnWhp z0lwif#1+urL;nMPAM}&Z{Io&!}o|13K3ajDK1{Pqp#0zN(*`p;tL z#>V}irKoZ~1q7na`&Enz>-@ul+Xa4^&n z;BY8({{KNIl^?$eI`{zSbm)BO@Es@b9(x(+>{qjSclmAOhapwR!C|NjQ0=U%>CE?! zJ{O?FpTOpQkgDOYK$p<{uVH_EM3e~v9z5C(3WH++I#JFQ5Q#@fX?r5~OMu;HYpszx`?X zbrTPOf?%j4>7ATQVXLca`6BfseV)A(~Koi!oRbABy;JE12QJNiM*n9Kz-rNTF*U zoi7yZ>Ommxo3-=BEupia_tEx;6?-y9whX|-+Yh>okADR1SL~c09R7_3tYY5&{<}bz za}T?qeac<=Av*@(Vci1qZTGD|wAi`cFbMS;?n%$%_msQxK{gD)!`Txu8b{q*y*?JF zx2`ZcN#XNa?(t~vwb+{hvS9DJOs_RTIk*Z z9a!w#8ZZD=v+_ldfvYRSN*I8fzA5xPKK>_xDaAF@;EBNn2*}MTV=prYq69c>xdJJv-xAJQm+{2yN?1Bt~ z^Z0x4zfZAEqEQXxgj(ZXP<|`$qOP4UIQEa($eG)i4j+b$VQB4q!EqmDqx0;ukivRv zU$xl*eiqud{8-F8`pPE&qudCJ3Mn+|581q?{I-oAFr(?oJilJ)XdJHq7-Y3a(=Ypv zK1b2U06#a{igP&mrvF^n>CkWT@mlC3e0~`e(-6*r2AN&EKwQrAo>1#}zW0J&4ZRgQ z4bm=9hgs0(Myho^+8rq*8GsY(8^3$`t&lk1R(>3V5iklK>Zv~t>CCx;|BfYIAZR$0 z{Z;vG8yztbXUKCk=kZ;T)9OKxg5g!{AM>BUIKFJEgu?9RB$`G5gD;#%gh7@6RDLYM zMNPdnyt)pD9PI_0xPIpz`re&Fw$TlC;Mfbp^Hz}722<}d)8I}hKaL^9C*GF-Rx_o> z(8K_`Fhf?&mq%PxMN_U^EP&$+qwO}@p@q-T^D2G}(h3+u%#mhVn~-tr%P5){Kz%cG z`c2@X`ms&5b3QrRRuM*%ZL~w}YcrW;1Xb2lT3el2M2M%b4>i(7(Zm39ko9zge(ZV& zIv2&{0yAzdDnE|FdOO?b2Cb}PW^+bhBbp77i=mpv9-vGnstK+WNHPEyv=Lz*(&T8) z-6r^5HAPphRW#I7Raf-uKaAfd!lKz{0*8L^divEiz6oWe--Qm~d1>~oPHm(W(w8=`6%Ob zc35l?Xz{x@&*vy8n!L(>pZ6p9sQur96;UiF8pmHA7bgCad zC2Y+TdhOKX@7C1$JU{acI)md4wh4W7T2tqXuHHc3di~NuiBjXw?{M-l%U~7!N27)L zbvD2N2R(>MaJ({x!`YauF-I7sb(0FD%|f~t-kGh>zZox3y=d1Izr zTY|+ozHWh1KvZBcNizwqW#o(I<$2}cJB1lo@kXv4w3^PKJ^Df(0~rA|YZ%+xA)mom z(W)xAp3=W1>Ku73W*y-3*7B<6r8m=2N>%Z;kX~1Yl;gS2nel2Diu|Cat zgDt%goJVJjf@P>nY!Hl9L(F-Ig8vvhA@k_O=^9|$gzM?ZJHr6I!^5X#02}QaH!lVd zK{`H#hgU7&GzPA%XX`5*leBL)f&ZJwphlyVuYIE~&p4c_qA5eDp1OjF>TnXAQ&rcL z@Hca8W4RcPRgQ3;Zw%Dv2^nC27@)WMniB&A`0$NDK97glD#dmMZC0XIt54p}nf5E(0@5feGsX9Yb3hgDmUd z+JTMKi0{H@OuYcVoyAM>~W< z-l95xCgeN<2rYXp0UT&B|I7mw`c! z09x)^JD-Pr2y`hQ)k@SN-UNk|^Lcl^P<4Af*KBfNJqTJ>=g)*(8Nm1gGmDa-1NMdN zWZj*HuP-y=NW)E2%)LI4ndFvbAy)>_qNR~13Oe2))(qrTW*Q$$JEA6A+O7a=#gpuj zBLmp+TG685p}U}yHuXNL+#=w!Yno!LIk*s0^gNLx1L($#1v%RJdI)tU<&tK8Fl~xv zL30m=ZaG3_T{SdE2B?Oi)nT6EyoyE60}6t5|HLL>0(}&!kJ&OoZVaH+%`Bn`@TRGM z8)T9jk_;eV0;7+NC&;pFY;Fv29Sab` zw_KNNc`&2X2cc>rZ49ndW)aM+kQ)Px>(#w;#giBbqM`GQ z0y21O-JWG4HwH*Km$VB^75TFwoB$J~o~=>T*&sIth|cr8IWj=BB3Y&|Jj{fb7>t9Q zIZAn6$c+JXvZT-}+I7WHYYEj=>M+)@FUCF|evE92XXeHL!V>B!n3LE$y=z6VphFBsHD^;eGe-tE zh-u&+qt9bt6wp^-J%Zd-LWlU2)lZLRvS!b+ks|}xrfOZ{F~9`6Sv9+6BS=MXCGCuw zwLzovXxYe-0mPw<0!iCC4_BA5d3l@b5o82SzJ{}>@Q~3q3*^cG26ZM=s{~B&G03v? zrlA`_U*=vY6QqFEZjIhtC733YcJ3y%c5B^ z)4WCROUvPBIG345q0LW+%nXW^SB*h~jX|dhZC2)Ng4cT%*E78>3ff?Hl9|hmbeE24C=gu_L$h=|11z;c8{Jv zBY`|#RnN8?mi`is?UXgdxh(Kx$a_dAofS)G06Sj2JX-&!2OP4^@;yDgsWEUp>)-Fm zF-F_Y4V>4MY#Bn;O#R$ol+SUUXiTEuKQh0hE-$l~Nt`T|lLF`Q|5o)d+qvtAl)}g9 z$bDge-U+k48GwfSpj5?ld(M*XWCDjN;U;uM!L>pqF<0I6=3Eaa88N($;2s@;PK2xl zH0|>=j>2rEl;)8{F9x8o>gRTRQwftcIHa2LWgdMJT+1QXX(3d+3~l&?zE0y>YBli^V2FeZRg?EjH@N^!m;tL1zY_fd)2O#WGZ~aJaM>ksaZ5;QZ-O zb}z^rcndF7VNRVl_);2}43OkHs&^h|LCGdqY|8R#7GC-!=gcY-hx>5a@_B{iRt4M> zXr~oTlHh*^zFI#ZmRcqw8EI^F7CYGzzG@ZLEDEECE48`AOeZ@@CkCKVCSz!T32LK7 z5FEav^?y0u2p%(H)rYNy@lwdLXvy$s{-Xr%eCYJjC+=Tn(n4rkEG*&tZ2*b$y5}@y zOdo>z2FoZN<6Cb53WxPr{-8`@S)8UKS!x|>EKYr%LxI%i?dUfFj=wV-gHA(@A%jj1 z;rLd!b1MUkI!4ZOt`auHH?$S}SMJ|~I~VNv*&nP~T#QW~Z&I`A;+`h2v#%-&iAoR5RO3+$lVo<6x1jrdo?$Uyb3FO~(ar zyp`2ldS5tACM~J~T^QgMY+SfdoW)7!mT}9uP3m-E$*?ZbvhaBBV{9EG+TtkZLKiZ+ zOz*Z@qRwJZ3an0dGLu8FZ>Lk8wG5P1`g-}y8BiW-tmoSY+=rv6vXfJyRY;RUwrFGk zPG-Kni5d<`s$h5LgcCk_j&uImi{Bb+=>%}R^>G|U7Evyx3#{}ZV{d{s`R4crtpZN3 zGD5QEKp$`~$_dH^mZzI2@?AxnDcA~>!FB0yHP-oD$koCeW@M6&@_85J+I3Oz+btoB zDOo9VRh=(l()cP9IUNbF;n_SNGA1Gle%lK=9(o7l+Dyklu|5pbz{B~yvCw841BAN0 zsc2R+V1vY}MCu%PB^RBD?PH(Pt&E~@(`6mptp?=|<)CYMR;s{bV4s4qT92k=lTK9^ zU0*n67YXHiou2HQB-$7t2s28oV+9Xskn^6V(K@t}Y1F7g^)3jK+epJsQ-i9*ng~)I zN6{Byafkwp)C#L%!HGtJG|2#*RDmWWV|6 zS|M^Oxnh_|G5{T}4jJZrD_+%)Rp(Awwqn^Y*@Qu@0m6b$RW#W!VpUB;)KR1Q*fs?O zPE;)#%gcvBtfU!R=)mDe!%rPa;cy!hxZU2T774LcXnc#sLps;%SJ}j9^j3otjd2id z3=rzvXrR%=TZb}$wr&G2?0D6Whrk?T3m(xRhoMC+1TjSH!`M3J1L2wKYF3c0dPFr) zJ%Zor7FO*lzHl0x8XZJ!4Dc2x3?Z&qH3lwh&EBW+QGw7!qwWL6tbY0t)DpJQ4O&Mv ziI0J2t0~xo6P7@Zt)W8dk4+dk{IGserqKJ?qhYTYV zWs@79A=J2Z4m8oU{-KbO|fr_MotW%EM=6HHkYNN*Ay^MaKp+9PA4v>qQz>fL7B*s z_!@Io()nU#3?R7iuR^|g*3+>FbTVvuK+vnIOte4ueh#$K60$30fB+uCr=TbBQGY@2 zyudYrLiB;$>r>wk=;tr;T;nK01#Mbng zN|lPh^SIppUAYGnUne4|bHU2gz`j6DGgZt`^0pPbn_!@d9TF|z#5Gh&&FE<=RMiIU z@rHOOlVNhg+Ax5igB+%{c$xg5C9ErqYCe)q(VpH4u45vM%+hCC&_r&^m-P(S25ZFt z0d$bzX)e32kD+bZb%$f4W_r~V;u_}e8+P3WR~$Dh_e`#lbK$Be){+4N=p;j*G%K4} zk%B}1R2DwYuk2woQwj#l_||KZT}!Mr0|d}fW*I3T)R$z}+O{tpJ%z78pCv06Dz>SHW;H5i9t**a^FAu@pB)T(A8R(?(Ydnzi)kg7Vu(y-C$ zAnT67F#yA~VO2&liwUbCzw4NZE#GGRoj!;SPU3NNY!{2(1UQdcg{g0z z%RtRlRuNeU1~~%IQA+=z(@&!#d&eE}AIsxBwfwem#t=JS40Q?+nuwjZO`b5DDW>B` z!LgQR(^lXJxr@%f*!@GqAg6!;I?`q6tYoS-pnX6G(nYDand8l6G$F%4Rn4X9PFR!B`CK!wmOXt1S_VzDaN!B9gF)>xe(wl6e_a--nhJww009#y z3>rglETqM+2}TQ9iOY!H!AB`?2?hv&5v;&<7~~wUOyHZZQON2W+6QzhT}sC%*De?q bHa7kr%cHl)@0Vbf00000NkvXXu0mjfCskCG literal 0 HcmV?d00001 diff --git a/assets/images/tusd_icon.png b/assets/images/tusd_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9a51894810f028c86229ce79d0b0b5e6126260a8 GIT binary patch literal 5414 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 QTL1t607*qoM6N<$f|>h-Jpcdz literal 0 HcmV?d00001 diff --git a/assets/images/usa.png b/assets/images/usa.png deleted file mode 100644 index d67d3e2b4cf6479dcef805315e9a9a12b437ae47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 838 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%0AyKs2}WWuWr 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%0D!YUR>Q!ckOj=kug9n($mZ_U13f{TAEUxv} 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 N002ovPDHLkV1m}Ggew35 literal 0 HcmV?d00001 diff --git a/assets/images/zaddr_icon.png b/assets/images/zaddr_icon.png deleted file mode 100644 index 095ad2c560c70f6a043c2db8f652574783f86254..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6158 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 UR&#qn2mk;807*qoM6N<$f&=}9&Hw-a literal 0 HcmV?d00001 diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index d95478116..62f649704 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,3 +1,2 @@ -Enable iPad/Tablet separate layout from mobile UI -SideShift update and fixes -Bug Fixes \ No newline at end of file +Bug fixes +Fiat Onramp improvements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 38e3e21df..62f649704 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,4 +1,2 @@ -Enable iPad/Tablet separate layout from mobile UI -SideShift update and fixes -Add MoonPay sell -Bug Fixes \ No newline at end of file +Bug fixes +Fiat Onramp improvements \ No newline at end of file diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh new file mode 100644 index 000000000..b80ebc46e --- /dev/null +++ b/configure_cake_wallet_android.sh @@ -0,0 +1,10 @@ +cd scripts/android +source ./app_env.sh cakewallet +./app_config.sh +cd ../.. && flutter pub get +cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 398d68fc2..bfadaf2a3 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -52,9 +52,32 @@ class BitcoinWalletService extends WalletService< } @override - Future remove(String wallet) async => - File(await pathForWalletDir(name: wallet, type: WalletType.bitcoin)) - .delete(recursive: true); + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (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.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await BitcoinWalletBase.open( + password: password, + name: currentName, + 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( diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 70b072f7b..a05c251fe 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -66,54 +66,68 @@ class ElectrumClient { socket!.listen((Uint8List event) { try { final msg = utf8.decode(event.toList()); - final response = - json.decode(msg) as Map; - _handleResponse(response); - } on FormatException catch (e) { - final msg = e.message.toLowerCase(); - - if (e.source is String) { - unterminatedString += e.source as String; - } - - if (msg.contains("not a subtype of type")) { - unterminatedString += e.source as String; - return; - } - - if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; - _handleResponse(response); - unterminatedString = ''; - } - } on TypeError catch (e) { - if (!e.toString().contains('Map') && !e.toString().contains('Map')) { - return; - } - - final source = utf8.decode(event.toList()); - unterminatedString += source; - - if (isJSONStringCorrect(unterminatedString)) { - final response = - json.decode(unterminatedString) as Map; - _handleResponse(response); - // unterminatedString = null; - unterminatedString = ''; + final messagesList = msg.split("\n"); + for (var message in messagesList) { + if (message.isEmpty) { + continue; + } + _parseResponse(message); } } catch (e) { print(e.toString()); } }, onError: (Object error) { print(error.toString()); + unterminatedString = ''; _setIsConnected(false); }, onDone: () { + unterminatedString = ''; _setIsConnected(false); }); keepAlive(); } + void _parseResponse(String message) { + try { + final response = json.decode(message) as Map; + _handleResponse(response); + } on FormatException catch (e) { + final msg = e.message.toLowerCase(); + + if (e.source is String) { + unterminatedString += e.source as String; + } + + if (msg.contains("not a subtype of type")) { + unterminatedString += e.source as String; + return; + } + + if (isJSONStringCorrect(unterminatedString)) { + final response = + json.decode(unterminatedString) as Map; + _handleResponse(response); + unterminatedString = ''; + } + } on TypeError catch (e) { + if (!e.toString().contains('Map') && !e.toString().contains('Map')) { + return; + } + + unterminatedString += message; + + if (isJSONStringCorrect(unterminatedString)) { + final response = + json.decode(unterminatedString) as Map; + _handleResponse(response); + // unterminatedString = null; + unterminatedString = ''; + } + } catch (e) { + print(e.toString()); + } + } + void keepAlive() { _aliveTimer?.cancel(); _aliveTimer = Timer.periodic(aliveTimerDuration, (_) async => ping()); @@ -217,7 +231,7 @@ class ElectrumClient { Future> getTransactionRaw( {required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash, true]) + callWithTimeout(method: 'blockchain.transaction.get', params: [hash, true], timeout: 10000) .then((dynamic result) { if (result is Map) { return result; @@ -228,7 +242,7 @@ class ElectrumClient { Future getTransactionHex( {required String hash}) async => - call(method: 'blockchain.transaction.get', params: [hash, false]) + callWithTimeout(method: 'blockchain.transaction.get', params: [hash, false], timeout: 10000) .then((dynamic result) { if (result is String) { return result; diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 8720dd82c..be039fa36 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_bitcoin/file.dart'; @@ -9,7 +8,7 @@ import 'package:cw_bitcoin/electrum_transaction_info.dart'; part 'electrum_transaction_history.g.dart'; -const _transactionsHistoryFileName = 'transactions.json'; +const transactionsHistoryFileName = 'transactions.json'; class ElectrumTransactionHistory = ElectrumTransactionHistoryBase with _$ElectrumTransactionHistory; @@ -42,7 +41,7 @@ abstract class ElectrumTransactionHistoryBase try { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$_transactionsHistoryFileName'; + final path = '$dirPath/$transactionsHistoryFileName'; final data = json.encode({'height': _height, 'transactions': transactions}); await writeData(path: path, password: _password, data: data); @@ -59,7 +58,7 @@ abstract class ElectrumTransactionHistoryBase Future> _read() async { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$_transactionsHistoryFileName'; + final path = '$dirPath/$transactionsHistoryFileName'; final content = await read(path: path, password: _password); return json.decode(content) as Map; } @@ -67,7 +66,7 @@ abstract class ElectrumTransactionHistoryBase Future _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map ?? {}; + final txs = content['transactions'] as Map? ?? {}; txs.entries.forEach((entry) { final val = entry.value; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b034c06b1..bf5ec2c4f 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; @@ -217,9 +216,9 @@ class ElectrumTransactionInfo extends TransactionInfo { height: info.height, amount: info.amount, fee: info.fee, - direction: direction ?? info.direction, - date: date ?? info.date, - isPending: isPending ?? info.isPending, + direction: direction, + date: date, + isPending: isPending, confirmations: info.confirmations); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 3f953b8e1..f9437e668 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; @@ -430,6 +431,29 @@ abstract class ElectrumWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = + await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + @override Future changePassword(String password) async { _password = password; diff --git a/cw_bitcoin/lib/litecoin_wallet_service.dart b/cw_bitcoin/lib/litecoin_wallet_service.dart index 2093647fd..b13ac7a7f 100644 --- a/cw_bitcoin/lib/litecoin_wallet_service.dart +++ b/cw_bitcoin/lib/litecoin_wallet_service.dart @@ -53,9 +53,32 @@ class LitecoinWalletService extends WalletService< } @override - Future remove(String wallet) async => - File(await pathForWalletDir(name: wallet, type: getType())) - .delete(recursive: true); + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (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.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(currentName, getType()))!; + final currentWallet = await LitecoinWalletBase.open( + password: password, + name: currentName, + 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( diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4d864059f..bfcd9e5a6 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -746,5 +746,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.0.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 455ceb4a7..481a41ac5 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: unorm_dart: ^0.2.0 cryptography: ^2.0.5 encrypt: ^5.0.1 - + dev_dependencies: flutter_test: sdk: flutter diff --git a/cw_core/lib/cake_hive.dart b/cw_core/lib/cake_hive.dart new file mode 100644 index 000000000..aadf6bf9a --- /dev/null +++ b/cw_core/lib/cake_hive.dart @@ -0,0 +1,4 @@ +import 'package:hive/hive.dart'; +import 'package:hive/src/hive_impl.dart'; + +final HiveInterface CakeHive = HiveImpl(); diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index da9b7f9e9..86ea3f214 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -67,6 +67,29 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.uni, CryptoCurrency.stx, CryptoCurrency.btcln, + CryptoCurrency.shib, + CryptoCurrency.aave, + CryptoCurrency.arb, + CryptoCurrency.bat, + CryptoCurrency.comp, + CryptoCurrency.cro, + CryptoCurrency.ens, + CryptoCurrency.ftm, + CryptoCurrency.frax, + CryptoCurrency.gusd, + CryptoCurrency.gtc, + CryptoCurrency.grt, + CryptoCurrency.ldo, + CryptoCurrency.nexo, + CryptoCurrency.cake, + CryptoCurrency.pepe, + CryptoCurrency.storj, + CryptoCurrency.tusd, + CryptoCurrency.wbtc, + CryptoCurrency.weth, + CryptoCurrency.zrx, + CryptoCurrency.dydx, + CryptoCurrency.steth, ]; static const havenCurrencies = [ @@ -121,7 +144,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png'); static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png'); static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png'); - static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/bttbsc_icon.png'); + static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/btt_icon.png'); static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png'); static const firo = CryptoCurrency(title: 'FIRO', raw: 35, name: 'firo', iconPath: 'assets/images/firo_icon.png'); static const usdttrc20 = CryptoCurrency(title: 'USDT', tag: 'TRX', fullName: 'USDT Tether', raw: 36, name: 'usdttrc20', iconPath: 'assets/images/usdttrc20_icon.png'); @@ -129,9 +152,9 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const sc = CryptoCurrency(title: 'SC', fullName: 'Siacoin', raw: 38, name: 'sc', iconPath: 'assets/images/sc_icon.png'); static const sol = CryptoCurrency(title: 'SOL', fullName: 'Solana', raw: 39, name: 'sol', iconPath: 'assets/images/sol_icon.png'); static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/usdc_icon.png'); - static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdcsol_icon.png'); - static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', iconPath: 'assets/images/zaddr_icon.png', raw: 42, name: 'zaddr'); - static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', iconPath: 'assets/images/zec_icon.png', raw: 43, name: 'zec'); + static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/usdc_icon.png'); + static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png'); + static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png'); static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png'); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png'); @@ -152,6 +175,29 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const uni = CryptoCurrency(title: 'UNI', tag: 'ETH', fullName: 'Uniswap', raw: 60, name: 'uni', iconPath: 'assets/images/uni_icon.png'); static const stx = CryptoCurrency(title: 'STX', fullName: 'Stacks', raw: 61, name: 'stx', iconPath: 'assets/images/stx_icon.png'); static const btcln = CryptoCurrency(title: 'BTC', tag: 'LN', fullName: 'Bitcoin Lightning Network', raw: 62, name: 'btcln', iconPath: 'assets/images/btc.png'); + static const shib = CryptoCurrency(title: 'SHIB', tag: 'ETH', fullName: 'Shiba Inu', raw: 63, name: 'shib', iconPath: 'assets/images/shib_icon.png'); + static const aave = CryptoCurrency(title: 'AAVE', tag: 'ETH', fullName: 'Aave', raw: 64, name: 'aave', iconPath: 'assets/images/aave_icon.png'); + static const arb = CryptoCurrency(title: 'ARB', fullName: 'Arbitrum', raw: 65, name: 'arb', iconPath: 'assets/images/arb_icon.png'); + static const bat = CryptoCurrency(title: 'BAT', tag: 'ETH', fullName: 'Basic Attention Token', raw: 66, name: 'bat', iconPath: 'assets/images/bat_icon.png'); + static const comp = CryptoCurrency(title: 'COMP', tag: 'ETH', fullName: 'Compound', raw: 67, name: 'comp', iconPath: 'assets/images/comp_icon.png'); + static const cro = CryptoCurrency(title: 'CRO', tag: 'ETH', fullName: 'Crypto.com Cronos', raw: 68, name: 'cro', iconPath: 'assets/images/cro_icon.png'); + static const ens = CryptoCurrency(title: 'ENS', tag: 'ETH', fullName: 'Ethereum Name Service', raw: 69, name: 'ens', iconPath: 'assets/images/ens_icon.png'); + static const ftm = CryptoCurrency(title: 'FTM', tag: 'ETH', fullName: 'Fantom', raw: 70, name: 'ftm', iconPath: 'assets/images/ftm_icon.png'); + static const frax = CryptoCurrency(title: 'FRAX', tag: 'ETH', fullName: 'Frax', raw: 71, name: 'frax', iconPath: 'assets/images/frax_icon.png'); + static const gusd = CryptoCurrency(title: 'GUSD', tag: 'ETH', fullName: 'Gemini USD', raw: 72, name: 'gusd', iconPath: 'assets/images/gusd_icon.png'); + static const gtc = CryptoCurrency(title: 'GTC', tag: 'ETH', fullName: 'Gitcoin', raw: 73, name: 'gtc', iconPath: 'assets/images/gtc_icon.png'); + static const grt = CryptoCurrency(title: 'GRT', tag: 'ETH', fullName: 'The Graph', raw: 74, name: 'grt', iconPath: 'assets/images/grt_icon.png'); + static const ldo = CryptoCurrency(title: 'LDO', tag: 'ETH', fullName: 'Lido DAO', raw: 75, name: 'ldo', iconPath: 'assets/images/ldo_icon.png'); + static const nexo = CryptoCurrency(title: 'NEXO', tag: 'ETH', fullName: 'Nexo', raw: 76, name: 'nexo', iconPath: 'assets/images/nexo_icon.png'); + static const cake = CryptoCurrency(title: 'CAKE', tag: 'BSC', fullName: 'PancakeSwap', raw: 77, name: 'cake', iconPath: 'assets/images/cake_icon.png'); + static const pepe = CryptoCurrency(title: 'PEPE', tag: 'ETH', fullName: 'Pepe', raw: 78, name: 'pepe', iconPath: 'assets/images/pepe_icon.png'); + static const storj = CryptoCurrency(title: 'STORJ', tag: 'ETH', fullName: 'Storj', raw: 79, name: 'storj', iconPath: 'assets/images/storj_icon.png'); + static const tusd = CryptoCurrency(title: 'TUSD', tag: 'ETH', fullName: 'TrueUSD', raw: 80, name: 'tusd', iconPath: 'assets/images/tusd_icon.png'); + static const wbtc = CryptoCurrency(title: 'WBTC', tag: 'ETH', fullName: 'Wrapped Bitcoin', raw: 81, name: 'wbtc', iconPath: 'assets/images/wbtc_icon.png'); + static const weth = CryptoCurrency(title: 'WETH', tag: 'ETH', fullName: 'Wrapped Ethereum', raw: 82, name: 'weth', iconPath: 'assets/images/weth_icon.png'); + static const zrx = CryptoCurrency(title: 'ZRX', tag: 'ETH', fullName: '0x Protocol', raw: 83, name: 'zrx', iconPath: 'assets/images/zrx_icon.png'); + static const dydx = CryptoCurrency(title: 'DYDX', tag: 'ETH', fullName: 'dYdX', raw: 84, name: 'dydx', iconPath: 'assets/images/dydx_icon.png'); + static const steth = CryptoCurrency(title: 'STETH', tag: 'ETH', fullName: 'Lido Staked Ethereum', raw: 85, name: 'steth', iconPath: 'assets/images/steth_icon.png'); 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 3904fc049..8ac8c1fc6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart new file mode 100644 index 000000000..fd27aaba6 --- /dev/null +++ b/cw_core/lib/erc20_token.dart @@ -0,0 +1,66 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'erc20_token.g.dart'; + +@HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + @HiveField(1) + final String symbol; + @HiveField(2) + final String contractAddress; + @HiveField(3) + final int decimal; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: "ETH", + iconPath: iconPath, + ); + + Erc20Token.copyWith(Erc20Token other, String? icon) + : this.name = other.name, + this.symbol = other.symbol, + this.contractAddress = other.contractAddress, + this.decimal = other.decimal, + this._enabled = other.enabled, + this.iconPath = icon, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: "ETH", + iconPath: icon, + ); + + static const typeId = ERC20_TOKEN_TYPE_ID; + static const boxName = 'Erc20Tokens'; + + @override + bool operator ==(other) => (other is Erc20Token && other.contractAddress == contractAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart new file mode 100644 index 000000000..0961182bc --- /dev/null +++ b/cw_core/lib/hive_type_ids.dart @@ -0,0 +1,13 @@ +const CONTACT_TYPE_ID = 0; +const NODE_TYPE_ID = 1; +const TRANSACTION_TYPE_ID = 2; +const TRADE_TYPE_ID = 3; +const WALLET_INFO_TYPE_ID = 4; +const WALLET_TYPE_TYPE_ID = 5; +const TEMPLATE_TYPE_ID = 6; +const EXCHANGE_TEMPLATE_TYPE_ID = 7; +const ORDER_TYPE_ID = 8; +const UNSPENT_COINS_INFO_TYPE_ID = 9; +const ANONPAY_INVOICE_INFO_TYPE_ID = 10; + +const ERC20_TOKEN_TYPE_ID = 12; diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 7df25d6a1..59a1450f6 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -3,6 +3,7 @@ import 'package:cw_core/keyable.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; 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; @@ -18,6 +19,7 @@ class Node extends HiveObject with Keyable { this.password, this.useSSL, this.trusted = false, + this.socksProxyAddress, String? uri, WalletType? type,}) { if (uri != null) { @@ -33,9 +35,10 @@ class Node extends HiveObject with Keyable { login = map['login'] as String?, password = map['password'] as String?, useSSL = map['useSSL'] as bool?, - trusted = map['trusted'] as bool? ?? false; + trusted = map['trusted'] as bool? ?? false, + socksProxyAddress = map['socksProxyPort'] as String?; - static const typeId = 1; + static const typeId = NODE_TYPE_ID; static const boxName = 'Nodes'; @HiveField(0, defaultValue: '') @@ -56,8 +59,13 @@ class Node extends HiveObject with Keyable { @HiveField(5, defaultValue: false) bool trusted; + @HiveField(6) + String? socksProxyAddress; + bool get isSSL => useSSL ?? false; + bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty; + Uri get uri { switch (type) { case WalletType.monero: @@ -68,6 +76,8 @@ class Node extends HiveObject with Keyable { return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); + case WalletType.ethereum: + return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -81,7 +91,8 @@ class Node extends HiveObject with Keyable { other.password == password && other.typeRaw == typeRaw && other.useSSL == useSSL && - other.trusted == trusted); + other.trusted == trusted && + other.socksProxyAddress == socksProxyAddress); @override int get hashCode => @@ -90,7 +101,8 @@ class Node extends HiveObject with Keyable { password.hashCode ^ typeRaw.hashCode ^ useSSL.hashCode ^ - trusted.hashCode; + trusted.hashCode ^ + socksProxyAddress.hashCode; @override dynamic get keyIndex { @@ -108,13 +120,15 @@ class Node extends HiveObject with Keyable { try { switch (type) { case WalletType.monero: - return requestMoneroNode(); + return useSocksProxy ? requestNodeWithProxy(socksProxyAddress ?? '') : requestMoneroNode(); case WalletType.bitcoin: return requestElectrumServer(); case WalletType.litecoin: return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); + case WalletType.ethereum: + return requestElectrumServer(); default: return false; } @@ -157,7 +171,23 @@ class Node extends HiveObject with Keyable { } catch (_) { return false; } -} + } + + Future requestNodeWithProxy(String proxy) async { + + if (proxy.isEmpty || !proxy.contains(':')) { + return false; + } + final proxyAddress = proxy.split(':')[0]; + final proxyPort = int.parse(proxy.split(':')[1]); + try { + final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } Future requestElectrumServer() async { try { @@ -168,4 +198,17 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } } diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart index 09e01556c..84096e2d6 100644 --- a/cw_core/lib/set_app_secure_native.dart +++ b/cw_core/lib/set_app_secure_native.dart @@ -1,7 +1,9 @@ import 'package:flutter/services.dart'; -const utils = const MethodChannel('com.cake_wallet/native_utils'); - void setIsAppSecureNative(bool isAppSecure) { - utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); -} \ No newline at end of file + try { + final utils = const MethodChannel('com.cake_wallet/native_utils'); + + utils.invokeMethod('setIsAppSecure', {'isAppSecure': isAppSecure}); + } catch (_) {} +} diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 75c13f2cd..33be2eb2c 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -1,3 +1,4 @@ +import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; part 'unspent_coins_info.g.dart'; @@ -14,7 +15,7 @@ class UnspentCoinsInfo extends HiveObject { required this.vout, required this.value}); - static const typeId = 9; + static const typeId = UNSPENT_COINS_INFO_TYPE_ID; static const boxName = 'Unspent'; static const boxKey = 'unspentBoxKey'; @@ -45,4 +46,4 @@ class UnspentCoinsInfo extends HiveObject { String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; -} \ No newline at end of file +} diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index e5f84f467..019f87631 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -75,4 +75,6 @@ abstract class WalletBase< Future? updateBalance(); void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; + + Future renameWalletFiles(String newWalletName); } diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index a25702cf7..6b3fa9e98 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,7 +1,7 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_core/wallet_type.dart'; import 'dart:async'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; part 'wallet_info.g.dart'; @@ -30,7 +30,7 @@ class WalletInfo extends HiveObject { yatEid, yatLastUsedAddressRaw, showIntroCakePayCard); } - static const typeId = 4; + static const typeId = WALLET_INFO_TYPE_ID; static const boxName = 'WalletInfo'; @HiveField(0, defaultValue: '') diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index f66f39583..f95bc1a44 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -17,4 +17,6 @@ abstract class WalletService isWalletExit(String name); Future remove(String wallet); + + Future rename(String currentName, String password, String newName); } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 61a571fcf..62c2ad410 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; part 'wallet_type.g.dart'; @@ -7,11 +8,11 @@ const walletTypes = [ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, - WalletType.haven + WalletType.haven, + WalletType.ethereum, ]; -const walletTypeTypeId = 5; -@HiveType(typeId: walletTypeTypeId) +@HiveType(typeId: WALLET_TYPE_TYPE_ID) enum WalletType { @HiveField(0) monero, @@ -27,6 +28,9 @@ enum WalletType { @HiveField(4) haven, + + @HiveField(5) + ethereum, } int serializeToInt(WalletType type) { @@ -39,6 +43,8 @@ int serializeToInt(WalletType type) { return 2; case WalletType.haven: return 3; + case WalletType.ethereum: + return 4; default: return -1; } @@ -54,6 +60,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.litecoin; case 3: return WalletType.haven; + case 4: + return WalletType.ethereum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -69,6 +77,8 @@ String walletTypeToString(WalletType type) { return 'Litecoin'; case WalletType.haven: return 'Haven'; + case WalletType.ethereum: + return 'Ethereum'; default: return ''; } @@ -84,6 +94,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Litecoin (LTC)'; case WalletType.haven: return 'Haven (XHV)'; + case WalletType.ethereum: + return 'Ethereum (ETH)'; default: return ''; } @@ -99,6 +111,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 70652ec35..01e19dda4 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -665,5 +665,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.0.0" diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_ethereum/.gitignore @@ -0,0 +1,30 @@ +# 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 +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_ethereum/.metadata b/cw_ethereum/.metadata new file mode 100644 index 000000000..1e05dac7f --- /dev/null +++ b/cw_ethereum/.metadata @@ -0,0 +1,10 @@ +# 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 and should not be manually edited. + +version: + revision: eb6d86ee27deecba4a83536aa20f366a6044895c + channel: stable + +project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_ethereum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_ethereum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_ethereum/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_ethereum/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_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart new file mode 100644 index 000000000..af9ea7ee0 --- /dev/null +++ b/cw_ethereum/lib/cw_ethereum.dart @@ -0,0 +1,7 @@ +library cw_ethereum; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_ethereum/lib/default_erc20_tokens.dart b/cw_ethereum/lib/default_erc20_tokens.dart new file mode 100644 index 000000000..8c38e2e64 --- /dev/null +++ b/cw_ethereum/lib/default_erc20_tokens.dart @@ -0,0 +1,309 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Gitcoin", + symbol: "GTC", + contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Compound", + symbol: "COMP", + contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Aave Token", + symbol: "AAVE", + contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap", + symbol: "UNI", + contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentraland", + symbol: "MANA", + contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Storj", + symbol: "STORJ", + contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Maker", + symbol: "MKR", + contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Orchid", + symbol: "OXT", + contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Paxos Gold", + symbol: "PAXG", + contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Binance Coin", + symbol: "BNB", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "stETH", + symbol: "stETH", + contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Lido DAO", + symbol: "LDO", + contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Graph Token", + symbol: "GRT", + contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Frax", + symbol: "FRAX", + contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Gemini dollar", + symbol: "GUSD", + contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + decimal: 2, + enabled: false, + ), + Erc20Token( + name: "Compound Ether", + symbol: "cETH", + contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Binance USD", + symbol: "BUSD", + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "TrueUSD", + symbol: "TUSD", + contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Cronos Coin", + symbol: "CRO", + contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Pax Dollar", + symbol: "USDP", + contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Fantom Token", + symbol: "FTM", + contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BitTorrent", + symbol: "BTT", + contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Nexo", + symbol: "NEXO", + contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "dYdX", + symbol: "DYDX", + contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PancakeSwap Token", + symbol: "Cake", + contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BAT", + symbol: "BAT", + contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "1INCH Token", + symbol: "1INCH", + contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Ethereum Name Service", + symbol: "ENS", + contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ZRX", + symbol: "ZRX", + contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Verse", + symbol: "VERSE", + contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PayPal USD", + symbol: "PYUSD", + contractAddress: "0x6c3ea9036406852006290770bedfcaba0e23a0e8", + decimal: 6, + enabled: false, + ), + ]; + + List get initialErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + if (iconPath != null) { + return Erc20Token.copyWith(token, iconPath); + } + + return token; + }).toList(); +} diff --git a/cw_ethereum/lib/erc20_balance.dart b/cw_ethereum/lib/erc20_balance.dart new file mode 100644 index 000000000..7d11f8e45 --- /dev/null +++ b/cw_ethereum/lib/erc20_balance.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/balance.dart'; + +class ERC20Balance extends Balance { + ERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), + balance.toInt()); + + final BigInt balance; + final int exponent; + + @override + String get formattedAdditionalBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + @override + String get formattedAvailableBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + String toJSON() => json.encode({ + 'balanceInWei': balance.toString(), + 'exponent': exponent, + }); + + static ERC20Balance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return ERC20Balance( + BigInt.parse(decoded['balanceInWei']), + exponent: decoded['exponent'], + ); + } catch (e) { + return ERC20Balance(BigInt.zero); + } + } +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..f00e2ef7b --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/pending_ethereum_transaction.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/contracts/erc20.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/.secrets.g.dart' as secrets; + +class EthereumClient { + final _httpClient = Client(); + Web3Client? _client; + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), _httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future getBalance(EthereumAddress address) async => + await _client!.getBalance(address); + + Future getGasUnitPrice() async { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } + + Future getEstimatedGas() async { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } + + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EthereumTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + }) async { + assert(currency == CryptoCurrency.eth || contractAddress != null); + + bool _isEthereum = currency == CryptoCurrency.eth; + + final price = await _client!.getGasPrice(); + + final Transaction transaction = Transaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxGas: gas, + gasPrice: price, + maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), + value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + ); + + final signedTransaction = await _client!.signTransaction(privateKey, transaction); + + final Function _sendTransaction; + + if (_isEthereum) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = Erc20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + BigInt.parse(amount), + credentials: privateKey, + ); + }; + } + + return PendingEthereumTransaction( + signedTransaction: signedTransaction, + amount: amount, + fee: BigInt.from(gas) * price.getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Future sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(signedTransaction); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + + // Print the receipt information + print('Transaction Hash: ${receipt.transactionHash}'); + print('Block Hash: ${receipt.blockHash}'); + print('Block Number: ${receipt.blockNumber}'); + print('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] +I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + print("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + // Print the receipt information + print('Transaction Hash: ${transactionInformation.hash}'); + print('Block Hash: ${transactionInformation.blockHash}'); + print('Block Number: ${transactionInformation.blockNumber}'); + print('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 +I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return ERC20Balance(balance, exponent: exponent); + } + + Future getErc20Token(String contractAddress) async { + try { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + void stop() { + _client?.dispose(); + } + + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final _jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) { + return (_jsonResponse['result'] as List) + .map((e) => EthereumTransactionModel.fromJson(e as Map)) + .toList(); + } + + return []; + } catch (e) { + print(e); + return []; + } + } + +// Future _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_ethereum/lib/ethereum_exceptions.dart b/cw_ethereum/lib/ethereum_exceptions.dart new file mode 100644 index 000000000..518f46275 --- /dev/null +++ b/cw_ethereum/lib/ethereum_exceptions.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EthereumTransactionCreationException implements Exception { + final String exceptionMessage; + + EthereumTransactionCreationException(CryptoCurrency currency) : + this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_ethereum/lib/ethereum_formatter.dart b/cw_ethereum/lib/ethereum_formatter.dart new file mode 100644 index 000000000..468c536f8 --- /dev/null +++ b/cw_ethereum/lib/ethereum_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const ethereumAmountLength = 12; +const ethereumAmountDivider = 1000000000000; +final ethereumAmountFormat = NumberFormat() + ..maximumFractionDigits = ethereumAmountLength + ..minimumFractionDigits = 1; + +class EthereumFormatter { + static int parseEthereumAmount(String amount) { + try { + return (double.parse(amount) * ethereumAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEthereumAmountToDouble(int amount) { + try { + return amount / ethereumAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics.dart b/cw_ethereum/lib/ethereum_mnemonics.dart new file mode 100644 index 000000000..8af7b10f3 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics.dart @@ -0,0 +1,2058 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class EthereumMnemonics { + static const englishWordlist = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' + ]; +} diff --git a/cw_ethereum/lib/ethereum_transaction_credentials.dart b/cw_ethereum/lib/ethereum_transaction_credentials.dart new file mode 100644 index 000000000..b015b7141 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_credentials.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + +class EthereumTransactionCredentials { + EthereumTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); + + final List outputs; + final EthereumTransactionPriority? priority; + final int? feeRate; + final CryptoCurrency currency; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart new file mode 100644 index 000000000..4511f4436 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; + +part 'ethereum_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'transactions.json'; + +class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; + +abstract class EthereumTransactionHistoryBase + extends TransactionHistoryBase with Store { + EthereumTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + print('Error while save ethereum transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = EthereumTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart new file mode 100644 index 000000000..efdc61407 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class EthereumTransactionInfo extends TransactionInfo { + EthereumTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + this.tokenSymbol = "ETH", + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + }) : this.amount = ethAmount.toInt(), + this.fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH'; + + factory EthereumTransactionInfo.fromJson(Map data) { + return EthereumTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + }; +} diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_ethereum/lib/ethereum_transaction_model.dart new file mode 100644 index 000000000..c1260795a --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_model.dart @@ -0,0 +1,47 @@ +class EthereumTransactionModel { + final DateTime date; + final String hash; + final String from; + final String to; + final BigInt amount; + final int gasUsed; + final BigInt gasPrice; + final String contractAddress; + final int confirmations; + final int blockNumber; + final String? tokenSymbol; + final int? tokenDecimal; + final bool isError; + + EthereumTransactionModel({ + required this.date, + required this.hash, + required this.from, + required this.to, + required this.amount, + required this.gasUsed, + required this.gasPrice, + required this.contractAddress, + required this.confirmations, + required this.blockNumber, + required this.tokenSymbol, + required this.tokenDecimal, + required this.isError, + }); + + factory EthereumTransactionModel.fromJson(Map json) => EthereumTransactionModel( + date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), + hash: json["hash"], + from: json["from"], + to: json["to"], + amount: BigInt.parse(json["value"]), + gasUsed: int.parse(json["gasUsed"]), + gasPrice: BigInt.parse(json["gasPrice"]), + contractAddress: json["contractAddress"], + confirmations: int.parse(json["confirmations"]), + blockNumber: int.parse(json["blockNumber"]), + tokenSymbol: json["tokenSymbol"] ?? "ETH", + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_ethereum/lib/ethereum_transaction_priority.dart b/cw_ethereum/lib/ethereum_transaction_priority.dart new file mode 100644 index 000000000..ff5668397 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EthereumTransactionPriority extends TransactionPriority { + final int tip; + + const EthereumTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const EthereumTransactionPriority slow = + EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EthereumTransactionPriority medium = + EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EthereumTransactionPriority fast = + EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EthereumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EthereumTransactionPriority.slow: + label = 'Slow'; + break; + case EthereumTransactionPriority.medium: + label = 'Medium'; + break; + case EthereumTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart new file mode 100644 index 000000000..404b78ca2 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -0,0 +1,486 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/cake_hive.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/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/default_erc20_tokens.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_exceptions.dart'; +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_ethereum/ethereum_transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:hive/hive.dart'; +import 'package:hex/hex.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; + +part 'ethereum_wallet.g.dart'; + +class EthereumWallet = EthereumWalletBase with _$EthereumWallet; + +abstract class EthereumWalletBase + extends WalletBase + with Store { + EthereumWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + ERC20Balance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _isTransactionUpdating = false, + _client = EthereumClient(), + walletAddresses = EthereumWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { + CakeHive.registerAdapter(Erc20TokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _mnemonic; + final String _password; + + late final Box erc20TokensBox; + + late final EthPrivateKey _privateKey; + + late EthereumClient _client; + + int? _gasPrice; + int? _estimatedGas; + bool _isTransactionUpdating; + + // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer _sharedPrefs = Completer(); + + Future init() async { + erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); + await walletAddresses.init(); + await transactionHistory.init(); + _privateKey = await getPrivateKey(_mnemonic, _password); + walletAddresses.address = _privateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EthereumTransactionPriority) { + final priorityFee = + EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Ethereum Node connection failed"); + } + + _client.setListeners(_privateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as EthereumTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == _credentials.currency.title); + + final _erc20Balance = balance[transactionCurrency]!; + BigInt totalAmount = BigInt.zero; + int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; + num amountToEthereumMultiplier = pow(10, exponent); + + // so far this can not be made with Ethereum as Ethereum does not support multiple recipients + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EthereumTransactionCreationException(transactionCurrency); + } + + final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + // since the fees are taken from Ethereum + // then no need to subtract the fees from the amount if send all + final BigInt allAmount; + if (transactionCurrency is Erc20Token) { + allAmount = _erc20Balance.balance; + } else { + allAmount = _erc20Balance.balance - + BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + } + final totalOriginalAmount = + EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(transactionCurrency); + } + } + + final pendingEthereumTransaction = await _client.signTransaction( + privateKey: _privateKey, + toAddress: _credentials.outputs.first.isParsedAddress + ? _credentials.outputs.first.extractedAddress! + : _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: transactionCurrency, + exponent: exponent, + contractAddress: + transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, + ); + + return pendingEthereumTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true; + if (!isEtherscanEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _privateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + ); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String get seed => _mnemonic; + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await _updateTransactions(); + _gasPrice = await _client.getGasUnitPrice(); + _estimatedGas = await _client.getEstimatedGas(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); + Timer.periodic(const Duration(seconds: 10), + (timer) async => _estimatedGas = await _client.getEstimatedGas()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'balance': balance[currency]!.toJSON(), + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; + final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); + + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchEthBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchEthBalance() async { + final balance = await _client.getBalance(_privateKey.address); + return ERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in erc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _privateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future getPrivateKey(String mnemonic, String password) async { + final seed = bip39.mnemonicToSeed(mnemonic); + + final root = bip32.BIP32.fromSeed(seed); + + const _hdPathEthereum = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$_hdPathEthereum/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => erc20TokensBox.values.toList(); + + Future addErc20Token(Erc20Token token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final _token = Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + iconPath: iconPath, + ); + + await erc20TokensBox.put(_token.contractAddress, _token); + + if (_token.enabled) { + balance[_token] = await _client.fetchERC20Balances( + _privateKey.address, + _token.contractAddress, + ); + } else { + balance.remove(_token); + } + } + + Future deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future getErc20Token(String contractAddress) async => + await _client.getErc20Token(contractAddress); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + void addInitialTokens() { + final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens; + + initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token)); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(Duration(seconds: 10), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + void updateEtherscanUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_ethereum/lib/ethereum_wallet_addresses.dart new file mode 100644 index 000000000..4a3492e6f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'ethereum_wallet_addresses.g.dart'; + +class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; + +abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { + EthereumWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart new file mode 100644 index 000000000..12d0d53e2 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EthereumNewWalletCredentials extends WalletCredentials { + EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { + EthereumRestoreWalletFromSeedCredentials( + {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials { + EthereumRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart new file mode 100644 index 000000000..318f287fc --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.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_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; + +class EthereumWalletService extends WalletService { + EthereumWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(EthereumNewWalletCredentials credentials) async { + final mnemonic = bip39.generateMnemonic(); + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + WalletType getType() => WalletType.ethereum; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await EthereumWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromKeys(credentials) { + throw UnimplementedError(); + } + + @override + Future restoreFromSeed( + EthereumRestoreWalletFromSeedCredentials credentials) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = EthereumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_ethereum/lib/file.dart b/cw_ethereum/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_ethereum/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_ethereum/lib/pending_ethereum_transaction.dart b/cw_ethereum/lib/pending_ethereum_transaction.dart new file mode 100644 index 000000000..35b0123cc --- /dev/null +++ b/cw_ethereum/lib/pending_ethereum_transaction.dart @@ -0,0 +1,42 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingEthereumTransaction with PendingTransaction { + final Function sendTransaction; + final Uint8List signedTransaction; + final BigInt fee; + final String amount; + final int exponent; + + PendingEthereumTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + required this.exponent, + }); + + @override + String get amountFormatted { + final _amount = BigInt.parse(amount) / BigInt.from(pow(10, exponent)); + return _amount.toStringAsFixed(min(15, _amount.toString().length)); + } + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted { + final _fee = fee / BigInt.from(pow(10, 18)); + return _fee.toStringAsFixed(min(15, _fee.toString().length)); + } + + @override + String get hex => bytesToHex(signedTransaction, include0x: true); + + @override + String get id => ''; +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml new file mode 100644 index 000000000..cb1046d5a --- /dev/null +++ b/cw_ethereum/pubspec.yaml @@ -0,0 +1,68 @@ +name: cw_ethereum +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: 2.3.5 + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^0.13.4 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# 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: + + # To add assets to your 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 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/cw_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart new file mode 100644 index 000000000..72026a4c0 --- /dev/null +++ b/cw_ethereum/test/cw_ethereum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_ethereum/cw_ethereum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart index 31c911edc..ae95b62dd 100644 --- a/cw_haven/lib/api/signatures.dart +++ b/cw_haven/lib/api/signatures.dart @@ -39,7 +39,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); typedef start_refresh = Void Function(); diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart index de9ff74a0..8c9dfdab2 100644 --- a/cw_haven/lib/api/types.dart +++ b/cw_haven/lib/api/types.dart @@ -39,7 +39,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); typedef StartRefresh = void Function(); diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart index bdf6a1af7..e6b75c0cc 100644 --- a/cw_haven/lib/api/wallet.dart +++ b/cw_haven/lib/api/wallet.dart @@ -154,9 +154,11 @@ bool setupNodeSync( String? login, String? password, bool useSSL = false, - bool isLightWallet = false}) { + bool isLightWallet = false, + String? socksProxyAddress}) { final addressPointer = address.toNativeUtf8(); Pointer? loginPointer; + Pointer? socksProxyAddressPointer; Pointer? passwordPointer; if (login != null) { @@ -167,6 +169,10 @@ bool setupNodeSync( passwordPointer = password.toNativeUtf8(); } + if (socksProxyAddress != null) { + socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); + } + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, @@ -174,6 +180,7 @@ bool setupNodeSync( passwordPointer, _boolToInt(useSSL), _boolToInt(isLightWallet), + socksProxyAddressPointer, errorMessagePointer) != 0; @@ -323,13 +330,15 @@ bool _setupNodeSync(Map args) { 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); + isLightWallet: isLightWallet, + socksProxyAddress: socksProxyAddress); } bool _isConnected(Object _) => isConnectedSync(); @@ -343,13 +352,15 @@ Future setupNode( String? login, String? password, bool useSSL = false, + String? socksProxyAddress, bool isLightWallet = false}) => compute, void>(_setupNodeSync, { 'address': address, 'login': login, 'password': password, 'useSSL': useSSL, - 'isLightWallet': isLightWallet + 'isLightWallet': isLightWallet, + 'socksProxyAddress': socksProxyAddress }); Future store() => compute(_storeSync, 0); diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 2a72f078f..226ace6a1 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:io'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_haven/haven_transaction_creation_credentials.dart'; import 'package:cw_core/monero_amount_format.dart'; @@ -123,7 +125,8 @@ abstract class HavenWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { + 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(currentWalletPath).delete(recursive: true); + } + @override Future changePassword(String password) async { haven_wallet.setPasswordSync(password); diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart index 137ade78f..0bc20d2a0 100644 --- a/cw_haven/lib/haven_wallet_service.dart +++ b/cw_haven/lib/haven_wallet_service.dart @@ -149,6 +149,26 @@ class HavenWalletService extends WalletService< 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 = HavenWallet(walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @override diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index 19d9cef8f..f4c36e69c 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -399,5 +399,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=2.18.1 <4.0.0" + dart: ">=2.18.1 <3.0.0" flutter: ">=3.0.0" diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 117214295..780fc5b14 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include "thread" @@ -405,13 +406,14 @@ extern "C" return is_connected; } - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + 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) { @@ -423,7 +425,12 @@ extern "C" _password = std::string(password); } - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + 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) { diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 0b14fd557..82bc7801e 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -35,7 +35,7 @@ typedef get_node_height = Int64 Function(); typedef is_connected = Int8 Function(); typedef setup_node = Int8 Function( - Pointer, Pointer?, Pointer?, Int8, Int8, Pointer); + Pointer, Pointer?, Pointer?, Int8, Int8, Pointer?, Pointer); typedef start_refresh = Void Function(); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index c5918c12a..051f317c9 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -35,7 +35,7 @@ typedef GetNodeHeight = int Function(); typedef IsConnected = int Function(); typedef SetupNode = int Function( - Pointer, Pointer?, Pointer?, int, int, Pointer); + Pointer, Pointer?, Pointer?, int, int, Pointer?, Pointer); typedef StartRefresh = void Function(); diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 7ddbf29dc..1680918e5 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -158,9 +158,11 @@ bool setupNodeSync( String? login, String? password, bool useSSL = false, - bool isLightWallet = false}) { + bool isLightWallet = false, + String? socksProxyAddress}) { final addressPointer = address.toNativeUtf8(); Pointer? loginPointer; + Pointer? socksProxyAddressPointer; Pointer? passwordPointer; if (login != null) { @@ -171,6 +173,10 @@ bool setupNodeSync( passwordPointer = password.toNativeUtf8(); } + if (socksProxyAddress != null) { + socksProxyAddressPointer = socksProxyAddress.toNativeUtf8(); + } + final errorMessagePointer = ''.toNativeUtf8(); final isSetupNode = setupNodeNative( addressPointer, @@ -178,6 +184,7 @@ bool setupNodeSync( passwordPointer, _boolToInt(useSSL), _boolToInt(isLightWallet), + socksProxyAddressPointer, errorMessagePointer) != 0; @@ -328,13 +335,15 @@ bool _setupNodeSync(Map args) { 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); + isLightWallet: isLightWallet, + socksProxyAddress: socksProxyAddress); } bool _isConnected(Object _) => isConnectedSync(); @@ -348,13 +357,15 @@ Future setupNode( String? login, String? password, bool useSSL = false, + String? socksProxyAddress, bool isLightWallet = false}) => compute, void>(_setupNodeSync, { 'address': address, 'login': login , 'password': password, 'useSSL': useSSL, - 'isLightWallet': isLightWallet + 'isLightWallet': isLightWallet, + 'socksProxyAddress': socksProxyAddress }); Future store() => compute(_storeSync, 0); diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 90cc3c279..748b65329 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + (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 { + fee = map['fee'] as int? ?? 0 { additionalInfo = { 'key': key, 'accountIndex': accountIndex, @@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, + direction = parseTransactionDirectionFromInt(row.direction), date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), isPending = row.isPending != 0, amount = row.getAmount(), diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index eea490ba9..ef25b6b93 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,4 +1,6 @@ import 'dart:async'; +import 'dart:io'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_monero/monero_transaction_creation_exception.dart'; @@ -6,7 +8,6 @@ import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_monero/api/transaction_history.dart' as monero_transaction_history; @@ -137,7 +138,8 @@ abstract class MoneroWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { + final currentWalletDirPath = await pathForWalletDir(name: name, type: type); + + 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'); + } + } 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 { monero_wallet.setPasswordSync(password); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 095fe83bb..6539d58a5 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -146,6 +146,26 @@ class MoneroWalletService extends WalletService< 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 = MoneroWallet(walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); } @override diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 1e33631d5..437184a7d 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -672,5 +672,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.0.0" diff --git a/howto-build-android.md b/howto-build-android.md index 4ef385b9f..d37f1b417 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -6,9 +6,9 @@ The following are the system requirements to build CakeWallet for your Android d ``` Ubuntu >= 16.04 -Android SDK 28 +Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 2 or above +Flutter 3.7.x ``` ## 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.x.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.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,9 +66,9 @@ 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.x.x, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 28) -[✓] Android Studio (version 4.0) +[✓] Flutter (Channel stable, 3.7.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) ``` ### 5. Generate a secure keystore for Android diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d5453bd06..f13c68629 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -126,7 +126,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.0): + - permission_handler_apple (9.1.1): - Flutter - platform_device_id (0.0.1): - Flutter @@ -134,6 +134,8 @@ PODS: - SDWebImage (5.16.0): - SDWebImage/Core (= 5.16.0) - SDWebImage/Core (5.16.0) + - sensitive_clipboard (0.0.1): + - Flutter - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -150,6 +152,8 @@ PODS: - Flutter - wakelock (0.0.1): - Flutter + - workmanager (0.0.1): + - Flutter DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) @@ -173,12 +177,14 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) + - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) + - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -235,6 +241,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" platform_device_id: :path: ".symlinks/plugins/platform_device_id/ios" + sensitive_clipboard: + :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -245,6 +253,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" + workmanager: + :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 @@ -270,20 +280,22 @@ SPEC CHECKSUMS: MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 - permission_handler_apple: 8f116445eff3c0e7c65ad60f5fef5490aa94b4e4 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 + sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: 09df1114e7c360f55770d35a79356bf5446e0100 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 05cf659e8..4bc10f9be 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -606,4 +606,9 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 401509606..acdfa4346 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Flutter import UnstoppableDomainsResolution +import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -16,6 +17,15 @@ import UnstoppableDomainsResolution UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } + WorkmanagerPlugin.setPluginRegistrantCallback { registry in + // Registry in this case is the FlutterEngine that is created in Workmanager's + // performFetchWithCompletionHandler or BGAppRefreshTask. + // This will make other plugins available during a background operation. + GeneratedPluginRegistrant.register(with: registry) + } + + WorkmanagerPlugin.registerTask(withIdentifier: "com.fotolockr.cakewallet.monero_sync_task") + makeSecure() let controller : FlutterViewController = window?.rootViewController as! FlutterViewController diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 29cd24cb4..821df195e 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -2,6 +2,10 @@ + BGTaskSchedulerPermittedIdentifiers + + com.fotolockr.cakewallet.monero_sync_task + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -113,6 +117,7 @@ UIBackgroundModes fetch + processing remote-notification UILaunchStoryboardName diff --git a/lib/anonpay/anonpay_invoice_info.dart b/lib/anonpay/anonpay_invoice_info.dart index 89613224e..bd6776d00 100644 --- a/lib/anonpay/anonpay_invoice_info.dart +++ b/lib/anonpay/anonpay_invoice_info.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/keyable.dart'; import 'package:hive/hive.dart'; @@ -35,7 +36,7 @@ class AnonpayInvoiceInfo extends HiveObject with Keyable implements AnonpayInfoB @HiveField(13) final String provider; - static const typeId = 10; + static const typeId = ANONPAY_INVOICE_INFO_TYPE_ID; static const boxName = 'AnonpayInvoiceInfo'; AnonpayInvoiceInfo({ diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630ecf27f..dfd3b1538 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority != null ? priority as BitcoinTransactionPriority : null, + priority: priority as BitcoinTransactionPriority, feeRate: feeRate); @override diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 33c63df61..68be59f4e 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -20,6 +20,8 @@ class OnRamperBuyProvider { switch (_wallet.currency) { case CryptoCurrency.ltc: return "LTC_LITECOIN"; + case CryptoCurrency.xmr: + return "XMR_MONERO"; default: return _wallet.currency.title; } @@ -60,11 +62,12 @@ class OnRamperBuyProvider { break; } + final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); return Uri.https(_baseUrl, '', { 'apiKey': _apiKey, 'defaultCrypto': _normalizeCryptoCurrency, - 'wallets': '${_wallet.currency.title}:${_wallet.walletAddresses.address}', + 'networkWallets': '${networkName}:${_wallet.walletAddresses.address}', 'supportSell': "false", 'supportSwap': "false", 'primaryColor': primaryColor, diff --git a/lib/buy/order.dart b/lib/buy/order.dart index 387fbcd34..5a677d291 100644 --- a/lib/buy/order.dart +++ b/lib/buy/order.dart @@ -1,7 +1,8 @@ import 'package:cake_wallet/buy/buy_provider_description.dart'; -import 'package:hive/hive.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; part 'order.g.dart'; @@ -26,7 +27,7 @@ class Order extends HiveObject { } } - static const typeId = 8; + static const typeId = ORDER_TYPE_ID; static const boxName = 'Orders'; static const boxKey = 'ordersBoxKey'; @@ -66,4 +67,4 @@ class Order extends HiveObject { BuyProviderDescription.deserialize(raw: providerRaw); String amountFormatted() => formatAmount(amount); -} \ No newline at end of file +} diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 064efa11b..f2a235363 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) @@ -14,6 +15,9 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { + if (type is Erc20Token) { + return '0x[0-9a-zA-Z]'; + } switch (type) { case CryptoCurrency.xmr: return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; @@ -36,6 +40,27 @@ class AddressValidator extends TextValidator { case CryptoCurrency.oxt: case CryptoCurrency.paxg: case CryptoCurrency.uni: + case CryptoCurrency.aave: + case CryptoCurrency.bat: + case CryptoCurrency.comp: + case CryptoCurrency.cro: + case CryptoCurrency.ens: + case CryptoCurrency.ftm: + case CryptoCurrency.frax: + case CryptoCurrency.gusd: + case CryptoCurrency.gtc: + case CryptoCurrency.grt: + case CryptoCurrency.ldo: + case CryptoCurrency.nexo: + case CryptoCurrency.pepe: + case CryptoCurrency.storj: + case CryptoCurrency.tusd: + case CryptoCurrency.wbtc: + case CryptoCurrency.weth: + case CryptoCurrency.zrx: + case CryptoCurrency.dydx: + case CryptoCurrency.steth: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -96,28 +121,57 @@ class AddressValidator extends TextValidator { } static List? getLength(CryptoCurrency type) { + if (type is Erc20Token) { + return [42]; + } switch (type) { case CryptoCurrency.xmr: return null; case CryptoCurrency.ada: return null; - case CryptoCurrency.ape: - return [42]; - case CryptoCurrency.avaxc: - return [42]; - case CryptoCurrency.bch: - return [42]; - case CryptoCurrency.bnb: - return [42]; case CryptoCurrency.btc: return null; - case CryptoCurrency.dai: - return [42]; case CryptoCurrency.dash: return [34]; case CryptoCurrency.eos: return [42]; case CryptoCurrency.eth: + case CryptoCurrency.usdcpoly: + case CryptoCurrency.mana: + case CryptoCurrency.matic: + case CryptoCurrency.maticpoly: + case CryptoCurrency.mkr: + case CryptoCurrency.oxt: + case CryptoCurrency.paxg: + case CryptoCurrency.uni: + case CryptoCurrency.dai: + case CryptoCurrency.ape: + case CryptoCurrency.usdc: + case CryptoCurrency.usdterc20: + case CryptoCurrency.aave: + case CryptoCurrency.bat: + case CryptoCurrency.comp: + case CryptoCurrency.cro: + case CryptoCurrency.ens: + case CryptoCurrency.ftm: + case CryptoCurrency.frax: + case CryptoCurrency.gusd: + case CryptoCurrency.gtc: + case CryptoCurrency.grt: + case CryptoCurrency.ldo: + case CryptoCurrency.nexo: + case CryptoCurrency.pepe: + case CryptoCurrency.storj: + case CryptoCurrency.tusd: + case CryptoCurrency.wbtc: + case CryptoCurrency.weth: + case CryptoCurrency.zrx: + case CryptoCurrency.dydx: + case CryptoCurrency.steth: + case CryptoCurrency.shib: + case CryptoCurrency.avaxc: + case CryptoCurrency.bch: + case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; @@ -129,14 +183,10 @@ class AddressValidator extends TextValidator { return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.trx: return [34]; - case CryptoCurrency.usdc: - return [42]; case CryptoCurrency.usdcsol: return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; case CryptoCurrency.usdt: return [34]; - case CryptoCurrency.usdterc20: - return [42]; case CryptoCurrency.usdttrc20: return [34]; case CryptoCurrency.xlm: @@ -159,11 +209,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xusd: return [98, 99, 106]; case CryptoCurrency.btt: - return [34]; case CryptoCurrency.bttc: - return [34]; case CryptoCurrency.doge: - return [34]; case CryptoCurrency.firo: return [34]; case CryptoCurrency.hbar: @@ -184,15 +231,6 @@ class AddressValidator extends TextValidator { return [35]; case CryptoCurrency.stx: return [40, 41, 42]; - case CryptoCurrency.usdcpoly: - case CryptoCurrency.mana: - case CryptoCurrency.matic: - case CryptoCurrency.maticpoly: - case CryptoCurrency.mkr: - case CryptoCurrency.oxt: - case CryptoCurrency.paxg: - case CryptoCurrency.uni: - return [42]; case CryptoCurrency.rune: return [43]; case CryptoCurrency.scrt: @@ -223,6 +261,8 @@ class AddressValidator extends TextValidator { return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.eth: + return '0x[0-9a-zA-Z]{42}'; default: return null; } diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 8091740e6..854640015 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -25,6 +25,10 @@ class AuthService with Store { Routes.setupPin, Routes.setup_2faPage, Routes.modify2FAPage, + Routes.newWallet, + Routes.newWalletType, + Routes.addressBookAddContact, + Routes.restoreOptions, ]; final FlutterSecureStorage secureStorage; @@ -81,21 +85,26 @@ class AuthService with Store { } Future authenticateAction(BuildContext context, - {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async { + {Function(bool)? onAuthSuccess, + String? route, + Object? arguments, + required bool conditionToDetermineIfToUse2FA}) async { assert(route != null || onAuthSuccess != null, 'Either route or onAuthSuccess param must be passed.'); - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { - if (onAuthSuccess != null) { - onAuthSuccess(true); - } else { - Navigator.of(context).pushNamed( - route ?? '', - arguments: arguments, - ); + if (!conditionToDetermineIfToUse2FA) { + if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (onAuthSuccess != null) { + onAuthSuccess(true); + } else { + Navigator.of(context).pushNamed( + route ?? '', + arguments: arguments, + ); + } + return; } - return; - } +} Navigator.of(context).pushNamed(Routes.auth, @@ -104,7 +113,7 @@ class AuthService with Store { onAuthSuccess?.call(false); return; } else { - if (settingsStore.useTOTP2FA) { + if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) { auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2870c4488..6476891ed 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; +import 'package:cw_core/cake_hive.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -16,11 +17,12 @@ import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/wallet_types.g.dart'; + import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService(this._flutterSecureStorage, this._walletInfoSource, - this._keyService, this._sharedPreferences) + BackupService( + this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = []; @@ -67,9 +69,8 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _exportBackupV1(String password, - {String nonce = secrets.backupSalt}) async - => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); + Future _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async => + throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); Future _exportBackupV2(String password) async { final zipEncoder = ZipFileEncoder(); @@ -112,8 +113,7 @@ class BackupService { return await _encryptV2(content, password); } - Future _importBackupV1(Uint8List data, String password, - {required String nonce}) async { + Future _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -161,10 +161,8 @@ class BackupService { Future _verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); - _correctWallets = walletInfoSource - .values - .where((info) => availableWalletTypes.contains(info.type)) - .toList(); + _correctWallets = + walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); if (_correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); @@ -173,14 +171,14 @@ class BackupService { Future> _reloadHiveWalletInfoBox() async { final appDir = await getApplicationDocumentsDirectory(); - await Hive.close(); - Hive.init(appDir.path); + await CakeHive.close(); + CakeHive.init(appDir.path); - if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { - Hive.registerAdapter(WalletInfoAdapter()); + if (!CakeHive.isAdapterRegistered(WalletInfo.typeId)) { + CakeHive.registerAdapter(WalletInfoAdapter()); } - return await Hive.openBox(WalletInfo.boxName); + return await CakeHive.openBox(WalletInfo.boxName); } Future _importPreferencesDump() async { @@ -191,14 +189,12 @@ class BackupService { return; } - final data = - json.decode(preferencesFile.readAsStringSync()) as Map; + final data = json.decode(preferencesFile.readAsStringSync()) as Map; String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; final isCorrentCurrentWallet = _correctWallets - .any((info) => info.name == currentWalletName && - info.type.index == currentWalletType); + .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { currentWalletName = _correctWallets.first.name; @@ -212,138 +208,193 @@ class BackupService { final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; - final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; + final currentTransactionPriorityKeyLegacy = + data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; + final allowBiometricalAuthentication = + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; + final currentBitcoinElectrumSererId = + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; - final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; + final currentDefaultSettingsMigrationVersion = + data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; + final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?; + final shouldRequireTOTP2FAForAccessingWallet = + data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?; + final shouldRequireTOTP2FAForSendsToContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?; + final shouldRequireTOTP2FAForSendsToNonContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?; + final shouldRequireTOTP2FAForSendsToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?; + final shouldRequireTOTP2FAForExchangesToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?; + final shouldRequireTOTP2FAForAddingContacts = + data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?; + final shouldRequireTOTP2FAForCreatingNewWallets = + data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; + final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; + final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; + final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; + final syncAll = data[PreferencesKey.syncAllKey] as bool?; + final syncMode = data[PreferencesKey.syncModeKey] as int?; - await _sharedPreferences.setString(PreferencesKey.currentWalletName, - currentWalletName); + await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); if (currentNodeId != null) - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, - currentNodeId); + await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); if (currentBalanceDisplayMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, - currentBalanceDisplayMode); + await _sharedPreferences.setInt( + PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - await _sharedPreferences.setInt(PreferencesKey.currentWalletType, - currentWalletType); + await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); if (currentFiatCurrency != null) - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, - currentFiatCurrency); + await _sharedPreferences.setString( + PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); if (shouldSaveRecipientAddress != null) await _sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, - shouldSaveRecipientAddress); + PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); if (isAppSecure != null) - await _sharedPreferences.setBool( - PreferencesKey.isAppSecureKey, - isAppSecure); + await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); if (disableBuy != null) - await _sharedPreferences.setBool( - PreferencesKey.disableBuyKey, - disableBuy); + await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); if (disableSell != null) - await _sharedPreferences.setBool( - PreferencesKey.disableSellKey, - disableSell); + await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKeyLegacy, - currentTransactionPriorityKeyLegacy); + PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); if (allowBiometricalAuthentication != null) await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, - allowBiometricalAuthentication); + PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - currentBitcoinElectrumSererId); + PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); if (currentLanguageCode != null) - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, - currentLanguageCode); + await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); if (displayActionListMode != null) - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, - displayActionListMode); + await _sharedPreferences.setInt( + PreferencesKey.displayActionListModeKey, displayActionListMode); if (fiatApiMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, - fiatApiMode); + await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); if (currentPinLength != null) - await _sharedPreferences.setInt(PreferencesKey.currentPinLength, - currentPinLength); + await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); if (currentTheme != null) - await _sharedPreferences.setInt( - PreferencesKey.currentTheme, currentTheme); + await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); if (exchangeStatus != null) - await _sharedPreferences.setInt( - PreferencesKey.exchangeStatusKey, exchangeStatus); + await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) - await _sharedPreferences.setInt( - PreferencesKey.currentDefaultSettingsMigrationVersion, - currentDefaultSettingsMigrationVersion); + await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, + currentDefaultSettingsMigrationVersion); if (moneroTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority, - moneroTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); if (bitcoinTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, - bitcoinTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); + + if (selectedCake2FAPreset != null) + await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset); + + if (shouldRequireTOTP2FAForAccessingWallet != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, + shouldRequireTOTP2FAForAccessingWallet); + + if (shouldRequireTOTP2FAForSendsToContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact, + shouldRequireTOTP2FAForSendsToContact); + + if (shouldRequireTOTP2FAForSendsToNonContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, + shouldRequireTOTP2FAForSendsToNonContact); + + if (shouldRequireTOTP2FAForSendsToInternalWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, + shouldRequireTOTP2FAForSendsToInternalWallets); + + if (shouldRequireTOTP2FAForExchangesToInternalWallets != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForExchangesToInternalWallets); + + if (shouldRequireTOTP2FAForAddingContacts != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts, + shouldRequireTOTP2FAForAddingContacts); + + if (shouldRequireTOTP2FAForCreatingNewWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, + shouldRequireTOTP2FAForCreatingNewWallets); + + if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + + if (sortBalanceTokensBy != null) + await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + + if (pinNativeTokenAtTop != null) + await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + + if (useEtherscan != null) + await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + + if (syncAll != null) + await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + if (syncMode != null) + await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); await preferencesFile.delete(); } Future _importKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async { + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV1( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -352,27 +403,24 @@ class BackupService { {String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV2( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map; + final decryptedKeychainDumpFileData = + await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -386,35 +434,26 @@ class BackupService { @Deprecated('Use v2 instead') Future _exportKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async - => throw Exception('Deprecated'); + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => + throw Exception('Deprecated'); Future _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); - final wallets = - await Future.wait(_walletInfoSource.values.map((walletInfo) async { + final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': - await _keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); - final backupPassword = - await _flutterSecureStorage.read(key: backupPasswordKey); - final data = utf8.encode(json.encode({ - 'pin': decodedPin, - 'wallets': wallets, - backupPasswordKey: backupPassword - })); - final encrypted = await _encryptV2( - Uint8List.fromList(data), '$keychainSalt$password'); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); + final data = utf8.encode( + json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); + final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; } @@ -423,46 +462,67 @@ class BackupService { final preferences = { PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), - PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType), + PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + PreferencesKey.currentBalanceDisplayModeKey: + _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), + PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), - PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences - .getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences - .getBool(PreferencesKey.disableSellKey), + PreferencesKey.shouldSaveRecipientAddressKey: + _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), + PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), + PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), PreferencesKey.isDarkThemeLegacy: _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), - PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength), - PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey), - PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), + PreferencesKey.currentTransactionPriorityKeyLegacy: + _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + PreferencesKey.allowBiometricalAuthenticationKey: + _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey), + PreferencesKey.currentBitcoinElectrumSererIdKey: + _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: - _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), - PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), + PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), + PreferencesKey.currentDefaultSettingsMigrationVersion: + _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + PreferencesKey.selectedCake2FAPreset: + _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset), + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet), + PreferencesKey.shouldRequireTOTP2FAForSendsToContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForAddingContacts: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts), + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + PreferencesKey.sortBalanceBy: + _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.pinNativeTokenAtTop: + _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: + _sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.syncModeKey: + _sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: + _sharedPreferences.getBool(PreferencesKey.syncAllKey), }; return json.encode(preferences); @@ -476,28 +536,23 @@ class BackupService { } @Deprecated('Use v2 instead') - Future _encryptV1( - Uint8List data, String secretKeySource, String nonceBase64) async - => throw Exception('Deprecated'); + Future _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async => + throw Exception('Deprecated'); - Future _decryptV1( - Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { + Future _decryptV1(Uint8List data, String secretKeySource, String nonceBase64, + {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); final nonce = base64.decode(nonceBase64).toList(); - final box = SecretBox( - Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), - nonce: nonce, - mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); + final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), + nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); final plainData = await _cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } - Future _encryptV2( - Uint8List data, String passphrase) async - => cake_backup.encrypt(passphrase, data, version: _v2); + Future _encryptV2(Uint8List data, String passphrase) async => + cake_backup.encrypt(passphrase, data, version: _v2); - Future _decryptV2( - Uint8List data, String passphrase) async - => cake_backup.decrypt(passphrase, data); + Future _decryptV2(Uint8List data, String passphrase) async => + cake_backup.decrypt(passphrase, data); } diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 11aef1374..9690c430a 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,21 +5,20 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; - const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; const _fiatApiPath = '/v2/rates'; Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as CryptoCurrency; - final fiat = args['fiat'] as FiatCurrency; + final crypto = args['crypto'] as String; + final fiat = args['fiat'] as String; final torOnly = args['torOnly'] as bool; final Map queryParams = { 'interval_count': '1', - 'base': crypto.toString(), - 'quote': fiat.toString(), - 'key' : secrets.fiatApiKey, + 'base': crypto, + 'quote': fiat, + 'key': secrets.fiatApiKey, }; double price = 0.0; @@ -52,7 +51,11 @@ Future _fetchPrice(Map args) async { } Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); + compute(_fetchPrice, { + 'fiat': fiat.toString(), + 'crypto': crypto.toString(), + 'torOnly': torOnly, + }); class FiatConversionService { static Future fetchPrice({ diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart index 1fe99623e..337f1ef21 100644 --- a/lib/core/key_service.dart +++ b/lib/core/key_service.dart @@ -21,4 +21,11 @@ class KeyService { await _secureStorage.write(key: key, value: encodedPassword); } + + Future deleteWalletPassword({required String walletName}) async { + final key = generateStoreKeyFor( + key: SecretStoreKey.moneroWalletPassword, walletName: walletName); + + await _secureStorage.delete(key: key); + } } diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index fe9a25f85..eba1bbda4 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,4 +1,5 @@ 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'; @@ -25,6 +26,8 @@ class SeedValidator extends Validator { return monero!.getMoneroWordList(language); case WalletType.haven: return haven!.getMoneroWordList(language); + case WalletType.ethereum: + return ethereum!.getEthereumWordList(language); default: return []; } diff --git a/lib/core/socks_proxy_node_address_validator.dart b/lib/core/socks_proxy_node_address_validator.dart new file mode 100644 index 000000000..eb1f78f1d --- /dev/null +++ b/lib/core/socks_proxy_node_address_validator.dart @@ -0,0 +1,10 @@ +import 'package:cake_wallet/core/validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; + +class SocksProxyNodeAddressValidator extends TextValidator { + SocksProxyNodeAddressValidator() + : super( + errorMessage: S.current.error_text_node_proxy_address, + pattern: + '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]+\$'); +} diff --git a/lib/core/wallet_loading_service.dart b/lib/core/wallet_loading_service.dart index 761c6acce..3323e7831 100644 --- a/lib/core/wallet_loading_service.dart +++ b/lib/core/wallet_loading_service.dart @@ -7,43 +7,66 @@ import 'package:cw_core/wallet_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; class WalletLoadingService { - WalletLoadingService( - this.sharedPreferences, - this.keyService, - this.walletServiceFactory); - - final SharedPreferences sharedPreferences; - final KeyService keyService; - final WalletService Function(WalletType type) walletServiceFactory; + WalletLoadingService( + this.sharedPreferences, this.keyService, this.walletServiceFactory); - Future load(WalletType type, String name) async { - final walletService = walletServiceFactory.call(type); - final password = await keyService.getWalletPassword(walletName: name); - final wallet = await walletService.openWallet(name, password); + final SharedPreferences sharedPreferences; + final KeyService keyService; + final WalletService Function(WalletType type) walletServiceFactory; - if (type == WalletType.monero) { - await updateMoneroWalletPassword(wallet); - } + Future renameWallet( + WalletType type, String name, String newName) async { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); - return wallet; - } + // Save the current wallet's password to the new wallet name's key + await keyService.saveWalletPassword( + walletName: newName, password: password); + // Delete previous wallet name from keyService to keep only new wallet's name + // otherwise keeps duplicate (old and new names) + await keyService.deleteWalletPassword(walletName: name); - Future updateMoneroWalletPassword(WalletBase wallet) async { - final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name); - var isPasswordUpdated = sharedPreferences.getBool(key) ?? false; + await walletService.rename(name, password, newName); - if (isPasswordUpdated) { - return; - } + // set shared preferences flag based on previous wallet name + if (type == WalletType.monero) { + final oldNameKey = PreferencesKey.moneroWalletUpdateV1Key(name); + final isPasswordUpdated = sharedPreferences.getBool(oldNameKey) ?? false; + final newNameKey = PreferencesKey.moneroWalletUpdateV1Key(newName); + await sharedPreferences.setBool(newNameKey, isPasswordUpdated); + } + } - final password = generateWalletPassword(); - // Save new generated password with backup key for case where - // wallet will change password, but it will fail to update in secure storage - final bakWalletName = '#__${wallet.name}_bak__#'; - await keyService.saveWalletPassword(walletName: bakWalletName, password: password); - await wallet.changePassword(password); - await keyService.saveWalletPassword(walletName: wallet.name, password: password); - isPasswordUpdated = true; - await sharedPreferences.setBool(key, isPasswordUpdated); - } -} \ No newline at end of file + Future load(WalletType type, String name) async { + final walletService = walletServiceFactory.call(type); + final password = await keyService.getWalletPassword(walletName: name); + final wallet = await walletService.openWallet(name, password); + + if (type == WalletType.monero) { + await updateMoneroWalletPassword(wallet); + } + + return wallet; + } + + Future updateMoneroWalletPassword(WalletBase wallet) async { + final key = PreferencesKey.moneroWalletUpdateV1Key(wallet.name); + var isPasswordUpdated = sharedPreferences.getBool(key) ?? false; + + if (isPasswordUpdated) { + return; + } + + final password = generateWalletPassword(); + // Save new generated password with backup key for case where + // wallet will change password, but it will fail to update in secure storage + final bakWalletName = '#__${wallet.name}_bak__#'; + await keyService.saveWalletPassword( + walletName: bakWalletName, password: password); + await wallet.changePassword(password); + await keyService.saveWalletPassword( + walletName: wallet.name, password: password); + isPasswordUpdated = true; + await sharedPreferences.setBool(key, isPasswordUpdated); + } +} diff --git a/lib/di.dart b/lib/di.dart index 76813f475..2a54c85a3 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; @@ -16,10 +18,13 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; +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'; @@ -31,6 +36,9 @@ import 'package:cake_wallet/src/screens/setup_2fa/modify_2fa_page.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_qr_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'; +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/themes/theme_list.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; @@ -39,6 +47,7 @@ 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/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; @@ -67,6 +76,9 @@ 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/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:cw_core/erc20_token.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -236,15 +248,21 @@ Future setup({ getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); } - final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) && - (secrets.wyreApiKey.isNotEmpty ?? false) && - (secrets.wyreAccountId.isNotEmpty ?? false); + if (!_isSetupFinished) { + getIt.registerFactory(() => BackgroundTasks()); + } + + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && + (secrets.wyreApiKey.isNotEmpty) && + (secrets.wyreAccountId.isNotEmpty); final settingsStore = await SettingsStoreBase.load( nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, // Enforce darkTheme on platforms other than mobile till the design for other themes is completed - initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme, + initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile + ? null + : ThemeList.darkTheme, ); if (_isSetupFinished) { @@ -386,7 +404,9 @@ Future setup({ final authStore = getIt.get(); final appStore = getIt.get(); final useTotp = appStore.settingsStore.useTOTP2FA; - if (useTotp) { + final shouldUseTotp2FAToAccessWallets = + appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { authPageState.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( @@ -522,17 +542,22 @@ Future setup({ getIt.get(), getIt.get())); - getIt.registerFactory(() => SendViewModel( + getIt.registerFactory( + () => SendViewModel( getIt.get().wallet!, getIt.get().settingsStore, getIt.get(), getIt.get(), getIt.get(), - _transactionDescriptionBox)); + getIt.get(), + _transactionDescriptionBox, + ), + ); getIt.registerFactoryParam( (PaymentRequest? initialPaymentRequest, _) => SendPage( sendViewModel: getIt.get(), + authService: getIt.get(), initialPaymentRequest: initialPaymentRequest, )); @@ -566,6 +591,20 @@ Future setup({ authService: getIt.get(), )); + getIt.registerFactoryParam( + (WalletListViewModel walletListViewModel, _) => + WalletEditViewModel(walletListViewModel, getIt.get())); + + getIt.registerFactoryParam, void>((args, _) { + final walletListViewModel = args.first as WalletListViewModel; + final editingWallet = args.last as WalletListItem; + return WalletEditPage( + walletEditViewModel: getIt.get(param1: walletListViewModel), + authService: getIt.get(), + walletNewVM: getIt.get(param1: editingWallet.type), + editingWallet: editingWallet); + }); + getIt.registerFactory(() { final wallet = getIt.get().wallet!; @@ -612,7 +651,7 @@ Future setup({ }); getIt.registerFactory(() { - return PrivacySettingsViewModel(getIt.get()); + return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); getIt.registerFactory(() { @@ -636,10 +675,11 @@ Future setup({ (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); getIt.registerFactoryParam( - (CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, _walletInfoSource, cur)); + (CryptoCurrency? cur, _) => + ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get())); - getIt.registerFactoryParam( - (CryptoCurrency? cur, _) => ContactListPage(getIt.get(param1: cur))); + getIt.registerFactoryParam((CryptoCurrency? cur, _) => + ContactListPage(getIt.get(param1: cur), getIt.get())); getIt.registerFactoryParam( (ContactRecord? contact, _) => ContactPage(getIt.get(param1: contact))); @@ -649,8 +689,7 @@ Future setup({ return NodeListViewModel(_nodeSource, appStore); }); - getIt.registerFactory( - () => ConnectionSyncPage(getIt.get(), getIt.get())); + getIt.registerFactory(() => ConnectionSyncPage(getIt.get())); getIt.registerFactory( () => SecurityBackupPage(getIt.get(), getIt.get())); @@ -684,13 +723,13 @@ Future setup({ )); getIt.registerFactory(() => ExchangeViewModel( - getIt.get().wallet!, - _tradesSource, - getIt.get(), - getIt.get(), - getIt.get().settingsStore, - getIt.get(), - )); + getIt.get().wallet!, + _tradesSource, + getIt.get(), + getIt.get(), + getIt.get().settingsStore, + getIt.get(), + getIt.get())); getIt.registerFactory(() => ExchangeTradeViewModel( wallet: getIt.get().wallet!, @@ -698,7 +737,8 @@ Future setup({ tradesStore: getIt.get(), sendViewModel: getIt.get())); - getIt.registerFactory(() => ExchangePage(getIt.get())); + getIt.registerFactory( + () => ExchangePage(getIt.get(), getIt.get())); getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get())); @@ -717,6 +757,8 @@ Future setup({ return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + case WalletType.ethereum: + return ethereum!.createEthereumWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -759,8 +801,8 @@ Future setup({ transactionDetailsViewModel: getIt.get(param1: transactionInfo))); - getIt.registerFactoryParam( - (param1, _) => NewWalletTypePage(onTypeSelected: param1)); + getIt.registerFactoryParam( + (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( (WalletType type, _) => PreSeedPage(type)); @@ -829,6 +871,12 @@ Future setup({ getIt.registerFactory(() => SupportPage(getIt.get())); + getIt.registerFactory(() => + SupportChatPage( + getIt.get(), secureStorage: getIt.get())); + + getIt.registerFactory(() => SupportOtherLinksPage(getIt.get())); + getIt.registerFactory(() { final wallet = getIt.get().wallet; @@ -872,7 +920,7 @@ Future setup({ getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get())); - getIt.registerFactory(()=> MarketPlaceViewModel(getIt.get())); + getIt.registerFactory(() => MarketPlaceViewModel(getIt.get())); getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get())); @@ -1006,5 +1054,21 @@ Future setup({ getIt.registerFactoryParam( (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); + getIt.registerFactoryParam((balanceViewModel, _) => + HomeSettingsPage(getIt.get(param1: balanceViewModel))); + + getIt.registerFactoryParam( + (balanceViewModel, _) => HomeSettingsViewModel(getIt.get(), balanceViewModel)); + + getIt.registerFactoryParam>( + (homeSettingsViewModel, arguments) => EditTokenPage( + homeSettingsViewModel: homeSettingsViewModel, + erc20token: arguments['token'] as Erc20Token?, + initialContractAddress: arguments['contractAddress'] as String?, + ), + ); + + getIt.registerFactory(() => ManageNodesPage(getIt.get())); + _isSetupFinished = true; } diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart new file mode 100644 index 000000000..ce1e2f6d8 --- /dev/null +++ b/lib/entities/background_tasks.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.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:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/di.dart'; + +const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + try { + switch (task) { + case moneroSyncTaskKey: + + /// The work manager runs on a separate isolate from the main flutter isolate. + /// thus we initialize app configs first; hive, getIt, etc... + await initializeAppConfigs(); + + final walletLoadingService = getIt.get(); + + final node = getIt.get().getCurrentNode(WalletType.monero); + + final typeRaw = getIt.get().getInt(PreferencesKey.currentWalletType); + + WalletBase? wallet; + + if (inputData!['sync_all'] as bool) { + /// get all Monero wallets of the user and sync them + final List moneroWallets = getIt + .get() + .wallets + .where((element) => element.type == WalletType.monero) + .toList(); + + for (int i = 0; i < moneroWallets.length; i++) { + wallet = await walletLoadingService.load(WalletType.monero, moneroWallets[i].name); + + 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) { + final name = + getIt.get().getString(PreferencesKey.currentWalletName); + + wallet = await walletLoadingService.load(WalletType.monero, name!); + + await wallet.connectToNode(node: node); + await wallet.startSync(); + } + } + + if (wallet?.syncStatus.progress() == null) { + return Future.error("No Monero wallet found"); + } + + for (int i = 0;; i++) { + await Future.delayed(const Duration(seconds: 1)); + if (wallet?.syncStatus.progress() == 1.0) { + break; + } + if (i > 600) { + return Future.error("Synchronization Timed out"); + } + } + break; + } + + return Future.value(true); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + return Future.error(error); + } + }); +} + +class BackgroundTasks { + void registerSyncTask({bool changeExisting = false}) async { + try { + bool hasMonero = getIt + .get() + .wallets + .any((element) => element.type == WalletType.monero); + + /// if its not android nor ios, or the user has no monero wallets; exit + if (!DeviceInfo.instance.isMobile || !hasMonero) { + return; + } + + final settingsStore = getIt.get(); + + final SyncMode syncMode = settingsStore.currentSyncMode; + final bool syncAll = settingsStore.currentSyncAll; + + if (syncMode.type == SyncType.disabled) { + cancelSyncTask(); + return; + } + + await Workmanager().initialize( + callbackDispatcher, + isInDebugMode: kDebugMode, + ); + + final inputData = {"sync_all": syncAll}; + final constraints = Constraints( + networkType: + syncMode.type == SyncType.unobtrusive ? NetworkType.unmetered : NetworkType.connected, + requiresBatteryNotLow: syncMode.type == SyncType.unobtrusive, + requiresCharging: syncMode.type == SyncType.unobtrusive, + requiresDeviceIdle: syncMode.type == SyncType.unobtrusive, + ); + + if (Platform.isIOS) { + await Workmanager().registerOneOffTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + existingWorkPolicy: ExistingWorkPolicy.replace, + inputData: inputData, + constraints: constraints, + ); + return; + } + + await Workmanager().registerPeriodicTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + frequency: syncMode.frequency, + existingWorkPolicy: changeExisting ? ExistingWorkPolicy.replace : ExistingWorkPolicy.keep, + inputData: inputData, + constraints: constraints, + ); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } + + void cancelSyncTask() { + try { + Workmanager().cancelByUniqueName(moneroSyncTaskKey); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } +} diff --git a/lib/entities/cake_2fa_preset_options.dart b/lib/entities/cake_2fa_preset_options.dart new file mode 100644 index 000000000..2aa6c4215 --- /dev/null +++ b/lib/entities/cake_2fa_preset_options.dart @@ -0,0 +1,35 @@ +import 'package:cw_core/enumerable_item.dart'; + +class Cake2FAPresetsOptions extends EnumerableItem with Serializable { + const Cake2FAPresetsOptions({required String super.title, required int super.raw}); + + static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0); + static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1); + static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2); + + static Cake2FAPresetsOptions deserialize({required int raw}) { + switch (raw) { + case 0: + return Cake2FAPresetsOptions.narrow; + case 1: + return Cake2FAPresetsOptions.normal; + case 2: + return Cake2FAPresetsOptions.aggressive; + default: + throw Exception( + 'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize', + ); + } + } +} + +enum VerboseControlSettings { + accessWallet, + addingContacts, + sendsToContacts, + sendsToNonContacts, + sendsToInternalWallets, + exchangesToInternalWallets, + securityAndBackupSettings, + creatingNewWallets, +} diff --git a/lib/entities/contact.dart b/lib/entities/contact.dart index e111429ca..cd4fa55a2 100644 --- a/lib/entities/contact.dart +++ b/lib/entities/contact.dart @@ -1,8 +1,7 @@ -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cake_wallet/utils/mobx.dart'; +import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/keyable.dart'; +import 'package:hive/hive.dart'; part 'contact.g.dart'; @@ -14,7 +13,7 @@ class Contact extends HiveObject with Keyable { } } - static const typeId = 0; + static const typeId = CONTACT_TYPE_ID; static const boxName = 'Contacts'; @HiveField(0, defaultValue: '') diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 77298c2b5..b4cb23131 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; +const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; Future defaultSettingsMigration( {required int version, @@ -157,6 +158,12 @@ Future defaultSettingsMigration( case 20: await migrateExchangeStatus(sharedPreferences); break; + case 21: + await addEthereumNodeList(nodes: nodes); + await changeEthereumCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + default: break; } @@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); } +Node? getEthereumDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == ethereumDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -438,6 +451,8 @@ Future checkCurrentNodes( .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final currentHavenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull( (node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( @@ -446,6 +461,8 @@ Future checkCurrentNodes( (node) => node.key == currentLitecoinElectrumSeverId); final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -479,6 +496,13 @@ Future checkCurrentNodes( await sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); } + + if (currentEthereumNodeServer == null) { + final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -522,8 +546,26 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { return; } - await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled ? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw); - + await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } + +Future addEthereumNodeList({required Box nodes}) async { + final nodeList = await loadDefaultEthereumNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeEthereumCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box nodes}) async { + final node = getEthereumDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); +} diff --git a/lib/entities/get_encryption_key.dart b/lib/entities/get_encryption_key.dart index 5fc4983d7..e67380bb8 100644 --- a/lib/entities/get_encryption_key.dart +++ b/lib/entities/get_encryption_key.dart @@ -1,23 +1,18 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:hive/hive.dart'; +import 'package:cw_core/cake_hive.dart'; Future> getEncryptionKey( {required String forKey, required FlutterSecureStorage secureStorage}) async { - final stringifiedKey = - await secureStorage.read(key: 'transactionDescriptionsBoxKey'); + final stringifiedKey = await secureStorage.read(key: 'transactionDescriptionsBoxKey'); List key; if (stringifiedKey == null) { - key = Hive.generateSecureKey(); + key = CakeHive.generateSecureKey(); final keyStringified = key.join(','); - await secureStorage.write( - key: 'transactionDescriptionsBoxKey', value: keyStringified); + await secureStorage.write(key: 'transactionDescriptionsBoxKey', value: keyStringified); } else { - key = stringifiedKey - .split(',') - .map((i) => int.parse(i)) - .toList(); + key = stringifiedKey.split(',').map((i) => int.parse(i)).toList(); } return key; -} \ No newline at end of file +} diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 882d1840e..d758b6697 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -1,8 +1,7 @@ import 'package:cake_wallet/di.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cw_core/wallet_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -24,4 +23,6 @@ Future loadCurrentWallet() async { final walletLoadingService = getIt.get(); final wallet = await walletLoadingService.load(type, name); appStore.changeCurrentWallet(wallet); + + getIt.get().registerSyncTask(); } diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 0cf3cead4..912269d8e 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -46,6 +46,8 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: + case WalletType.monero: if (viewModel.isEnabledBuyAction) { final uri = getIt.get().requestUrl(); if (DeviceInfo.instance.isMobile) { @@ -56,13 +58,6 @@ class MainActions { } } break; - case WalletType.monero: - if (viewModel.isEnabledBuyAction) { - // final uri = getIt.get().requestUrl(); - final uri = Uri.parse("https://monero.com/trade"); - await launchUrl(uri); - } - break; default: await showPopUp( context: context, @@ -116,6 +111,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); final uri = await moonPaySellProvider.requestUrl( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 58847ccfa..b06351a79 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -70,6 +70,22 @@ Future> loadDefaultHavenNodes() async { return nodes; } +Future> loadDefaultEthereumNodes() async { + final nodesRaw = await rootBundle.loadString('assets/ethereum_server_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.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/openalias_record.dart b/lib/entities/openalias_record.dart index 842a711fe..6d8b759f5 100644 --- a/lib/entities/openalias_record.dart +++ b/lib/entities/openalias_record.dart @@ -37,7 +37,7 @@ class OpenaliasRecord { required String ticker, required List txtRecord, }) { - String address = formattedName; + String address = ''; String name = formattedName; String note = ''; diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 8ac9bb51f..c0eab6d65 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -19,14 +19,19 @@ class AddressResolver { 'crypto', 'zil', 'x', - 'coin', 'wallet', 'bitcoin', '888', 'nft', 'dao', 'blockchain', - 'polygon' + 'polygon', + 'klever', + 'hi', + 'kresus', + 'anime', + 'manga', + 'binanceus' ]; static String? extractAddressByType({required String raw, required CryptoCurrency type}) { diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index a73b44e33..67caebcb5 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter } +enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, contact } class ParsedAddress { ParsedAddress({ @@ -40,13 +40,17 @@ class ParsedAddress { ); } - factory ParsedAddress.fetchOpenAliasAddress({required OpenaliasRecord record, required String name}){ - return ParsedAddress( - addresses: [record.address], - name: record.name, - description: record.description, - parseFrom: ParseFrom.openAlias, - ); + factory ParsedAddress.fetchOpenAliasAddress( + {required OpenaliasRecord record, required String name}) { + if (record.address.isEmpty) { + return ParsedAddress(addresses: [name]); + } + return ParsedAddress( + addresses: [record.address], + name: record.name, + description: record.description, + parseFrom: ParseFrom.openAlias, + ); } factory ParsedAddress.fetchFioAddress({required String address, required String name}){ @@ -65,6 +69,14 @@ class ParsedAddress { ); } + factory ParsedAddress.fetchContactAddress({required String address, required String name}){ + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.contact, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 3bbaf4941..c50629c1b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -31,23 +32,45 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; + static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; + static const syncModeKey = 'sync_mode'; + static const syncAllKey = 'sync_all'; static const pinTimeOutDuration = 'pin_timeout_duration'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; + static const sortBalanceBy = 'sort_balance_by'; + static const pinNativeTokenAtTop = 'pin_native_token_at_top'; + static const useEtherscan = 'use_etherscan'; - - - static String moneroWalletUpdateV1Key(String name) - => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; + static String moneroWalletUpdateV1Key(String name) => + '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; - static const clearnetDonationLink = 'clearnet_donation_link'; + static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; - static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; + static const shouldShowMarketPlaceInDashboard = + 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; + static const shouldRequireTOTP2FAForAccessingWallet = + 'should_require_totp_2fa_for_accessing_wallets'; + static const shouldRequireTOTP2FAForSendsToContact = + 'should_require_totp_2fa_for_sends_to_contact'; + static const shouldRequireTOTP2FAForSendsToNonContact = + 'should_require_totp_2fa_for_sends_to_non_contact'; + static const shouldRequireTOTP2FAForSendsToInternalWallets = + 'should_require_totp_2fa_for_sends_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToInternalWallets = + 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForAddingContacts = + 'should_require_totp_2fa_for_adding_contacts'; + static const shouldRequireTOTP2FAForCreatingNewWallets = + 'should_require_totp_2fa_for_creating_new_wallets'; + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + 'should_require_totp_2fa_for_all_security_and_backup_settings'; + static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; } diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 927ab8803..eb9417763 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,4 +1,5 @@ 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/monero/monero.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -14,6 +15,8 @@ List priorityForWalletType(WalletType type) { return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.haven: return haven!.getTransactionPriorities(); + case WalletType.ethereum: + return ethereum!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/sort_balance_types.dart b/lib/entities/sort_balance_types.dart new file mode 100644 index 000000000..5db64884e --- /dev/null +++ b/lib/entities/sort_balance_types.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum SortBalanceBy { + FiatBalance, + GrossBalance, + Alphabetical; + + @override + String toString() { + switch (this) { + case SortBalanceBy.FiatBalance: + return S.current.fiat_balance; + case SortBalanceBy.GrossBalance: + return S.current.gross_balance; + case SortBalanceBy.Alphabetical: + return S.current.alphabetical; + } + } +} \ No newline at end of file diff --git a/lib/entities/template.dart b/lib/entities/template.dart index c26e3a501..7cdd2c74a 100644 --- a/lib/entities/template.dart +++ b/lib/entities/template.dart @@ -1,19 +1,21 @@ +import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; part 'template.g.dart'; @HiveType(typeId: Template.typeId) class Template extends HiveObject { - Template({ - required this.nameRaw, - required this.isCurrencySelectedRaw, - required this.addressRaw, - required this.cryptoCurrencyRaw, - required this.amountRaw, - required this.fiatCurrencyRaw, - required this.amountFiatRaw}); + Template( + {required this.nameRaw, + required this.isCurrencySelectedRaw, + required this.addressRaw, + required this.cryptoCurrencyRaw, + required this.amountRaw, + required this.fiatCurrencyRaw, + required this.amountFiatRaw, + this.additionalRecipientsRaw}); - static const typeId = 6; + static const typeId = TEMPLATE_TYPE_ID; static const boxName = 'Template'; @HiveField(0) @@ -37,6 +39,9 @@ class Template extends HiveObject { @HiveField(6) String? amountFiatRaw; + @HiveField(7) + List