From 01150ef2a715296ab0f8a37f1d7aa4e46ff2c9fe Mon Sep 17 00:00:00 2001 From: mkyq <53115730+mkyq@users.noreply.github.com> Date: Wed, 30 Mar 2022 17:57:04 +0200 Subject: [PATCH] Hv (#295) * hv * Change build version --- .gitignore | 7 +- .../com/cakewallet/haven/Application.java | 11 + .../com/cakewallet/haven/MainActivity.java | 90 + assets/haven_node_list.yml | 6 + assets/images/haven_logo.png | Bin 0 -> 90638 bytes assets/images/haven_menu.png | Bin 0 -> 97503 bytes cw_bitcoin/lib/electrum_wallet.dart | 15 +- {cw_monero => cw_core}/lib/account.dart | 8 +- cw_core/lib/account_list.dart | 16 + cw_core/lib/crypto_currency.dart | 74 +- cw_core/lib/currency_for_wallet_type.dart | 2 + .../lib/get_height_by_date.dart | 0 .../lib/monero_amount_format.dart | 3 +- .../lib/monero_balance.dart | 2 +- .../lib/monero_transaction_priority.dart | 0 .../lib/monero_wallet_keys.dart | 0 .../lib/monero_wallet_utils.dart | 0 cw_core/lib/node.dart | 2 + cw_core/lib/subaddress.dart | 12 + .../lib/wallet_addresses_with_account.dart | 13 + cw_core/lib/wallet_base.dart | 3 +- cw_core/lib/wallet_type.dart | 18 +- cw_haven/.gitignore | 7 + cw_haven/.metadata | 10 + cw_haven/CHANGELOG.md | 3 + cw_haven/LICENSE | 1 + cw_haven/README.md | 15 + cw_haven/android/.gitignore | 8 + cw_haven/android/CMakeLists.txt | 220 +++ cw_haven/android/build.gradle | 45 + cw_haven/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + cw_haven/android/settings.gradle | 1 + cw_haven/android/src/main/AndroidManifest.xml | 3 + .../com/cakewallet/cw_haven/CwHavenPlugin.kt | 36 + cw_haven/ios/.gitignore | 37 + cw_haven/ios/Assets/.gitkeep | 0 cw_haven/ios/Classes/CwHavenPlugin.h | 4 + cw_haven/ios/Classes/CwHavenPlugin.m | 15 + cw_haven/ios/Classes/SwiftCwHavenPlugin.swift | 14 + cw_haven/ios/Classes/haven_api.cpp | 922 ++++++++++ cw_haven/ios/cw_haven.podspec | 50 + cw_haven/lib/api/account_list.dart | 83 + cw_haven/lib/api/asset_types.dart | 23 + cw_haven/lib/api/balance_list.dart | 58 + cw_haven/lib/api/convert_utf8_to_string.dart | 8 + cw_haven/lib/api/cw_haven.dart | 14 + .../connection_to_node_exception.dart | 5 + .../creation_transaction_exception.dart | 8 + .../exceptions/setup_wallet_exception.dart | 5 + .../exceptions/wallet_creation_exception.dart | 8 + .../exceptions/wallet_opening_exception.dart | 8 + .../wallet_restore_from_keys_exception.dart | 5 + .../wallet_restore_from_seed_exception.dart | 5 + cw_haven/lib/api/haven_api.dart | 6 + cw_haven/lib/api/monero_output.dart | 8 + cw_haven/lib/api/signatures.dart | 138 ++ cw_haven/lib/api/structs/account_row.dart | 11 + .../lib/api/structs/haven_balance_row.dart | 11 + cw_haven/lib/api/structs/haven_rate.dart | 11 + .../lib/api/structs/pending_transaction.dart | 23 + cw_haven/lib/api/structs/subaddress_row.dart | 13 + .../lib/api/structs/transaction_info_row.dart | 44 + cw_haven/lib/api/structs/ut8_box.dart | 8 + cw_haven/lib/api/subaddress_list.dart | 97 + cw_haven/lib/api/transaction_history.dart | 246 +++ cw_haven/lib/api/types.dart | 136 ++ cw_haven/lib/api/wallet.dart | 329 ++++ cw_haven/lib/api/wallet_manager.dart | 248 +++ cw_haven/lib/haven_account_list.dart | 84 + cw_haven/lib/haven_balance.dart | 34 + cw_haven/lib/haven_subaddress_list.dart | 87 + ...aven_transaction_creation_credentials.dart | 10 + .../haven_transaction_creation_exception.dart | 8 + cw_haven/lib/haven_transaction_history.dart | 27 + cw_haven/lib/haven_transaction_info.dart | 70 + cw_haven/lib/haven_wallet.dart | 388 ++++ cw_haven/lib/haven_wallet_addresses.dart | 86 + cw_haven/lib/haven_wallet_service.dart | 228 +++ .../lib/mnemonics/chinese_simplified.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/dutch.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/english.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/french.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/german.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/italian.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/japanese.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/portuguese.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/russian.dart | 1630 +++++++++++++++++ cw_haven/lib/mnemonics/spanish.dart | 1630 +++++++++++++++++ cw_haven/lib/pending_haven_transaction.dart | 48 + cw_haven/lib/update_haven_rate.dart | 15 + cw_haven/pubspec.lock | 609 ++++++ cw_haven/pubspec.yaml | 78 + cw_monero/android/CMakeLists.txt | 2 - cw_monero/android/build.gradle | 8 +- cw_monero/ios/cw_monero.podspec | 45 +- cw_monero/lib/monero_account_list.dart | 6 +- cw_monero/lib/monero_subaddress_list.dart | 10 +- ...nero_transaction_creation_credentials.dart | 4 +- cw_monero/lib/monero_transaction_info.dart | 2 +- cw_monero/lib/monero_wallet.dart | 50 +- cw_monero/lib/monero_wallet_addresses.dart | 4 +- cw_monero/lib/monero_wallet_service.dart | 2 +- cw_monero/lib/subaddress.dart | 22 - cw_shared_external/.gitignore | 7 + cw_shared_external/.metadata | 10 + cw_shared_external/LICENSE | 1 + cw_shared_external/README.md | 7 + cw_shared_external/android/.gitignore | 8 + cw_shared_external/android/build.gradle | 40 + cw_shared_external/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 5 + cw_shared_external/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../CwSharedExternalPlugin.kt | 36 + cw_shared_external/ios/.gitignore | 37 + cw_shared_external/ios/Assets/.gitkeep | 0 .../ios/Classes/CwSharedExternalPlugin.h | 4 + .../ios/Classes/CwSharedExternalPlugin.m | 15 + .../Classes/SwiftCwSharedExternalPlugin.swift | 14 + .../ios/cw_shared_external.podspec | 41 + .../lib/cw_shared_external.dart | 14 + cw_shared_external/pubspec.lock | 147 ++ cw_shared_external/pubspec.yaml | 26 + ios/Podfile.lock | 48 +- ios/Runner.xcodeproj/project.pbxproj | 6 +- ios/Runner/Runner.entitlements | 5 +- lib/core/address_validator.dart | 32 + lib/core/amount_converter.dart | 45 + lib/core/seed_validator.dart | 3 + lib/di.dart | 14 +- lib/entities/calculate_fiat_amount.dart | 13 +- lib/entities/default_settings_migration.dart | 43 + lib/entities/node_list.dart | 22 +- lib/entities/preferences_key.dart | 1 + lib/entities/update_haven_rate.dart | 21 + lib/haven/cw_haven.dart | 298 +++ lib/haven/haven.dart | 144 ++ lib/main.dart | 13 +- lib/monero/cw_monero.dart | 11 +- lib/reactions/fiat_rate_update.dart | 17 +- lib/reactions/on_current_wallet_change.dart | 9 +- .../on_wallet_sync_status_change.dart | 11 +- lib/router.dart | 16 +- lib/routes.dart | 1 + .../screens/contact/contact_list_page.dart | 3 + lib/src/screens/dashboard/dashboard_page.dart | 163 +- .../dashboard/widgets/action_button.dart | 13 +- .../dashboard/widgets/address_page.dart | 63 +- .../dashboard/widgets/balance_page.dart | 303 ++- .../dashboard/widgets/menu_widget.dart | 4 + .../new_wallet/new_wallet_type_page.dart | 4 + lib/src/screens/receive/receive_page.dart | 2 +- .../screens/receive/widgets/qr_widget.dart | 49 +- .../wallet_restore_from_seed_form.dart | 3 +- .../screens/restore/wallet_restore_page.dart | 2 +- lib/src/screens/send/send_page.dart | 78 +- lib/src/screens/send/widgets/send_card.dart | 4 +- .../screens/wallet_list/wallet_list_page.dart | 30 +- lib/src/screens/welcome/welcome_page.dart | 32 +- lib/src/widgets/blockchain_height_widget.dart | 75 +- lib/store/settings_store.dart | 6 +- .../dashboard/balance_view_model.dart | 172 +- .../dashboard/dashboard_view_model.dart | 34 + .../dashboard/transaction_list_item.dart | 33 +- .../exchange/exchange_view_model.dart | 2 +- ...ero_account_edit_or_create_view_model.dart | 41 +- .../monero_account_list_view_model.dart | 47 +- .../node_create_or_edit_view_model.dart | 3 +- lib/view_model/send/output.dart | 23 +- .../send/send_template_view_model.dart | 2 +- lib/view_model/send/send_view_model.dart | 35 +- .../settings/settings_view_model.dart | 17 +- .../transaction_details_view_model.dart | 22 + ...let_address_edit_or_create_view_model.dart | 22 + .../wallet_address_list_view_model.dart | 77 +- lib/view_model/wallet_keys_view_model.dart | 17 + .../wallet_list/wallet_list_view_model.dart | 3 +- lib/view_model/wallet_new_vm.dart | 6 +- lib/view_model/wallet_restore_view_model.dart | 44 +- lib/wallet_type_utils.dart | 26 +- pubspec_base.yaml | 1 + res/values/strings_de.arb | 3 + res/values/strings_en.arb | 2 + res/values/strings_es.arb | 3 + res/values/strings_hi.arb | 3 + res/values/strings_hr.arb | 3 + res/values/strings_it.arb | 3 + res/values/strings_ja.arb | 3 + res/values/strings_ko.arb | 3 + res/values/strings_nl.arb | 3 + res/values/strings_pl.arb | 6 + res/values/strings_pt.arb | 3 + res/values/strings_ru.arb | 3 + res/values/strings_uk.arb | 3 + res/values/strings_zh.arb | 3 + scripts/android/app_env.sh | 20 +- scripts/android/app_icon.sh | 11 +- scripts/android/build_all.sh | 20 +- scripts/android/build_haven.sh | 70 + scripts/android/build_haven_all.sh | 8 + scripts/android/build_monero_all.sh | 8 + scripts/android/copy_monero_deps.sh | 2 +- scripts/android/pubspec_gen.sh | 6 +- scripts/ios/app_config.sh | 6 +- scripts/ios/app_env.sh | 18 +- scripts/ios/app_icon.sh | 4 + scripts/ios/{build_deps.sh => build_all.sh} | 1 + scripts/ios/build_haven.sh | 62 + scripts/ios/build_haven_all.sh | 9 + scripts/ios/config.sh | 2 +- scripts/ios/setup.sh | 29 + tool/configure.dart | 204 ++- 213 files changed, 23972 insertions(+), 672 deletions(-) create mode 100644 android/app/src/main/java/com/cakewallet/haven/Application.java create mode 100644 android/app/src/main/java/com/cakewallet/haven/MainActivity.java create mode 100644 assets/haven_node_list.yml create mode 100644 assets/images/haven_logo.png create mode 100644 assets/images/haven_menu.png rename {cw_monero => cw_core}/lib/account.dart (59%) create mode 100644 cw_core/lib/account_list.dart rename {cw_monero => cw_core}/lib/get_height_by_date.dart (100%) rename {cw_monero => cw_core}/lib/monero_amount_format.dart (93%) rename {cw_monero => cw_core}/lib/monero_balance.dart (95%) rename {cw_monero => cw_core}/lib/monero_transaction_priority.dart (100%) rename {cw_monero => cw_core}/lib/monero_wallet_keys.dart (100%) rename {cw_monero => cw_core}/lib/monero_wallet_utils.dart (100%) create mode 100644 cw_core/lib/subaddress.dart create mode 100644 cw_core/lib/wallet_addresses_with_account.dart create mode 100644 cw_haven/.gitignore create mode 100644 cw_haven/.metadata create mode 100644 cw_haven/CHANGELOG.md create mode 100644 cw_haven/LICENSE create mode 100644 cw_haven/README.md create mode 100644 cw_haven/android/.gitignore create mode 100644 cw_haven/android/CMakeLists.txt create mode 100644 cw_haven/android/build.gradle create mode 100644 cw_haven/android/gradle.properties create mode 100644 cw_haven/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 cw_haven/android/settings.gradle create mode 100644 cw_haven/android/src/main/AndroidManifest.xml create mode 100644 cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt create mode 100644 cw_haven/ios/.gitignore create mode 100644 cw_haven/ios/Assets/.gitkeep create mode 100644 cw_haven/ios/Classes/CwHavenPlugin.h create mode 100644 cw_haven/ios/Classes/CwHavenPlugin.m create mode 100644 cw_haven/ios/Classes/SwiftCwHavenPlugin.swift create mode 100644 cw_haven/ios/Classes/haven_api.cpp create mode 100644 cw_haven/ios/cw_haven.podspec create mode 100644 cw_haven/lib/api/account_list.dart create mode 100644 cw_haven/lib/api/asset_types.dart create mode 100644 cw_haven/lib/api/balance_list.dart create mode 100644 cw_haven/lib/api/convert_utf8_to_string.dart create mode 100644 cw_haven/lib/api/cw_haven.dart create mode 100644 cw_haven/lib/api/exceptions/connection_to_node_exception.dart create mode 100644 cw_haven/lib/api/exceptions/creation_transaction_exception.dart create mode 100644 cw_haven/lib/api/exceptions/setup_wallet_exception.dart create mode 100644 cw_haven/lib/api/exceptions/wallet_creation_exception.dart create mode 100644 cw_haven/lib/api/exceptions/wallet_opening_exception.dart create mode 100644 cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart create mode 100644 cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart create mode 100644 cw_haven/lib/api/haven_api.dart create mode 100644 cw_haven/lib/api/monero_output.dart create mode 100644 cw_haven/lib/api/signatures.dart create mode 100644 cw_haven/lib/api/structs/account_row.dart create mode 100644 cw_haven/lib/api/structs/haven_balance_row.dart create mode 100644 cw_haven/lib/api/structs/haven_rate.dart create mode 100644 cw_haven/lib/api/structs/pending_transaction.dart create mode 100644 cw_haven/lib/api/structs/subaddress_row.dart create mode 100644 cw_haven/lib/api/structs/transaction_info_row.dart create mode 100644 cw_haven/lib/api/structs/ut8_box.dart create mode 100644 cw_haven/lib/api/subaddress_list.dart create mode 100644 cw_haven/lib/api/transaction_history.dart create mode 100644 cw_haven/lib/api/types.dart create mode 100644 cw_haven/lib/api/wallet.dart create mode 100644 cw_haven/lib/api/wallet_manager.dart create mode 100644 cw_haven/lib/haven_account_list.dart create mode 100644 cw_haven/lib/haven_balance.dart create mode 100644 cw_haven/lib/haven_subaddress_list.dart create mode 100644 cw_haven/lib/haven_transaction_creation_credentials.dart create mode 100644 cw_haven/lib/haven_transaction_creation_exception.dart create mode 100644 cw_haven/lib/haven_transaction_history.dart create mode 100644 cw_haven/lib/haven_transaction_info.dart create mode 100644 cw_haven/lib/haven_wallet.dart create mode 100644 cw_haven/lib/haven_wallet_addresses.dart create mode 100644 cw_haven/lib/haven_wallet_service.dart create mode 100644 cw_haven/lib/mnemonics/chinese_simplified.dart create mode 100644 cw_haven/lib/mnemonics/dutch.dart create mode 100644 cw_haven/lib/mnemonics/english.dart create mode 100644 cw_haven/lib/mnemonics/french.dart create mode 100644 cw_haven/lib/mnemonics/german.dart create mode 100644 cw_haven/lib/mnemonics/italian.dart create mode 100644 cw_haven/lib/mnemonics/japanese.dart create mode 100644 cw_haven/lib/mnemonics/portuguese.dart create mode 100644 cw_haven/lib/mnemonics/russian.dart create mode 100644 cw_haven/lib/mnemonics/spanish.dart create mode 100644 cw_haven/lib/pending_haven_transaction.dart create mode 100644 cw_haven/lib/update_haven_rate.dart create mode 100644 cw_haven/pubspec.lock create mode 100644 cw_haven/pubspec.yaml delete mode 100644 cw_monero/lib/subaddress.dart create mode 100644 cw_shared_external/.gitignore create mode 100644 cw_shared_external/.metadata create mode 100644 cw_shared_external/LICENSE create mode 100644 cw_shared_external/README.md create mode 100644 cw_shared_external/android/.gitignore create mode 100644 cw_shared_external/android/build.gradle create mode 100644 cw_shared_external/android/gradle.properties create mode 100644 cw_shared_external/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 cw_shared_external/android/settings.gradle create mode 100644 cw_shared_external/android/src/main/AndroidManifest.xml create mode 100644 cw_shared_external/android/src/main/kotlin/com/cakewallet/cw_shared_external/CwSharedExternalPlugin.kt create mode 100644 cw_shared_external/ios/.gitignore create mode 100644 cw_shared_external/ios/Assets/.gitkeep create mode 100644 cw_shared_external/ios/Classes/CwSharedExternalPlugin.h create mode 100644 cw_shared_external/ios/Classes/CwSharedExternalPlugin.m create mode 100644 cw_shared_external/ios/Classes/SwiftCwSharedExternalPlugin.swift create mode 100644 cw_shared_external/ios/cw_shared_external.podspec create mode 100644 cw_shared_external/lib/cw_shared_external.dart create mode 100644 cw_shared_external/pubspec.lock create mode 100644 cw_shared_external/pubspec.yaml create mode 100644 lib/entities/update_haven_rate.dart create mode 100644 lib/haven/cw_haven.dart create mode 100644 lib/haven/haven.dart create mode 100755 scripts/android/build_haven.sh create mode 100755 scripts/android/build_haven_all.sh create mode 100755 scripts/android/build_monero_all.sh rename scripts/ios/{build_deps.sh => build_all.sh} (84%) create mode 100755 scripts/ios/build_haven.sh create mode 100755 scripts/ios/build_haven_all.sh create mode 100755 scripts/ios/setup.sh diff --git a/.gitignore b/.gitignore index 69cae6503..d929b12dc 100644 --- a/.gitignore +++ b/.gitignore @@ -111,8 +111,11 @@ ios/build *.sublime-project shared_external/** -cw_shared_external/** -cw_haven/** +cw_shared_external/ios/External/ +# cw_haven/** +cw_haven/ios/External/ +cw_haven/android/.externalNativeBuild/ +cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart diff --git a/android/app/src/main/java/com/cakewallet/haven/Application.java b/android/app/src/main/java/com/cakewallet/haven/Application.java new file mode 100644 index 000000000..d88eb66b4 --- /dev/null +++ b/android/app/src/main/java/com/cakewallet/haven/Application.java @@ -0,0 +1,11 @@ +package com.cakewallet.haven; + +import io.flutter.app.FlutterApplication; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class Application extends FlutterApplication implements PluginRegistrantCallback { + @Override + public void registerWith(PluginRegistry registry) {} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cakewallet/haven/MainActivity.java b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java new file mode 100644 index 000000000..065af9318 --- /dev/null +++ b/android/app/src/main/java/com/cakewallet/haven/MainActivity.java @@ -0,0 +1,90 @@ +package com.cakewallet.haven; + +import androidx.annotation.NonNull; + +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.GeneratedPluginRegistrant; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +import android.os.AsyncTask; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.view.WindowManager; + +import com.unstoppabledomains.resolution.DomainResolution; +import com.unstoppabledomains.resolution.Resolution; + +import java.security.SecureRandom; + +public class MainActivity extends FlutterFragmentActivity { + final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; + final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + + MethodChannel utilsChannel = + new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), + UTILS_CHANNEL); + + utilsChannel.setMethodCallHandler(this::handle); + } + + private void handle(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + Handler handler = new Handler(Looper.getMainLooper()); + + try { + switch (call.method) { + case "enableWakeScreen": + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + handler.post(() -> result.success(true)); + break; + case "disableWakeScreen": + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + handler.post(() -> result.success(true)); + break; + case "sec_random": + int count = call.argument("count"); + SecureRandom random = new SecureRandom(); + byte bytes[] = new byte[count]; + random.nextBytes(bytes); + handler.post(() -> result.success(bytes)); + break; + case "getUnstoppableDomainAddress": + int version = Build.VERSION.SDK_INT; + if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { + getUnstoppableDomainAddress(call, result); + } else { + handler.post(() -> result.success("")); + } + break; + default: + handler.post(() -> result.notImplemented()); + } + } catch (Exception e) { + handler.post(() -> result.error("UNCAUGHT_ERROR", e.getMessage(), null)); + } + } + + private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + DomainResolution resolution = new Resolution(); + Handler handler = new Handler(Looper.getMainLooper()); + String domain = call.argument("domain"); + String ticker = call.argument("ticker"); + + AsyncTask.execute(() -> { + try { + String address = resolution.getAddress(domain, ticker); + handler.post(() -> result.success(address)); + } catch (Exception e) { + System.out.println("Expected Address, but got " + e.getMessage()); + handler.post(() -> result.success("")); + } + }); + } +} \ No newline at end of file diff --git a/assets/haven_node_list.yml b/assets/haven_node_list.yml new file mode 100644 index 000000000..7c6cb623c --- /dev/null +++ b/assets/haven_node_list.yml @@ -0,0 +1,6 @@ +- + uri: vault.havenprotocol.org:443 + login: super + password: super + useSSL: true + is_default: true \ No newline at end of file diff --git a/assets/images/haven_logo.png b/assets/images/haven_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e16c05df1e2fdec174c27a45cc4c695c377c0f7a GIT binary patch literal 90638 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%0AR$?IdAjv%CowY=n;?Bz&+f_khs^ zsV$|?LJBUR)7va$g!I76Dt0wlGG%=^d~^o*a9eWwsi0cicP)rYB()ncUoQxtQmL!z@Tn* z{mn^z*==NHp;I80wX0gSff=NXHEv^6RluPdc#o4(i1G7(&k;_pUB(+Ly~qp3>c zX-uVayV2IvgF{0UNI?mi%1{U}Y}ot8s0HF2`ijga5rYy?BcC7zp>R%0eoLdvH*@u| z66)Ok`rmF|cYNgtZg5x3VN$V~cY7We?iM-5PHPU=-ize-uSkJw43;x1;#TarR~LGa zIW39MUHBLRdX{45<@J}YhS8%-nO#mabvoqSKs{ShtnvswWq(x6sJ@J@gwTf=7KAQU zWGWTIl{PXVU+_lQ+7MAlz5=Sc>Hpf$zhsBl1Qvhfb;|{a9U&0kUQwR&t@y+LY}6Ne z1TQ6>!jNcTJrfA2a){IhXg6?Eu1CF5fyhHa>;YfX^ZtCgzq&B+wATUf(5t}bE~7@8 zRFHl3HlJq{HV>U(jPp5{Ye2%5DKf$7R6V!eVQ>x&Ng}NjH?{Mr^b_v;;EA54PXV3z z%r`%^kf#MuWTD?JHs6JG?)KK^oFR&gq~yKePe)@y8Kwu-0|@~XNMQiqaR9FnzY(4w z82tYAf)`Z%)ð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

    3uf&$D8@y8^J(l5J`w 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[], _isTransactionUpdating = false, super(walletInfo) { + balance = ObservableMap.of({ + currency: initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0)}); this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; this.unspentCoinsInfo = unspentCoinsInfo; @@ -82,7 +83,7 @@ abstract class ElectrumWalletBase extends WalletBase balance; @override @observable @@ -233,7 +234,7 @@ abstract class ElectrumWalletBase extends WalletBase balance.confirmed || totalAmount > allInputsAmount) { + if (totalAmount > balance[currency].confirmed || totalAmount > allInputsAmount) { throw BitcoinTransactionWrongBalanceException(currency); } @@ -326,7 +327,7 @@ abstract class ElectrumWalletBase extends WalletBase addr.toJSON()).toList(), - 'balance': balance?.toJSON() + 'balance': balance[currency]?.toJSON() }); int feeRate(TransactionPriority priority) { @@ -617,7 +618,7 @@ abstract class ElectrumWalletBase extends WalletBase _updateBalance() async { - balance = await _fetchBalances(); + balance[currency] = await _fetchBalances(); await save(); } diff --git a/cw_monero/lib/account.dart b/cw_core/lib/account.dart similarity index 59% rename from cw_monero/lib/account.dart rename to cw_core/lib/account.dart index 04b2f0f23..5bb002945 100644 --- a/cw_monero/lib/account.dart +++ b/cw_core/lib/account.dart @@ -1,5 +1,3 @@ -import 'package:cw_monero/api/structs/account_row.dart'; - class Account { Account({this.id, this.label}); @@ -7,10 +5,6 @@ class Account { : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), this.label = (map['label'] ?? '') as String; - Account.fromRow(AccountRow row) - : this.id = row.getId(), - this.label = row.getLabel(); - final int id; final String label; -} +} \ No newline at end of file diff --git a/cw_core/lib/account_list.dart b/cw_core/lib/account_list.dart new file mode 100644 index 000000000..4ac5faa3e --- /dev/null +++ b/cw_core/lib/account_list.dart @@ -0,0 +1,16 @@ +import 'package:mobx/mobx.dart'; + +abstract class AccountList { + + ObservableList get accounts; + + void update(); + + List getAll(); + + Future addAccount({String label}); + + Future setLabelAccount({int accountIndex, String label}); + + void refresh(); +} diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 457564cbf..ddcdb5856 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -23,7 +23,8 @@ class CryptoCurrency extends EnumerableItem with Serializable { CryptoCurrency.usdt, CryptoCurrency.usdterc20, CryptoCurrency.xlm, - CryptoCurrency.xrp + CryptoCurrency.xrp, + CryptoCurrency.xhv ]; static const xmr = CryptoCurrency(title: 'XMR', raw: 0); static const ada = CryptoCurrency(title: 'ADA', raw: 1); @@ -41,6 +42,21 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const usdterc20 = CryptoCurrency(title: 'USDTERC20', raw: 13); static const xlm = CryptoCurrency(title: 'XLM', raw: 14); static const xrp = CryptoCurrency(title: 'XRP', raw: 15); + static const xhv = CryptoCurrency(title: 'XHV', raw: 16); + + static const xag = CryptoCurrency(title: 'XAG', raw: 17); + static const xau = CryptoCurrency(title: 'XAU', raw: 18); + static const xaud = CryptoCurrency(title: 'XAUD', raw: 19); + static const xbtc = CryptoCurrency(title: 'XBTC', raw: 20); + static const xcad = CryptoCurrency(title: 'XCAD', raw: 21); + static const xchf = CryptoCurrency(title: 'XCHF', raw: 22); + static const xcny = CryptoCurrency(title: 'XCNY', raw: 23); + static const xeur = CryptoCurrency(title: 'XEUR', raw: 24); + static const xgbp = CryptoCurrency(title: 'XGBP', raw: 25); + static const xjpy = CryptoCurrency(title: 'XJPY', raw: 26); + static const xnok = CryptoCurrency(title: 'XNOK', raw: 27); + static const xnzd = CryptoCurrency(title: 'XNZD', raw: 28); + static const xusd = CryptoCurrency(title: 'XUSD', raw: 29); static CryptoCurrency deserialize({int raw}) { switch (raw) { @@ -76,6 +92,34 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.xlm; case 15: return CryptoCurrency.xrp; + case 16: + return CryptoCurrency.xhv; + case 17: + return CryptoCurrency.xag; + case 18: + return CryptoCurrency.xau; + case 19: + return CryptoCurrency.xaud; + case 20: + return CryptoCurrency.xbtc; + case 21: + return CryptoCurrency.xcad; + case 22: + return CryptoCurrency.xchf; + case 23: + return CryptoCurrency.xcny; + case 24: + return CryptoCurrency.xeur; + case 25: + return CryptoCurrency.xgbp; + case 26: + return CryptoCurrency.xjpy; + case 27: + return CryptoCurrency.xnok; + case 28: + return CryptoCurrency.xnzd; + case 29: + return CryptoCurrency.xusd; default: return null; } @@ -115,6 +159,34 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.xlm; case 'xrp': return CryptoCurrency.xrp; + case 'xhv': + return CryptoCurrency.xhv; + case 'xag': + return CryptoCurrency.xag; + case 'xau': + return CryptoCurrency.xau; + case 'xaud': + return CryptoCurrency.xaud; + case 'xbtc': + return CryptoCurrency.xbtc; + case 'xcad': + return CryptoCurrency.xcad; + case 'xchf': + return CryptoCurrency.xchf; + case 'xcny': + return CryptoCurrency.xcny; + case 'xeur': + return CryptoCurrency.xeur; + case 'xgbp': + return CryptoCurrency.xgbp; + case 'xjpy': + return CryptoCurrency.xjpy; + case 'xnok': + return CryptoCurrency.xnok; + case 'xnzd': + return CryptoCurrency.xnzd; + case 'xusd': + return CryptoCurrency.xusd; default: return null; } diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 2816a0ca8..b6d1f18c0 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -9,6 +9,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.xmr; case WalletType.litecoin: return CryptoCurrency.ltc; + case WalletType.haven: + return CryptoCurrency.xhv; default: return null; } diff --git a/cw_monero/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart similarity index 100% rename from cw_monero/lib/get_height_by_date.dart rename to cw_core/lib/get_height_by_date.dart diff --git a/cw_monero/lib/monero_amount_format.dart b/cw_core/lib/monero_amount_format.dart similarity index 93% rename from cw_monero/lib/monero_amount_format.dart rename to cw_core/lib/monero_amount_format.dart index ab8ef891c..92bf0da25 100644 --- a/cw_monero/lib/monero_amount_format.dart +++ b/cw_core/lib/monero_amount_format.dart @@ -8,7 +8,8 @@ final moneroAmountFormat = NumberFormat() ..minimumFractionDigits = 1; String moneroAmountToString({int amount}) => moneroAmountFormat - .format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)); + .format(cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider)) + .replaceAll(',', ''); double moneroAmountToDouble({int amount}) => cryptoAmountToDouble(amount: amount, divider: moneroAmountDivider); diff --git a/cw_monero/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart similarity index 95% rename from cw_monero/lib/monero_balance.dart rename to cw_core/lib/monero_balance.dart index 00a66aa91..8f0ae610e 100644 --- a/cw_monero/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -1,6 +1,6 @@ import 'package:cw_core/balance.dart'; import 'package:flutter/foundation.dart'; -import 'package:cw_monero/monero_amount_format.dart'; +import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { MoneroBalance({@required this.fullBalance, @required this.unlockedBalance}) diff --git a/cw_monero/lib/monero_transaction_priority.dart b/cw_core/lib/monero_transaction_priority.dart similarity index 100% rename from cw_monero/lib/monero_transaction_priority.dart rename to cw_core/lib/monero_transaction_priority.dart diff --git a/cw_monero/lib/monero_wallet_keys.dart b/cw_core/lib/monero_wallet_keys.dart similarity index 100% rename from cw_monero/lib/monero_wallet_keys.dart rename to cw_core/lib/monero_wallet_keys.dart diff --git a/cw_monero/lib/monero_wallet_utils.dart b/cw_core/lib/monero_wallet_utils.dart similarity index 100% rename from cw_monero/lib/monero_wallet_utils.dart rename to cw_core/lib/monero_wallet_utils.dart diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index ff1da7026..2560a72d7 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -60,6 +60,8 @@ class Node extends HiveObject with Keyable { return createUriFromElectrumAddress(uriRaw); case WalletType.litecoin: return createUriFromElectrumAddress(uriRaw); + case WalletType.haven: + return Uri.http(uriRaw, ''); default: return null; } diff --git a/cw_core/lib/subaddress.dart b/cw_core/lib/subaddress.dart new file mode 100644 index 000000000..0f884dfa4 --- /dev/null +++ b/cw_core/lib/subaddress.dart @@ -0,0 +1,12 @@ +class Subaddress { + Subaddress({this.id, this.address, this.label}); + + Subaddress.fromMap(Map map) + : this.id = map['id'] == null ? 0 : int.parse(map['id'] as String), + this.address = (map['address'] ?? '') as String, + this.label = (map['label'] ?? '') as String; + + final int id; + final String address; + final String label; +} diff --git a/cw_core/lib/wallet_addresses_with_account.dart b/cw_core/lib/wallet_addresses_with_account.dart new file mode 100644 index 000000000..5691ad320 --- /dev/null +++ b/cw_core/lib/wallet_addresses_with_account.dart @@ -0,0 +1,13 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/account_list.dart'; +import 'package:cw_core/wallet_info.dart'; + +abstract class WalletAddressesWithAccount extends WalletAddresses { + WalletAddressesWithAccount(WalletInfo walletInfo) : super(walletInfo); + + T get account; + + set account(T account); + + AccountList get accountList; +} \ No newline at end of file diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index a9eb37580..b8c1aaa45 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -1,3 +1,4 @@ +import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -35,7 +36,7 @@ abstract class WalletBase< //set address(String address); - BalanceType get balance; + ObservableMap get balance; SyncStatus get syncStatus; diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 882a8c92d..6a39fa63f 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -6,7 +6,8 @@ part 'wallet_type.g.dart'; const walletTypes = [ WalletType.monero, WalletType.bitcoin, - WalletType.litecoin + WalletType.litecoin, + WalletType.haven ]; const walletTypeTypeId = 5; @@ -22,7 +23,10 @@ enum WalletType { bitcoin, @HiveField(3) - litecoin + litecoin, + + @HiveField(4) + haven } int serializeToInt(WalletType type) { @@ -33,6 +37,8 @@ int serializeToInt(WalletType type) { return 1; case WalletType.litecoin: return 2; + case WalletType.haven: + return 3; default: return -1; } @@ -46,6 +52,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.bitcoin; case 2: return WalletType.litecoin; + case 3: + return WalletType.haven; default: return null; } @@ -59,6 +67,8 @@ String walletTypeToString(WalletType type) { return 'Bitcoin'; case WalletType.litecoin: return 'Litecoin'; + case WalletType.haven: + return 'Haven'; default: return ''; } @@ -72,6 +82,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Bitcoin (Electrum)'; case WalletType.litecoin: return 'Litecoin (Electrum)'; + case WalletType.haven: + return 'Haven'; default: return ''; } @@ -85,6 +97,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.btc; case WalletType.litecoin: return CryptoCurrency.ltc; + case WalletType.haven: + return CryptoCurrency.xhv; default: return null; } diff --git a/cw_haven/.gitignore b/cw_haven/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/cw_haven/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/cw_haven/.metadata b/cw_haven/.metadata new file mode 100644 index 000000000..cb1a29e7c --- /dev/null +++ b/cw_haven/.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: 4d7946a68d26794349189cf21b3f68cc6fe61dcb + channel: stable + +project_type: plugin diff --git a/cw_haven/CHANGELOG.md b/cw_haven/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_haven/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_haven/LICENSE b/cw_haven/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_haven/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_haven/README.md b/cw_haven/README.md new file mode 100644 index 000000000..150aebc42 --- /dev/null +++ b/cw_haven/README.md @@ -0,0 +1,15 @@ +# cw_haven + +A new flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/cw_haven/android/.gitignore b/cw_haven/android/.gitignore new file mode 100644 index 000000000..c6cbe562a --- /dev/null +++ b/cw_haven/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/cw_haven/android/CMakeLists.txt b/cw_haven/android/CMakeLists.txt new file mode 100644 index 000000000..83d93faa0 --- /dev/null +++ b/cw_haven/android/CMakeLists.txt @@ -0,0 +1,220 @@ +cmake_minimum_required(VERSION 3.4.1) + +add_library( cw_haven + SHARED + ../ios/Classes/haven_api.cpp) + + find_library( log-lib log ) + +set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../ios/External/android) + +############ +# libsodium +############ + +add_library(sodium STATIC IMPORTED) +set_target_properties(sodium PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libsodium.a) + +############ +# OpenSSL +############ + +add_library(crypto STATIC IMPORTED) +set_target_properties(crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libcrypto.a) + +add_library(ssl STATIC IMPORTED) +set_target_properties(ssl PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libssl.a) + +############ +# Boost +############ + +add_library(boost_chrono STATIC IMPORTED) +set_target_properties(boost_chrono PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_chrono.a) + +add_library(boost_date_time STATIC IMPORTED) +set_target_properties(boost_date_time PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_date_time.a) + +add_library(boost_filesystem STATIC IMPORTED) +set_target_properties(boost_filesystem PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_filesystem.a) + +add_library(boost_program_options STATIC IMPORTED) +set_target_properties(boost_program_options PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_program_options.a) + +add_library(boost_regex STATIC IMPORTED) +set_target_properties(boost_regex PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_regex.a) + +add_library(boost_serialization STATIC IMPORTED) +set_target_properties(boost_serialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_serialization.a) + +add_library(boost_system STATIC IMPORTED) +set_target_properties(boost_system PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_system.a) + +add_library(boost_thread STATIC IMPORTED) +set_target_properties(boost_thread PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_thread.a) + +add_library(boost_wserialization STATIC IMPORTED) +set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/libboost_wserialization.a) + +############# +# Haven +############# + +add_library(wallet_api STATIC IMPORTED) +set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet_api.a) + +add_library(wallet STATIC IMPORTED) +set_target_properties(wallet PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet.a) + +add_library(cryptonote_core STATIC IMPORTED) +set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcryptonote_core.a) + +add_library(cryptonote_basic STATIC IMPORTED) +set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcryptonote_basic.a) + +add_library(mnemonics STATIC IMPORTED) +set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libmnemonics.a) + +add_library(common STATIC IMPORTED) +set_target_properties(common PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcommon.a) + +add_library(cncrypto STATIC IMPORTED) +set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcncrypto.a) + +add_library(ringct STATIC IMPORTED) +set_target_properties(ringct PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libringct.a) + +add_library(ringct_basic STATIC IMPORTED) +set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libringct_basic.a) + +add_library(blockchain_db STATIC IMPORTED) +set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libblockchain_db.a) + +add_library(lmdb STATIC IMPORTED) +set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/liblmdb.a) + +add_library(easylogging STATIC IMPORTED) +set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libeasylogging.a) + +add_library(unbound STATIC IMPORTED) +set_target_properties(unbound PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libunbound.a) + +add_library(epee STATIC IMPORTED) +set_target_properties(epee PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libepee.a) + +add_library(checkpoints STATIC IMPORTED) +set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libcheckpoints.a) + +add_library(device STATIC IMPORTED) +set_target_properties(device PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libdevice.a) + +add_library(device_trezor STATIC IMPORTED) +set_target_properties(device_trezor PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libdevice_trezor.a) + +add_library(multisig STATIC IMPORTED) +set_target_properties(multisig PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libmultisig.a) + +add_library(version STATIC IMPORTED) +set_target_properties(version PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libversion.a) + +add_library(net STATIC IMPORTED) +set_target_properties(net PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libnet.a) + +add_library(hardforks STATIC IMPORTED) +set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libhardforks.a) + +add_library(randomx STATIC IMPORTED) +set_target_properties(randomx PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/librandomx.a) + +add_library(offshore STATIC IMPORTED) +set_target_properties(offshore PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/liboffshore.a) + + +add_library(rpc_base STATIC IMPORTED) +set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/librpc_base.a) + +add_library(wallet-crypto STATIC IMPORTED) +set_target_properties(wallet-crypto PROPERTIES IMPORTED_LOCATION + ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/lib/haven/libwallet-crypto.a) + +include_directories( ${EXTERNAL_LIBS_DIR}/${ANDROID_ABI}/include ) + +target_link_libraries( cw_haven + + wallet_api + wallet + cryptonote_core + cryptonote_basic + mnemonics + ringct + ringct_basic + net + common + cncrypto + blockchain_db + lmdb + easylogging + unbound + epee + checkpoints + device + device_trezor + multisig + version + randomx + offshore + hardforks + rpc_base + + boost_chrono + boost_date_time + boost_filesystem + boost_program_options + boost_regex + boost_serialization + boost_system + boost_thread + boost_wserialization + + ssl + crypto + + sodium + + ${log-lib} ) \ No newline at end of file diff --git a/cw_haven/android/build.gradle b/cw_haven/android/build.gradle new file mode 100644 index 000000000..91da1b857 --- /dev/null +++ b/cw_haven/android/build.gradle @@ -0,0 +1,45 @@ +group 'com.cakewallet.cw_haven' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 21 + } + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/cw_haven/android/gradle.properties b/cw_haven/android/gradle.properties new file mode 100644 index 000000000..94adc3a3f --- /dev/null +++ b/cw_haven/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/cw_haven/android/gradle/wrapper/gradle-wrapper.properties b/cw_haven/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..3c9d0852b --- /dev/null +++ b/cw_haven/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/cw_haven/android/settings.gradle b/cw_haven/android/settings.gradle new file mode 100644 index 000000000..2a0a2fea8 --- /dev/null +++ b/cw_haven/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_haven' diff --git a/cw_haven/android/src/main/AndroidManifest.xml b/cw_haven/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b72b21d5d --- /dev/null +++ b/cw_haven/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt b/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt new file mode 100644 index 000000000..b31493c6e --- /dev/null +++ b/cw_haven/android/src/main/kotlin/com/cakewallet/cw_haven/CwHavenPlugin.kt @@ -0,0 +1,36 @@ +package com.cakewallet.cw_haven + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar + +/** CwHavenPlugin */ +class CwHavenPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_haven") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_haven/ios/.gitignore b/cw_haven/ios/.gitignore new file mode 100644 index 000000000..aa479fd3c --- /dev/null +++ b/cw_haven/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_haven/ios/Assets/.gitkeep b/cw_haven/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_haven/ios/Classes/CwHavenPlugin.h b/cw_haven/ios/Classes/CwHavenPlugin.h new file mode 100644 index 000000000..8a4195232 --- /dev/null +++ b/cw_haven/ios/Classes/CwHavenPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface CwHavenPlugin : NSObject +@end diff --git a/cw_haven/ios/Classes/CwHavenPlugin.m b/cw_haven/ios/Classes/CwHavenPlugin.m new file mode 100644 index 000000000..4683f4b68 --- /dev/null +++ b/cw_haven/ios/Classes/CwHavenPlugin.m @@ -0,0 +1,15 @@ +#import "CwHavenPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "cw_haven-Swift.h" +#endif + +@implementation CwHavenPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftCwHavenPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift b/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift new file mode 100644 index 000000000..ddee88ae7 --- /dev/null +++ b/cw_haven/ios/Classes/SwiftCwHavenPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftCwHavenPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_haven", binaryMessenger: registrar.messenger()) + let instance = SwiftCwHavenPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/cw_haven/ios/Classes/haven_api.cpp b/cw_haven/ios/Classes/haven_api.cpp new file mode 100644 index 000000000..39d834cec --- /dev/null +++ b/cw_haven/ios/Classes/haven_api.cpp @@ -0,0 +1,922 @@ +#include +#include "cstdlib" +#include +#include +#include +#include +#include +#include "thread" +#if __APPLE__ +// Fix for randomx on ios +void __clear_cache(void* start, void* end) { } +#include "../External/ios/include/wallet2_api.h" +#else +#include "../External/android/x86/include/wallet2_api.h" +#endif + +using namespace std::chrono_literals; + +#ifdef __cplusplus +extern "C" +{ +#endif + const uint64_t MONERO_BLOCK_SIZE = 1000; + + struct Utf8Box + { + char *value; + + Utf8Box(char *_value) + { + value = _value; + } + }; + + + struct SubaddressRow + { + uint64_t id; + char *address; + char *label; + + SubaddressRow(std::size_t _id, char *_address, char *_label) + { + id = static_cast(_id); + address = _address; + label = _label; + } + }; + + struct AccountRow + { + uint64_t id; + char *label; + + AccountRow(std::size_t _id, char *_label) + { + id = static_cast(_id); + label = _label; + } + }; + + struct HavenBalance + { + uint64_t amount; + char *assetType; + + HavenBalance(char *_assetType, uint64_t _amount) + { + amount = _amount; + assetType = _assetType; + } + }; + + struct HavenRate + { + uint64_t rate; + char *assetType; + + HavenRate(char *_assetType, uint64_t _rate) + { + rate = _rate; + assetType = _assetType; + } + }; + + struct MoneroWalletListener : Monero::WalletListener + { + uint64_t m_height; + bool m_need_to_refresh; + bool m_new_transaction; + + MoneroWalletListener() + { + m_height = 0; + m_need_to_refresh = false; + m_new_transaction = false; + } + + void moneySpent(const std::string &txId, uint64_t amount, std::string assetType) + { + m_new_transaction = true; + } + + void moneyReceived(const std::string &txId, uint64_t amount, std::string assetType) + { + m_new_transaction = true; + } + + void unconfirmedMoneyReceived(const std::string &txId, uint64_t amount) + { + m_new_transaction = true; + } + + void newBlock(uint64_t height) + { + m_height = height; + } + + void updated() + { + m_new_transaction = true; + } + + void refreshed() + { + m_need_to_refresh = true; + } + + void resetNeedToRefresh() + { + m_need_to_refresh = false; + } + + bool isNeedToRefresh() + { + return m_need_to_refresh; + } + + bool isNewTransactionExist() + { + return m_new_transaction; + } + + void resetIsNewTransactionExist() + { + m_new_transaction = false; + } + + uint64_t height() + { + return m_height; + } + }; + + struct TransactionInfoRow + { + uint64_t amount; + uint64_t fee; + uint64_t blockHeight; + uint64_t confirmations; + uint32_t subaddrAccount; + int8_t direction; + int8_t isPending; + uint32_t subaddrIndex; + + char *hash; + char *paymentId; + char *assetType; + + int64_t datetime; + + TransactionInfoRow(Monero::TransactionInfo *transaction) + { + amount = transaction->amount(); + fee = transaction->fee(); + blockHeight = transaction->blockHeight(); + subaddrAccount = transaction->subaddrAccount(); + std::set::iterator it = transaction->subaddrIndex().begin(); + subaddrIndex = *it; + confirmations = transaction->confirmations(); + datetime = static_cast(transaction->timestamp()); + direction = transaction->direction(); + isPending = static_cast(transaction->isPending()); + std::string *hash_str = new std::string(transaction->hash()); + hash = strdup(hash_str->c_str()); + paymentId = strdup(transaction->paymentId().c_str()); + assetType = strdup(transaction->assetType().c_str()); + } + }; + + struct PendingTransactionRaw + { + uint64_t amount; + uint64_t fee; + char *hash; + Monero::PendingTransaction *transaction; + + PendingTransactionRaw(Monero::PendingTransaction *_transaction) + { + transaction = _transaction; + amount = _transaction->amount(); + fee = _transaction->fee(); + hash = strdup(_transaction->txid()[0].c_str()); + } + }; + + Monero::Wallet *m_wallet; + Monero::TransactionHistory *m_transaction_history; + MoneroWalletListener *m_listener; + Monero::Subaddress *m_subaddress; + Monero::SubaddressAccount *m_account; + uint64_t m_last_known_wallet_height; + uint64_t m_cached_syncing_blockchain_height = 0; + std::mutex store_lock; + bool is_storing = false; + + void change_current_wallet(Monero::Wallet *wallet) + { + m_wallet = wallet; + m_listener = nullptr; + + + if (wallet != nullptr) + { + m_transaction_history = wallet->history(); + } + else + { + m_transaction_history = nullptr; + } + + if (wallet != nullptr) + { + m_account = wallet->subaddressAccount(); + } + else + { + m_account = nullptr; + } + + if (wallet != nullptr) + { + m_subaddress = wallet->subaddress(); + } + else + { + m_subaddress = nullptr; + } + } + + Monero::Wallet *get_current_wallet() + { + return m_wallet; + } + + bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) + { + Monero::WalletManagerFactory::setLogLevel(4); + + Monero::NetworkType _networkType = static_cast(networkType); + Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); + Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (wallet->status() != Monero::Wallet::Status_Ok) + { + error = strdup(wallet->errorString().c_str()); + return false; + } + + change_current_wallet(wallet); + + return true; + } + + bool restore_wallet_from_seed(char *path, char *password, char *seed, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->recoveryWallet( + std::string(path), + std::string(password), + std::string(seed), + _networkType, + (uint64_t)restoreHeight); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + + bool restore_wallet_from_keys(char *path, char *password, char *language, char *address, char *viewKey, char *spendKey, int32_t networkType, uint64_t restoreHeight, char *error) + { + Monero::NetworkType _networkType = static_cast(networkType); + Monero::Wallet *wallet = Monero::WalletManagerFactory::getWalletManager()->createWalletFromKeys( + std::string(path), + std::string(password), + std::string(language), + _networkType, + (uint64_t)restoreHeight, + std::string(address), + std::string(viewKey), + std::string(spendKey)); + + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) + { + error = strdup(errorString.c_str()); + return false; + } + + change_current_wallet(wallet); + return true; + } + + bool load_wallet(char *path, char *password, int32_t nettype) + { + nice(19); + Monero::NetworkType networkType = static_cast(nettype); + Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); + Monero::Wallet *wallet = walletManager->openWallet(std::string(path), std::string(password), networkType); + int status; + std::string errorString; + + wallet->statusWithErrorString(status, errorString); + change_current_wallet(wallet); + + return !(status != Monero::Wallet::Status_Ok || !errorString.empty()); + } + + char *error_string() { + return strdup(get_current_wallet()->errorString().c_str()); + } + + + bool is_wallet_exist(char *path) + { + return Monero::WalletManagerFactory::getWalletManager()->walletExists(std::string(path)); + } + + void close_current_wallet() + { + Monero::WalletManagerFactory::getWalletManager()->closeWallet(get_current_wallet()); + change_current_wallet(nullptr); + } + + char *get_filename() + { + return strdup(get_current_wallet()->filename().c_str()); + } + + char *secret_view_key() + { + return strdup(get_current_wallet()->secretViewKey().c_str()); + } + + char *public_view_key() + { + return strdup(get_current_wallet()->publicViewKey().c_str()); + } + + char *secret_spend_key() + { + return strdup(get_current_wallet()->secretSpendKey().c_str()); + } + + char *public_spend_key() + { + return strdup(get_current_wallet()->publicSpendKey().c_str()); + } + + char *get_address(uint32_t account_index, uint32_t address_index) + { + return strdup(get_current_wallet()->address(account_index, address_index).c_str()); + } + + + const char *seed() + { + return strdup(get_current_wallet()->seed().c_str()); + } + + int64_t *get_full_balance(uint32_t account_index) + { + std::map accountBalance; + std::map> balanceSubaddresses = get_current_wallet()->balance(account_index); + std::vector assetList = Monero::Assets::list(); + //prefill balances + for (const auto &asset_type : assetList) { + + accountBalance[asset_type] = 0; + } + // balances are mapped to their subaddress + // we compute total balances of account + for (auto const& balanceSubaddress : balanceSubaddresses) + { + + std::map balanceOfSubaddress = balanceSubaddress.second; + + for (auto const& balance : balanceOfSubaddress) + { + + const std::string &assetType = balance.first; + const uint64_t &amount = balance.second; + accountBalance[assetType] +=amount; + } + } + + size_t size = accountBalance.size(); + int64_t *balanceAddresses = (int64_t *)malloc(size * sizeof(int64_t)); + int i = 0; + + for (auto const& balance : accountBalance) + { + char *assetType = strdup(balance.first.c_str()); + HavenBalance *hb = new HavenBalance(assetType, balance.second); + balanceAddresses[i] = reinterpret_cast(hb); + i++; + } + return balanceAddresses; + } + + int64_t *get_unlocked_balance(uint32_t account_index) + { + std::map accountBalance; + std::map> balanceSubaddresses = get_current_wallet()->unlockedBalance(account_index); + std::vector assetList = Monero::Assets::list(); + + //prefill balances + for (const auto &asset_type : assetList) { + + accountBalance[asset_type] = 0; + } + // balances are mapped to their subaddress + // we compute total balances of account + for (auto const& balanceSubaddress : balanceSubaddresses) + { + + std::map balanceOfSubaddress = balanceSubaddress.second; + + for (auto const& balance : balanceOfSubaddress) + { + + const std::string &assetType = balance.first; + const uint64_t &amount = balance.second; + accountBalance[assetType] +=amount; + } + } + + size_t size = accountBalance.size(); + int64_t *balanceAddresses = (int64_t *)malloc(size * sizeof(int64_t)); + int i = 0; + + for (auto const& balance : accountBalance) + { + char *assetType = strdup(balance.first.c_str()); + HavenBalance *hb = new HavenBalance(assetType, balance.second); + balanceAddresses[i] = reinterpret_cast(hb); + i++; + } + return balanceAddresses; + } + + uint64_t get_current_height() + { + return get_current_wallet()->blockChainHeight(); + } + + uint64_t get_node_height() + { + return get_current_wallet()->daemonBlockChainHeight(); + } + + bool connect_to_node(char *error) + { + nice(19); + bool is_connected = get_current_wallet()->connectToDaemon(); + + if (!is_connected) + { + error = strdup(get_current_wallet()->errorString().c_str()); + } + + return is_connected; + } + + bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + { + nice(19); + Monero::Wallet *wallet = get_current_wallet(); + + std::string _login = ""; + std::string _password = ""; + + if (login != nullptr) + { + _login = std::string(login); + } + + if (password != nullptr) + { + _password = std::string(password); + } + + bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + + if (!inited) + { + error = strdup(wallet->errorString().c_str()); + } else if (!wallet->connectToDaemon()) { + error = strdup(wallet->errorString().c_str()); + } + + return inited; + } + + bool is_connected() + { + return get_current_wallet()->connected(); + } + + void start_refresh() + { + get_current_wallet()->refreshAsync(); + get_current_wallet()->startRefresh(); + } + + void set_refresh_from_block_height(uint64_t height) + { + get_current_wallet()->setRefreshFromBlockHeight(height); + } + + void set_recovering_from_seed(bool is_recovery) + { + get_current_wallet()->setRecoveringFromSeed(is_recovery); + } + + void store(char *path) + { + store_lock.lock(); + if (is_storing) { + return; + } + + is_storing = true; + get_current_wallet()->store(std::string(path)); + is_storing = false; + store_lock.unlock(); + } + + bool transaction_create(char *address, char *asset_type, char *payment_id, char *amount, + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + { + nice(19); + + auto priority = static_cast(priority_raw); + std::string _payment_id; + Monero::PendingTransaction *transaction; + + if (payment_id != nullptr) + { + _payment_id = std::string(payment_id); + } + + if (amount != nullptr) + { + uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account, {}); + } + else + { + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(),std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account, {}); + } + + int status = transaction->status(); + + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) + { + error = Utf8Box(strdup(transaction->errorString().c_str())); + return false; + } + + if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + pendingTransaction = PendingTransactionRaw(transaction); + return true; + } + + bool transaction_create_mult_dest(char **addresses, char *asset_type, char *payment_id, char **amounts, uint32_t size, + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + { + nice(19); + + std::vector _addresses; + std::vector _amounts; + + for (int i = 0; i < size; i++) { + _addresses.push_back(std::string(*addresses)); + _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); + addresses++; + amounts++; + } + + auto priority = static_cast(priority_raw); + std::string _payment_id; + Monero::PendingTransaction *transaction; + + if (payment_id != nullptr) + { + _payment_id = std::string(payment_id); + } + + transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, + std::string(asset_type), std::string(asset_type), m_wallet->defaultMixin(), priority, subaddr_account,{}); + + int status = transaction->status(); + + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) + { + error = Utf8Box(strdup(transaction->errorString().c_str())); + return false; + } + + if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + pendingTransaction = PendingTransactionRaw(transaction); + return true; + } + + bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) + { + bool committed = transaction->transaction->commit(); + + if (!committed) + { + error = Utf8Box(strdup(transaction->transaction->errorString().c_str())); + } else if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + return committed; + } + + uint64_t get_node_height_or_update(uint64_t base_eight) + { + if (m_cached_syncing_blockchain_height < base_eight) { + m_cached_syncing_blockchain_height = base_eight; + } + + return m_cached_syncing_blockchain_height; + } + + uint64_t get_syncing_height() + { + if (m_listener == nullptr) { + return 0; + } + + uint64_t height = m_listener->height(); + + if (height <= 1) { + return 0; + } + + if (height != m_last_known_wallet_height) + { + m_last_known_wallet_height = height; + } + + return height; + } + + uint64_t is_needed_to_refresh() + { + if (m_listener == nullptr) { + return false; + } + + bool should_refresh = m_listener->isNeedToRefresh(); + + if (should_refresh) { + m_listener->resetNeedToRefresh(); + } + + return should_refresh; + } + + uint8_t is_new_transaction_exist() + { + if (m_listener == nullptr) { + return false; + } + + bool is_new_transaction_exist = m_listener->isNewTransactionExist(); + + if (is_new_transaction_exist) + { + m_listener->resetIsNewTransactionExist(); + } + + return is_new_transaction_exist; + } + + void set_listener() + { + m_last_known_wallet_height = 0; + + if (m_listener != nullptr) + { + free(m_listener); + } + + m_listener = new MoneroWalletListener(); + get_current_wallet()->setListener(m_listener); + } + + int64_t *subaddrress_get_all() + { + std::vector _subaddresses = m_subaddress->getAll(); + size_t size = _subaddresses.size(); + int64_t *subaddresses = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::SubaddressRow *row = _subaddresses[i]; + SubaddressRow *_row = new SubaddressRow(row->getRowId(), strdup(row->getAddress().c_str()), strdup(row->getLabel().c_str())); + subaddresses[i] = reinterpret_cast(_row); + } + + return subaddresses; + } + + int32_t subaddrress_size() + { + std::vector _subaddresses = m_subaddress->getAll(); + return _subaddresses.size(); + } + + void subaddress_add_row(uint32_t accountIndex, char *label) + { + m_subaddress->addRow(accountIndex, std::string(label)); + } + + void subaddress_set_label(uint32_t accountIndex, uint32_t addressIndex, char *label) + { + m_subaddress->setLabel(accountIndex, addressIndex, std::string(label)); + } + + void subaddress_refresh(uint32_t accountIndex) + { + m_subaddress->refresh(accountIndex); + } + + int32_t account_size() + { + std::vector _accocunts = m_account->getAll(); + return _accocunts.size(); + } + + int64_t *account_get_all() + { + std::vector _accocunts = m_account->getAll(); + size_t size = _accocunts.size(); + int64_t *accocunts = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::SubaddressAccountRow *row = _accocunts[i]; + AccountRow *_row = new AccountRow(row->getRowId(), strdup(row->getLabel().c_str())); + accocunts[i] = reinterpret_cast(_row); + } + + return accocunts; + } + + void account_add_row(char *label) + { + m_account->addRow(std::string(label)); + } + + void account_set_label_row(uint32_t account_index, char *label) + { + m_account->setLabel(account_index, label); + } + + void account_refresh() + { + m_account->refresh(); + } + + int64_t *transactions_get_all() + { + std::vector transactions = m_transaction_history->getAll(); + size_t size = transactions.size(); + int64_t *transactionAddresses = (int64_t *)malloc(size * sizeof(int64_t)); + + for (int i = 0; i < size; i++) + { + Monero::TransactionInfo *row = transactions[i]; + TransactionInfoRow *tx = new TransactionInfoRow(row); + transactionAddresses[i] = reinterpret_cast(tx); + } + + return transactionAddresses; + } + + void transactions_refresh() + { + m_transaction_history->refresh(); + } + + int64_t transactions_count() + { + return m_transaction_history->count(); + } + + int LedgerExchange( + unsigned char *command, + unsigned int cmd_len, + unsigned char *response, + unsigned int max_resp_len) + { + return -1; + } + + int LedgerFind(char *buffer, size_t len) + { + return -1; + } + + void on_startup() + { + Monero::Utils::onStartup(); + Monero::WalletManagerFactory::setLogLevel(4); + } + + void rescan_blockchain() + { + m_wallet->rescanBlockchainAsync(); + } + + char * get_tx_key(char * txId) + { + return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); + } + + int32_t asset_types_size() + { + return Monero::Assets::list().size(); + } + + char **asset_types() + { + size_t size = Monero::Assets::list().size(); + std::vector assetList = Monero::Assets::list(); + char **assetTypesPts; + assetTypesPts = (char **) malloc( size * sizeof(char*)); + + for (int i = 0; i < size; i++) + { + + std::string asset = assetList[i]; + //assetTypes[i] = (char *)malloc( 5 * sizeof(char)); + assetTypesPts[i] = strdup(asset.c_str()); + } + + return assetTypesPts; + } + + std::map rates; + + void update_rate() + { + rates = get_current_wallet()->oracleRates(); + } + + int64_t *get_rate() + { + size_t size = rates.size(); + int64_t *havenRates = (int64_t *)malloc(size * sizeof(int64_t)); + int i = 0; + + for (auto const& rate : rates) + { + char *assetType = strdup(rate.first.c_str()); + HavenRate *havenRate = new HavenRate(assetType, rate.second); + havenRates[i] = reinterpret_cast(havenRate); + i++; + } + + return havenRates; + } + + int32_t size_of_rate() + { + return static_cast(rates.size()); + } + +#ifdef __cplusplus +} +#endif diff --git a/cw_haven/ios/cw_haven.podspec b/cw_haven/ios/cw_haven.podspec new file mode 100644 index 000000000..4a9267d15 --- /dev/null +++ b/cw_haven/ios/cw_haven.podspec @@ -0,0 +1,50 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_haven.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_haven' + s.version = '0.0.1' + s.summary = 'Cake Wallet Haven' + s.description = 'Cake Wallet wrapper over Haven project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h, Classes/*.h, ../shared_external/ios/libs/monero/include/src/**/*.h, ../shared_external/ios/libs/monero/include/contrib/**/*.h, ../shared_external/ios/libs/monero/include/../shared_external/ios/**/*.h' + s.dependency 'Flutter' + s.dependency 'cw_shared_external' + s.platform = :ios, '10.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } + s.swift_version = '5.0' + s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } + + s.subspec 'OpenSSL' do |openssl| + openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' + openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' + openssl.libraries = 'ssl', 'crypto' + openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + + s.subspec 'Sodium' do |sodium| + sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' + sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libsodium.a' + sodium.libraries = 'sodium' + sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + + s.subspec 'Boost' do |boost| + boost.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h', + boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libboost.a', + boost.libraries = 'boost' + boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + + s.subspec 'Haven' do |haven| + haven.preserve_paths = 'External/ios/include/**/*.h' + haven.vendored_libraries = 'External/ios/lib/libhaven.a' + haven.libraries = 'haven' + haven.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include" } + end +end diff --git a/cw_haven/lib/api/account_list.dart b/cw_haven/lib/api/account_list.dart new file mode 100644 index 000000000..96bf3d654 --- /dev/null +++ b/cw_haven/lib/api/account_list.dart @@ -0,0 +1,83 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/structs/account_row.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_haven/api/wallet.dart'; + +final accountSizeNative = havenApi + .lookup>('account_size') + .asFunction(); + +final accountRefreshNative = havenApi + .lookup>('account_refresh') + .asFunction(); + +final accountGetAllNative = havenApi + .lookup>('account_get_all') + .asFunction(); + +final accountAddNewNative = havenApi + .lookup>('account_add_row') + .asFunction(); + +final accountSetLabelNative = havenApi + .lookup>('account_set_label_row') + .asFunction(); + +bool isUpdating = false; + +void refreshAccounts() { + try { + isUpdating = true; + accountRefreshNative(); + isUpdating = false; + } catch (e) { + isUpdating = false; + rethrow; + } +} + +List getAllAccount() { + final size = accountSizeNative(); + final accountAddressesPointer = accountGetAllNative(); + final accountAddresses = accountAddressesPointer.asTypedList(size); + + return accountAddresses + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +void addAccountSync({String label}) { + final labelPointer = Utf8.toUtf8(label); + accountAddNewNative(labelPointer); + free(labelPointer); +} + +void setLabelForAccountSync({int accountIndex, String label}) { + final labelPointer = Utf8.toUtf8(label); + accountSetLabelNative(accountIndex, labelPointer); + free(labelPointer); +} + +void _addAccount(String label) => addAccountSync(label: label); + +void _setLabelForAccount(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + + setLabelForAccountSync(label: label, accountIndex: accountIndex); +} + +Future addAccount({String label}) async { + await compute(_addAccount, label); + await store(); +} + +Future setLabelForAccount({int accountIndex, String label}) async { + await compute( + _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); + await store(); +} \ No newline at end of file diff --git a/cw_haven/lib/api/asset_types.dart b/cw_haven/lib/api/asset_types.dart new file mode 100644 index 000000000..f57b10f78 --- /dev/null +++ b/cw_haven/lib/api/asset_types.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; +import 'package:cw_haven/api/convert_utf8_to_string.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:ffi/ffi.dart'; + +final assetTypesSizeNative = havenApi + .lookup>('asset_types_size') + .asFunction(); + +final getAssetTypesNative = havenApi + .lookup>('asset_types') + .asFunction(); + +List getAssetTypes() { + List assetTypes = []; + Pointer> assetTypePointers = getAssetTypesNative(); + Pointer assetpointer = assetTypePointers.elementAt(0)[0]; + String asset = convertUTF8ToString(pointer: assetpointer); + + return assetTypes; +} diff --git a/cw_haven/lib/api/balance_list.dart b/cw_haven/lib/api/balance_list.dart new file mode 100644 index 000000000..3488a6583 --- /dev/null +++ b/cw_haven/lib/api/balance_list.dart @@ -0,0 +1,58 @@ +import 'dart:ffi'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/structs/haven_balance_row.dart'; +import 'package:cw_haven/api/structs/haven_rate.dart'; +import 'asset_types.dart'; + +List getHavenFullBalance({int accountIndex = 0}) { + final size = assetTypesSizeNative(); + final balanceAddressesPointer = getHavenFullBalanceNative(accountIndex); + final balanceAddresses = balanceAddressesPointer.asTypedList(size); + + return balanceAddresses + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +List getHavenUnlockedBalance({int accountIndex = 0}) { + final size = assetTypesSizeNative(); + final balanceAddressesPointer = getHavenUnlockedBalanceNative(accountIndex); + final balanceAddresses = balanceAddressesPointer.asTypedList(size); + + return balanceAddresses + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +List getRate() { + updateRateNative(); + final size = sizeOfRateNative(); + final ratePointer = getRateNative(); + final rate = ratePointer.asTypedList(size); + + return rate + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +final getHavenFullBalanceNative = havenApi + .lookup>('get_full_balance') + .asFunction(); + +final getHavenUnlockedBalanceNative = havenApi + .lookup>('get_unlocked_balance') + .asFunction(); + +final getRateNative = havenApi + .lookup>('get_rate') + .asFunction(); + +final sizeOfRateNative = havenApi + .lookup>('size_of_rate') + .asFunction(); + +final updateRateNative = havenApi + .lookup>('update_rate') + .asFunction(); \ No newline at end of file diff --git a/cw_haven/lib/api/convert_utf8_to_string.dart b/cw_haven/lib/api/convert_utf8_to_string.dart new file mode 100644 index 000000000..7fa5a68df --- /dev/null +++ b/cw_haven/lib/api/convert_utf8_to_string.dart @@ -0,0 +1,8 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +String convertUTF8ToString({Pointer pointer}) { + final str = Utf8.fromUtf8(pointer); + free(pointer); + return str; +} \ No newline at end of file diff --git a/cw_haven/lib/api/cw_haven.dart b/cw_haven/lib/api/cw_haven.dart new file mode 100644 index 000000000..1d3726e17 --- /dev/null +++ b/cw_haven/lib/api/cw_haven.dart @@ -0,0 +1,14 @@ + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +class CwHaven { + static const MethodChannel _channel = + const MethodChannel('cw_haven'); + + static Future get platformVersion async { + final String version = await _channel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_haven/lib/api/exceptions/connection_to_node_exception.dart b/cw_haven/lib/api/exceptions/connection_to_node_exception.dart new file mode 100644 index 000000000..6ee272b89 --- /dev/null +++ b/cw_haven/lib/api/exceptions/connection_to_node_exception.dart @@ -0,0 +1,5 @@ +class ConnectionToNodeException implements Exception { + ConnectionToNodeException({this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/creation_transaction_exception.dart b/cw_haven/lib/api/exceptions/creation_transaction_exception.dart new file mode 100644 index 000000000..bb477d673 --- /dev/null +++ b/cw_haven/lib/api/exceptions/creation_transaction_exception.dart @@ -0,0 +1,8 @@ +class CreationTransactionException implements Exception { + CreationTransactionException({this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/setup_wallet_exception.dart b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart new file mode 100644 index 000000000..ce43c0ec6 --- /dev/null +++ b/cw_haven/lib/api/exceptions/setup_wallet_exception.dart @@ -0,0 +1,5 @@ +class SetupWalletException implements Exception { + SetupWalletException({this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_creation_exception.dart b/cw_haven/lib/api/exceptions/wallet_creation_exception.dart new file mode 100644 index 000000000..6b00445ad --- /dev/null +++ b/cw_haven/lib/api/exceptions/wallet_creation_exception.dart @@ -0,0 +1,8 @@ +class WalletCreationException implements Exception { + WalletCreationException({this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_opening_exception.dart b/cw_haven/lib/api/exceptions/wallet_opening_exception.dart new file mode 100644 index 000000000..8d84b0f7e --- /dev/null +++ b/cw_haven/lib/api/exceptions/wallet_opening_exception.dart @@ -0,0 +1,8 @@ +class WalletOpeningException implements Exception { + WalletOpeningException({this.message}); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart new file mode 100644 index 000000000..5f08437d4 --- /dev/null +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -0,0 +1,5 @@ +class WalletRestoreFromKeysException implements Exception { + WalletRestoreFromKeysException({this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart new file mode 100644 index 000000000..fd89e4161 --- /dev/null +++ b/cw_haven/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -0,0 +1,5 @@ +class WalletRestoreFromSeedException implements Exception { + WalletRestoreFromSeedException({this.message}); + + final String message; +} \ No newline at end of file diff --git a/cw_haven/lib/api/haven_api.dart b/cw_haven/lib/api/haven_api.dart new file mode 100644 index 000000000..41b50d9bc --- /dev/null +++ b/cw_haven/lib/api/haven_api.dart @@ -0,0 +1,6 @@ +import 'dart:ffi'; +import 'dart:io'; + +final DynamicLibrary havenApi = Platform.isAndroid + ? DynamicLibrary.open("libcw_haven.so") + : DynamicLibrary.open("cw_haven.framework/cw_haven"); \ No newline at end of file diff --git a/cw_haven/lib/api/monero_output.dart b/cw_haven/lib/api/monero_output.dart new file mode 100644 index 000000000..831ee1f22 --- /dev/null +++ b/cw_haven/lib/api/monero_output.dart @@ -0,0 +1,8 @@ +import 'package:flutter/foundation.dart'; + +class MoneroOutput { + MoneroOutput({@required this.address, @required this.amount}); + + final String address; + final String amount; +} \ No newline at end of file diff --git a/cw_haven/lib/api/signatures.dart b/cw_haven/lib/api/signatures.dart new file mode 100644 index 000000000..c9db9ac8d --- /dev/null +++ b/cw_haven/lib/api/signatures.dart @@ -0,0 +1,138 @@ +import 'dart:ffi'; +import 'package:cw_haven/api/structs/pending_transaction.dart'; +import 'package:cw_haven/api/structs/ut8_box.dart'; +import 'package:ffi/ffi.dart'; + +typedef create_wallet = Int8 Function( + Pointer, Pointer, Pointer, Int32, Pointer); + +typedef restore_wallet_from_seed = Int8 Function( + Pointer, Pointer, Pointer, Int32, Int64, Pointer); + +typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, + Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); + +typedef is_wallet_exist = Int8 Function(Pointer); + +typedef load_wallet = Int8 Function(Pointer, Pointer, Int8); + +typedef error_string = Pointer Function(); + +typedef get_filename = Pointer Function(); + +typedef get_seed = Pointer Function(); + +typedef get_address = Pointer Function(Int32, Int32); + +typedef get_full_balance = Pointer Function(Int32); + +typedef get_unlocked_balance = Pointer Function(Int32); + +typedef get_full_balanace = Int64 Function(Int32); + +typedef get_unlocked_balanace = Int64 Function(Int32); + +typedef get_current_height = Int64 Function(); + +typedef get_node_height = Int64 Function(); + +typedef is_connected = Int8 Function(); + +typedef setup_node = Int8 Function( + Pointer, Pointer, Pointer, Int8, Int8, Pointer); + +typedef start_refresh = Void Function(); + +typedef connect_to_node = Int8 Function(); + +typedef set_refresh_from_block_height = Void Function(Int64); + +typedef set_recovering_from_seed = Void Function(Int8); + +typedef store_c = Void Function(Pointer); + +typedef set_listener = Void Function(); + +typedef get_syncing_height = Int64 Function(); + +typedef is_needed_to_refresh = Int8 Function(); + +typedef is_new_transaction_exist = Int8 Function(); + +typedef subaddrress_size = Int32 Function(); + +typedef subaddrress_refresh = Void Function(Int32); + +typedef subaddress_get_all = Pointer Function(); + +typedef subaddress_add_new = Void Function( + Int32 accountIndex, Pointer label); + +typedef subaddress_set_label = Void Function( + Int32 accountIndex, Int32 addressIndex, Pointer label); + +typedef account_size = Int32 Function(); + +typedef account_refresh = Void Function(); + +typedef account_get_all = Pointer Function(); + +typedef account_add_new = Void Function(Pointer label); + +typedef account_set_label = Void Function( + Int32 accountIndex, Pointer label); + +typedef transactions_refresh = Void Function(); + +typedef get_tx_key = Pointer Function(Pointer txId); + +typedef transactions_count = Int64 Function(); + +typedef transactions_get_all = Pointer Function(); + +typedef transaction_create = Int8 Function( + Pointer address, + Pointer assetType, + Pointer paymentId, + Pointer amount, + Int8 priorityRaw, + Int32 subaddrAccount, + Pointer error, + Pointer pendingTransaction); + +typedef transaction_create_mult_dest = Int8 Function( + Pointer> addresses, + Pointer assetType, + Pointer paymentId, + Pointer> amounts, + Int32 size, + Int8 priorityRaw, + Int32 subaddrAccount, + Pointer error, + Pointer pendingTransaction); + +typedef transaction_commit = Int8 Function(Pointer, Pointer); + +typedef secret_view_key = Pointer Function(); + +typedef public_view_key = Pointer Function(); + +typedef secret_spend_key = Pointer Function(); + +typedef public_spend_key = Pointer Function(); + +typedef close_current_wallet = Void Function(); + +typedef on_startup = Void Function(); + +typedef rescan_blockchain = Void Function(); + +typedef asset_types = Pointer> Function(); + +typedef asset_types_size = Int32 Function(); + +typedef get_rate = Pointer Function(); + +typedef size_of_rate = Int32 Function(); + +typedef update_rate = Void Function(); \ No newline at end of file diff --git a/cw_haven/lib/api/structs/account_row.dart b/cw_haven/lib/api/structs/account_row.dart new file mode 100644 index 000000000..c3fc22de1 --- /dev/null +++ b/cw_haven/lib/api/structs/account_row.dart @@ -0,0 +1,11 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class AccountRow extends Struct { + @Int64() + int id; + Pointer label; + + String getLabel() => Utf8.fromUtf8(label); + int getId() => id; +} diff --git a/cw_haven/lib/api/structs/haven_balance_row.dart b/cw_haven/lib/api/structs/haven_balance_row.dart new file mode 100644 index 000000000..4b68a7bb6 --- /dev/null +++ b/cw_haven/lib/api/structs/haven_balance_row.dart @@ -0,0 +1,11 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class HavenBalanceRow extends Struct { + @Int64() + int amount; + Pointer assetType; + + int getAmount() => amount; + String getAssetType() => Utf8.fromUtf8(assetType); +} diff --git a/cw_haven/lib/api/structs/haven_rate.dart b/cw_haven/lib/api/structs/haven_rate.dart new file mode 100644 index 000000000..818615559 --- /dev/null +++ b/cw_haven/lib/api/structs/haven_rate.dart @@ -0,0 +1,11 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class HavenRate extends Struct { + @Int64() + int rate; + Pointer assetType; + + int getRate() => rate; + String getAssetType() => Utf8.fromUtf8(assetType); +} diff --git a/cw_haven/lib/api/structs/pending_transaction.dart b/cw_haven/lib/api/structs/pending_transaction.dart new file mode 100644 index 000000000..b492f28a0 --- /dev/null +++ b/cw_haven/lib/api/structs/pending_transaction.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class PendingTransactionRaw extends Struct { + @Int64() + int amount; + + @Int64() + int fee; + + Pointer hash; + + String getHash() => Utf8.fromUtf8(hash); +} + +class PendingTransactionDescription { + PendingTransactionDescription({this.amount, this.fee, this.hash, this.pointerAddress}); + + final int amount; + final int fee; + final String hash; + final int pointerAddress; +} \ No newline at end of file diff --git a/cw_haven/lib/api/structs/subaddress_row.dart b/cw_haven/lib/api/structs/subaddress_row.dart new file mode 100644 index 000000000..1673e00c7 --- /dev/null +++ b/cw_haven/lib/api/structs/subaddress_row.dart @@ -0,0 +1,13 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class SubaddressRow extends Struct { + @Int64() + int id; + Pointer address; + Pointer label; + + String getLabel() => Utf8.fromUtf8(label); + String getAddress() => Utf8.fromUtf8(address); + int getId() => id; +} \ No newline at end of file diff --git a/cw_haven/lib/api/structs/transaction_info_row.dart b/cw_haven/lib/api/structs/transaction_info_row.dart new file mode 100644 index 000000000..68a84e0a2 --- /dev/null +++ b/cw_haven/lib/api/structs/transaction_info_row.dart @@ -0,0 +1,44 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class TransactionInfoRow extends Struct { + @Uint64() + int amount; + + @Uint64() + int fee; + + @Uint64() + int blockHeight; + + @Uint64() + int confirmations; + + @Uint32() + int subaddrAccount; + + @Int8() + int direction; + + @Int8() + int isPending; + + @Uint32() + int subaddrIndex; + + Pointer hash; + + Pointer paymentId; + + Pointer assetType; + + @Int64() + int datetime; + + int getDatetime() => datetime; + int getAmount() => amount >= 0 ? amount : amount * -1; + bool getIsPending() => isPending != 0; + String getHash() => Utf8.fromUtf8(hash); + String getPaymentId() => Utf8.fromUtf8(paymentId); + String getAssetType() => Utf8.fromUtf8(assetType); +} diff --git a/cw_haven/lib/api/structs/ut8_box.dart b/cw_haven/lib/api/structs/ut8_box.dart new file mode 100644 index 000000000..a6f41bc75 --- /dev/null +++ b/cw_haven/lib/api/structs/ut8_box.dart @@ -0,0 +1,8 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class Utf8Box extends Struct { + Pointer value; + + String getValue() => Utf8.fromUtf8(value); +} diff --git a/cw_haven/lib/api/subaddress_list.dart b/cw_haven/lib/api/subaddress_list.dart new file mode 100644 index 000000000..c735400ec --- /dev/null +++ b/cw_haven/lib/api/subaddress_list.dart @@ -0,0 +1,97 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/structs/subaddress_row.dart'; +import 'package:cw_haven/api/wallet.dart'; + +final subaddressSizeNative = havenApi + .lookup>('subaddrress_size') + .asFunction(); + +final subaddressRefreshNative = havenApi + .lookup>('subaddress_refresh') + .asFunction(); + +final subaddrressGetAllNative = havenApi + .lookup>('subaddrress_get_all') + .asFunction(); + +final subaddrressAddNewNative = havenApi + .lookup>('subaddress_add_row') + .asFunction(); + +final subaddrressSetLabelNative = havenApi + .lookup>('subaddress_set_label') + .asFunction(); + +bool isUpdating = false; + +void refreshSubaddresses({@required int accountIndex}) { + try { + isUpdating = true; + subaddressRefreshNative(accountIndex); + isUpdating = false; + } catch (e) { + isUpdating = false; + rethrow; + } +} + +List getAllSubaddresses() { + final size = subaddressSizeNative(); + final subaddressAddressesPointer = subaddrressGetAllNative(); + final subaddressAddresses = subaddressAddressesPointer.asTypedList(size); + + return subaddressAddresses + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +void addSubaddressSync({int accountIndex, String label}) { + final labelPointer = Utf8.toUtf8(label); + subaddrressAddNewNative(accountIndex, labelPointer); + free(labelPointer); +} + +void setLabelForSubaddressSync( + {int accountIndex, int addressIndex, String label}) { + final labelPointer = Utf8.toUtf8(label); + + subaddrressSetLabelNative(accountIndex, addressIndex, labelPointer); + free(labelPointer); +} + +void _addSubaddress(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + + addSubaddressSync(accountIndex: accountIndex, label: label); +} + +void _setLabelForSubaddress(Map args) { + final label = args['label'] as String; + final accountIndex = args['accountIndex'] as int; + final addressIndex = args['addressIndex'] as int; + + setLabelForSubaddressSync( + accountIndex: accountIndex, addressIndex: addressIndex, label: label); +} + +Future addSubaddress({int accountIndex, String label}) async { + await compute, void>( + _addSubaddress, {'accountIndex': accountIndex, 'label': label}); + await store(); +} + +Future setLabelForSubaddress( + {int accountIndex, int addressIndex, String label}) async { + await compute, void>(_setLabelForSubaddress, { + 'accountIndex': accountIndex, + 'addressIndex': addressIndex, + 'label': label + }); + await store(); +} diff --git a/cw_haven/lib/api/transaction_history.dart b/cw_haven/lib/api/transaction_history.dart new file mode 100644 index 000000000..185207941 --- /dev/null +++ b/cw_haven/lib/api/transaction_history.dart @@ -0,0 +1,246 @@ +import 'dart:ffi'; +import 'package:cw_haven/api/convert_utf8_to_string.dart'; +import 'package:cw_haven/api/monero_output.dart'; +import 'package:cw_haven/api/structs/ut8_box.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/structs/transaction_info_row.dart'; +import 'package:cw_haven/api/structs/pending_transaction.dart'; +import 'package:cw_haven/api/exceptions/creation_transaction_exception.dart'; + +final transactionsRefreshNative = havenApi + .lookup>('transactions_refresh') + .asFunction(); + +final transactionsCountNative = havenApi + .lookup>('transactions_count') + .asFunction(); + +final transactionsGetAllNative = havenApi + .lookup>('transactions_get_all') + .asFunction(); + +final transactionCreateNative = havenApi + .lookup>('transaction_create') + .asFunction(); + +final transactionCreateMultDestNative = havenApi + .lookup>('transaction_create_mult_dest') + .asFunction(); + +final transactionCommitNative = havenApi + .lookup>('transaction_commit') + .asFunction(); + +final getTxKeyNative = havenApi + .lookup>('get_tx_key') + .asFunction(); + +String getTxKey(String txId) { + final txIdPointer = Utf8.toUtf8(txId); + final keyPointer = getTxKeyNative(txIdPointer); + + free(txIdPointer); + + if (keyPointer != null) { + return convertUTF8ToString(pointer: keyPointer); + } + + return null; +} + +void refreshTransactions() => transactionsRefreshNative(); + +int countOfTransactions() => transactionsCountNative(); + +List getAllTransations() { + final size = transactionsCountNative(); + final transactionsPointer = transactionsGetAllNative(); + final transactionsAddresses = transactionsPointer.asTypedList(size); + + return transactionsAddresses + .map((addr) => Pointer.fromAddress(addr).ref) + .toList(); +} + +PendingTransactionDescription createTransactionSync( + {String address, + String assetType, + String paymentId, + String amount, + int priorityRaw, + int accountIndex = 0}) { + final addressPointer = Utf8.toUtf8(address); + final assetTypePointer = Utf8.toUtf8(assetType); + final paymentIdPointer = Utf8.toUtf8(paymentId); + final amountPointer = amount != null ? Utf8.toUtf8(amount) : nullptr; + final errorMessagePointer = allocate(); + final pendingTransactionRawPointer = allocate(); + final created = transactionCreateNative( + addressPointer, + assetTypePointer, + paymentIdPointer, + amountPointer, + priorityRaw, + accountIndex, + errorMessagePointer, + pendingTransactionRawPointer) != + 0; + + free(addressPointer); + free(assetTypePointer); + free(paymentIdPointer); + + if (amountPointer != nullptr) { + free(amountPointer); + } + + if (!created) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw CreationTransactionException(message: message); + } + + return PendingTransactionDescription( + amount: pendingTransactionRawPointer.ref.amount, + fee: pendingTransactionRawPointer.ref.fee, + hash: pendingTransactionRawPointer.ref.getHash(), + pointerAddress: pendingTransactionRawPointer.address); +} + +PendingTransactionDescription createTransactionMultDestSync( + {List outputs, + String assetType, + String paymentId, + int priorityRaw, + int accountIndex = 0}) { + final int size = outputs.length; + final List> addressesPointers = outputs.map((output) => + Utf8.toUtf8(output.address)).toList(); + final Pointer> addressesPointerPointer = allocate(count: size); + final List> amountsPointers = outputs.map((output) => + Utf8.toUtf8(output.amount)).toList(); + final Pointer> amountsPointerPointer = allocate(count: size); + + for (int i = 0; i < size; i++) { + addressesPointerPointer[i] = addressesPointers[i]; + amountsPointerPointer[i] = amountsPointers[i]; + } + + final assetTypePointer = Utf8.toUtf8(assetType); + final paymentIdPointer = Utf8.toUtf8(paymentId); + final errorMessagePointer = allocate(); + final pendingTransactionRawPointer = allocate(); + final created = transactionCreateMultDestNative( + addressesPointerPointer, + assetTypePointer, + paymentIdPointer, + amountsPointerPointer, + size, + priorityRaw, + accountIndex, + errorMessagePointer, + pendingTransactionRawPointer) != + 0; + + free(addressesPointerPointer); + free(assetTypePointer); + free(amountsPointerPointer); + + addressesPointers.forEach((element) => free(element)); + amountsPointers.forEach((element) => free(element)); + + free(paymentIdPointer); + + if (!created) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw CreationTransactionException(message: message); + } + + return PendingTransactionDescription( + amount: pendingTransactionRawPointer.ref.amount, + fee: pendingTransactionRawPointer.ref.fee, + hash: pendingTransactionRawPointer.ref.getHash(), + pointerAddress: pendingTransactionRawPointer.address); +} + +void commitTransactionFromPointerAddress({int address}) => commitTransaction( + transactionPointer: Pointer.fromAddress(address)); + +void commitTransaction({Pointer transactionPointer}) { + final errorMessagePointer = allocate(); + final isCommited = + transactionCommitNative(transactionPointer, errorMessagePointer) != 0; + + if (!isCommited) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw CreationTransactionException(message: message); + } +} + +PendingTransactionDescription _createTransactionSync(Map args) { + final address = args['address'] as String; + final assetType = args['assetType'] as String; + final paymentId = args['paymentId'] as String; + final amount = args['amount'] as String; + final priorityRaw = args['priorityRaw'] as int; + final accountIndex = args['accountIndex'] as int; + + return createTransactionSync( + address: address, + assetType: assetType, + paymentId: paymentId, + amount: amount, + priorityRaw: priorityRaw, + accountIndex: accountIndex); +} + +PendingTransactionDescription _createTransactionMultDestSync(Map args) { + final outputs = args['outputs'] as List; + final assetType = args['assetType'] as String; + final paymentId = args['paymentId'] as String; + final priorityRaw = args['priorityRaw'] as int; + final accountIndex = args['accountIndex'] as int; + + return createTransactionMultDestSync( + outputs: outputs, + assetType: assetType, + paymentId: paymentId, + priorityRaw: priorityRaw, + accountIndex: accountIndex); +} + +Future createTransaction( + {String address, + String assetType, + String paymentId = '', + String amount, + int priorityRaw, + int accountIndex = 0}) => + compute(_createTransactionSync, { + 'address': address, + 'assetType': assetType, + 'paymentId': paymentId, + 'amount': amount, + 'priorityRaw': priorityRaw, + 'accountIndex': accountIndex + }); + +Future createTransactionMultDest( + {List outputs, + String assetType, + String paymentId = '', + int priorityRaw, + int accountIndex = 0}) => + compute(_createTransactionMultDestSync, { + 'outputs': outputs, + 'assetType': assetType, + 'paymentId': paymentId, + 'priorityRaw': priorityRaw, + 'accountIndex': accountIndex + }); diff --git a/cw_haven/lib/api/types.dart b/cw_haven/lib/api/types.dart new file mode 100644 index 000000000..09b6f77e0 --- /dev/null +++ b/cw_haven/lib/api/types.dart @@ -0,0 +1,136 @@ +import 'dart:ffi'; +import 'package:cw_haven/api/structs/pending_transaction.dart'; +import 'package:cw_haven/api/structs/ut8_box.dart'; +import 'package:ffi/ffi.dart'; + +typedef CreateWallet = int Function( + Pointer, Pointer, Pointer, int, Pointer); + +typedef RestoreWalletFromSeed = int Function( + Pointer, Pointer, Pointer, int, int, Pointer); + +typedef RestoreWalletFromKeys = int Function(Pointer, Pointer, + Pointer, Pointer, Pointer, Pointer, int, int, Pointer); + +typedef IsWalletExist = int Function(Pointer); + +typedef LoadWallet = int Function(Pointer, Pointer, int); + +typedef ErrorString = Pointer Function(); + +typedef GetFilename = Pointer Function(); + +typedef GetSeed = Pointer Function(); + +typedef GetAddress = Pointer Function(int, int); + +typedef GetHavenFullBalance = Pointer Function(int); + +typedef GetHavenUnlockedBalance = Pointer Function(int); + +typedef GetFullBalance = int Function(int); + +typedef GetUnlockedBalance = int Function(int); + +typedef GetCurrentHeight = int Function(); + +typedef GetNodeHeight = int Function(); + +typedef IsConnected = int Function(); + +typedef SetupNode = int Function( + Pointer, Pointer, Pointer, int, int, Pointer); + +typedef StartRefresh = void Function(); + +typedef ConnectToNode = int Function(); + +typedef SetRefreshFromBlockHeight = void Function(int); + +typedef SetRecoveringFromSeed = void Function(int); + +typedef Store = void Function(Pointer); + +typedef SetListener = void Function(); + +typedef GetSyncingHeight = int Function(); + +typedef IsNeededToRefresh = int Function(); + +typedef IsNewTransactionExist = int Function(); + +typedef SubaddressSize = int Function(); + +typedef SubaddressRefresh = void Function(int); + +typedef SubaddressGetAll = Pointer Function(); + +typedef SubaddressAddNew = void Function(int accountIndex, Pointer label); + +typedef SubaddressSetLabel = void Function( + int accountIndex, int addressIndex, Pointer label); + +typedef AccountSize = int Function(); + +typedef AccountRefresh = void Function(); + +typedef AccountGetAll = Pointer Function(); + +typedef AccountAddNew = void Function(Pointer label); + +typedef AccountSetLabel = void Function(int accountIndex, Pointer label); + +typedef TransactionsRefresh = void Function(); + +typedef GetTxKey = Pointer Function(Pointer txId); + +typedef TransactionsCount = int Function(); + +typedef TransactionsGetAll = Pointer Function(); + +typedef TransactionCreate = int Function( + Pointer address, + Pointer assetType, + Pointer paymentId, + Pointer amount, + int priorityRaw, + int subaddrAccount, + Pointer error, + Pointer pendingTransaction); + +typedef TransactionCreateMultDest = int Function( + Pointer> addresses, + Pointer assetType, + Pointer paymentId, + Pointer> amounts, + int size, + int priorityRaw, + int subaddrAccount, + Pointer error, + Pointer pendingTransaction); + +typedef TransactionCommit = int Function(Pointer, Pointer); + +typedef SecretViewKey = Pointer Function(); + +typedef PublicViewKey = Pointer Function(); + +typedef SecretSpendKey = Pointer Function(); + +typedef PublicSpendKey = Pointer Function(); + +typedef CloseCurrentWallet = void Function(); + +typedef OnStartup = void Function(); + +typedef RescanBlockchainAsync = void Function(); + +typedef AssetTypes = Pointer> Function(); + +typedef AssetTypesSize = int Function(); + +typedef GetRate = Pointer Function(); + +typedef SizeOfRate = int Function(); + +typedef UpdateRate = void Function(); \ No newline at end of file diff --git a/cw_haven/lib/api/wallet.dart b/cw_haven/lib/api/wallet.dart new file mode 100644 index 000000000..e490743d2 --- /dev/null +++ b/cw_haven/lib/api/wallet.dart @@ -0,0 +1,329 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:cw_haven/api/convert_utf8_to_string.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/exceptions/setup_wallet_exception.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +int _boolToInt(bool value) => value ? 1 : 0; + +final getFileNameNative = havenApi + .lookup>('get_filename') + .asFunction(); + +final getSeedNative = + havenApi.lookup>('seed').asFunction(); + +final getAddressNative = havenApi + .lookup>('get_address') + .asFunction(); + +final getFullBalanceNative = havenApi + .lookup>('get_full_balance') + .asFunction(); + +final getUnlockedBalanceNative = havenApi + .lookup>('get_unlocked_balance') + .asFunction(); + +final getCurrentHeightNative = havenApi + .lookup>('get_current_height') + .asFunction(); + +final getNodeHeightNative = havenApi + .lookup>('get_node_height') + .asFunction(); + +final isConnectedNative = havenApi + .lookup>('is_connected') + .asFunction(); + +final setupNodeNative = havenApi + .lookup>('setup_node') + .asFunction(); + +final startRefreshNative = havenApi + .lookup>('start_refresh') + .asFunction(); + +final connecToNodeNative = havenApi + .lookup>('connect_to_node') + .asFunction(); + +final setRefreshFromBlockHeightNative = havenApi + .lookup>( + 'set_refresh_from_block_height') + .asFunction(); + +final setRecoveringFromSeedNative = havenApi + .lookup>( + 'set_recovering_from_seed') + .asFunction(); + +final storeNative = + havenApi.lookup>('store').asFunction(); + +final setListenerNative = havenApi + .lookup>('set_listener') + .asFunction(); + +final getSyncingHeightNative = havenApi + .lookup>('get_syncing_height') + .asFunction(); + +final isNeededToRefreshNative = havenApi + .lookup>('is_needed_to_refresh') + .asFunction(); + +final isNewTransactionExistNative = havenApi + .lookup>( + 'is_new_transaction_exist') + .asFunction(); + +final getSecretViewKeyNative = havenApi + .lookup>('secret_view_key') + .asFunction(); + +final getPublicViewKeyNative = havenApi + .lookup>('public_view_key') + .asFunction(); + +final getSecretSpendKeyNative = havenApi + .lookup>('secret_spend_key') + .asFunction(); + +final getPublicSpendKeyNative = havenApi + .lookup>('public_spend_key') + .asFunction(); + +final closeCurrentWalletNative = havenApi + .lookup>('close_current_wallet') + .asFunction(); + +final onStartupNative = havenApi + .lookup>('on_startup') + .asFunction(); + +final rescanBlockchainAsyncNative = havenApi + .lookup>('rescan_blockchain') + .asFunction(); + +int getSyncingHeight() => getSyncingHeightNative(); + +bool isNeededToRefresh() => isNeededToRefreshNative() != 0; + +bool isNewTransactionExist() => isNewTransactionExistNative() != 0; + +String getFilename() => convertUTF8ToString(pointer: getFileNameNative()); + +String getSeed() => convertUTF8ToString(pointer: getSeedNative()); + +String getAddress({int accountIndex = 0, int addressIndex = 0}) => + convertUTF8ToString(pointer: getAddressNative(accountIndex, addressIndex)); + +int getFullBalance({int accountIndex = 0}) => + getFullBalanceNative(accountIndex); + +int getUnlockedBalance({int accountIndex = 0}) => + getUnlockedBalanceNative(accountIndex); + +int getCurrentHeight() => getCurrentHeightNative(); + +int getNodeHeightSync() => getNodeHeightNative(); + +bool isConnectedSync() => isConnectedNative() != 0; + +bool setupNodeSync( + {String address, + String login, + String password, + bool useSSL = false, + bool isLightWallet = false}) { + final addressPointer = Utf8.toUtf8(address); + Pointer loginPointer; + Pointer passwordPointer; + + if (login != null) { + loginPointer = Utf8.toUtf8(login); + } + + if (password != null) { + passwordPointer = Utf8.toUtf8(password); + } + + final errorMessagePointer = allocate(); + final isSetupNode = setupNodeNative( + addressPointer, + loginPointer, + passwordPointer, + _boolToInt(useSSL), + _boolToInt(isLightWallet), + errorMessagePointer) != + 0; + + free(addressPointer); + free(loginPointer); + free(passwordPointer); + + if (!isSetupNode) { + throw SetupWalletException( + message: convertUTF8ToString(pointer: errorMessagePointer)); + } + + return isSetupNode; +} + +void startRefreshSync() => startRefreshNative(); + +Future connectToNode() async => connecToNodeNative() != 0; + +void setRefreshFromBlockHeight({int height}) => + setRefreshFromBlockHeightNative(height); + +void setRecoveringFromSeed({bool isRecovery}) => + setRecoveringFromSeedNative(_boolToInt(isRecovery)); + +void storeSync() { + final pathPointer = Utf8.toUtf8(''); + storeNative(pathPointer); + free(pathPointer); +} + +void closeCurrentWallet() => closeCurrentWalletNative(); + +String getSecretViewKey() => + convertUTF8ToString(pointer: getSecretViewKeyNative()); + +String getPublicViewKey() => + convertUTF8ToString(pointer: getPublicViewKeyNative()); + +String getSecretSpendKey() => + convertUTF8ToString(pointer: getSecretSpendKeyNative()); + +String getPublicSpendKey() => + convertUTF8ToString(pointer: getPublicSpendKeyNative()); + +class SyncListener { + SyncListener(this.onNewBlock, this.onNewTransaction) { + _cachedBlockchainHeight = 0; + _lastKnownBlockHeight = 0; + _initialSyncHeight = 0; + } + + void Function(int, int, double) onNewBlock; + void Function() onNewTransaction; + + Timer _updateSyncInfoTimer; + int _cachedBlockchainHeight; + int _lastKnownBlockHeight; + int _initialSyncHeight; + + Future getNodeHeightOrUpdate(int baseHeight) async { + if (_cachedBlockchainHeight < baseHeight || _cachedBlockchainHeight == 0) { + _cachedBlockchainHeight = await getNodeHeight(); + } + + return _cachedBlockchainHeight; + } + + void start() { + _cachedBlockchainHeight = 0; + _lastKnownBlockHeight = 0; + _initialSyncHeight = 0; + _updateSyncInfoTimer ??= + Timer.periodic(Duration(milliseconds: 1200), (_) async { + if (isNewTransactionExist()) { + onNewTransaction?.call(); + } + + var syncHeight = getSyncingHeight(); + + if (syncHeight <= 0) { + syncHeight = getCurrentHeight(); + } + + if (_initialSyncHeight <= 0) { + _initialSyncHeight = syncHeight; + } + + final bchHeight = await getNodeHeightOrUpdate(syncHeight); + + if (_lastKnownBlockHeight == syncHeight || syncHeight == null) { + return; + } + + _lastKnownBlockHeight = syncHeight; + final track = bchHeight - _initialSyncHeight; + final diff = track - (bchHeight - syncHeight); + final ptc = diff <= 0 ? 0.0 : diff / track; + final left = bchHeight - syncHeight; + + if (syncHeight < 0 || left < 0) { + return; + } + + // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents; + onNewBlock?.call(syncHeight, left, ptc); + }); + } + + void stop() => _updateSyncInfoTimer?.cancel(); +} + +SyncListener setListeners(void Function(int, int, double) onNewBlock, + void Function() onNewTransaction) { + final listener = SyncListener(onNewBlock, onNewTransaction); + setListenerNative(); + return listener; +} + +void onStartup() => onStartupNative(); + +void _storeSync(Object _) => storeSync(); + +bool _setupNodeSync(Map args) { + final address = args['address'] as String; + final login = (args['login'] ?? '') as String; + final password = (args['password'] ?? '') as String; + final useSSL = args['useSSL'] as bool; + final isLightWallet = args['isLightWallet'] as bool; + + return setupNodeSync( + address: address, + login: login, + password: password, + useSSL: useSSL, + isLightWallet: isLightWallet); +} + +bool _isConnected(Object _) => isConnectedSync(); + +int _getNodeHeight(Object _) => getNodeHeightSync(); + +void startRefresh() => startRefreshSync(); + +Future setupNode( + {String address, + String login, + String password, + bool useSSL = false, + bool isLightWallet = false}) => + compute, void>(_setupNodeSync, { + 'address': address, + 'login': login, + 'password': password, + 'useSSL': useSSL, + 'isLightWallet': isLightWallet + }); + +Future store() => compute(_storeSync, 0); + +Future isConnected() => compute(_isConnected, 0); + +Future getNodeHeight() => compute(_getNodeHeight, 0); + +void rescanBlockchainAsync() => rescanBlockchainAsyncNative(); diff --git a/cw_haven/lib/api/wallet_manager.dart b/cw_haven/lib/api/wallet_manager.dart new file mode 100644 index 000000000..d134dcbb4 --- /dev/null +++ b/cw_haven/lib/api/wallet_manager.dart @@ -0,0 +1,248 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_haven/api/convert_utf8_to_string.dart'; +import 'package:cw_haven/api/signatures.dart'; +import 'package:cw_haven/api/types.dart'; +import 'package:cw_haven/api/haven_api.dart'; +import 'package:cw_haven/api/wallet.dart'; +import 'package:cw_haven/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_haven/api/exceptions/wallet_creation_exception.dart'; +import 'package:cw_haven/api/exceptions/wallet_restore_from_keys_exception.dart'; +import 'package:cw_haven/api/exceptions/wallet_restore_from_seed_exception.dart'; + +final createWalletNative = havenApi + .lookup>('create_wallet') + .asFunction(); + +final restoreWalletFromSeedNative = havenApi + .lookup>( + 'restore_wallet_from_seed') + .asFunction(); + +final restoreWalletFromKeysNative = havenApi + .lookup>( + 'restore_wallet_from_keys') + .asFunction(); + +final isWalletExistNative = havenApi + .lookup>('is_wallet_exist') + .asFunction(); + +final loadWalletNative = havenApi + .lookup>('load_wallet') + .asFunction(); + +final errorStringNative = havenApi + .lookup>('error_string') + .asFunction(); + +void createWalletSync( + {String path, String password, String language, int nettype = 0}) { + final pathPointer = Utf8.toUtf8(path); + final passwordPointer = Utf8.toUtf8(password); + final languagePointer = Utf8.toUtf8(language); + final errorMessagePointer = allocate(); + final isWalletCreated = createWalletNative(pathPointer, passwordPointer, + languagePointer, nettype, errorMessagePointer) != + 0; + + free(pathPointer); + free(passwordPointer); + free(languagePointer); + + if (!isWalletCreated) { + throw WalletCreationException( + message: convertUTF8ToString(pointer: errorMessagePointer)); + } + + // setupNodeSync(address: "node.moneroworld.com:18089"); +} + +bool isWalletExistSync({String path}) { + final pathPointer = Utf8.toUtf8(path); + final isExist = isWalletExistNative(pathPointer) != 0; + + free(pathPointer); + + return isExist; +} + +void restoreWalletFromSeedSync( + {String path, + String password, + String seed, + int nettype = 0, + int restoreHeight = 0}) { + final pathPointer = Utf8.toUtf8(path); + final passwordPointer = Utf8.toUtf8(password); + final seedPointer = Utf8.toUtf8(seed); + final errorMessagePointer = allocate(); + final isWalletRestored = restoreWalletFromSeedNative( + pathPointer, + passwordPointer, + seedPointer, + nettype, + restoreHeight, + errorMessagePointer) != + 0; + + free(pathPointer); + free(passwordPointer); + free(seedPointer); + + if (!isWalletRestored) { + throw WalletRestoreFromSeedException( + message: convertUTF8ToString(pointer: errorMessagePointer)); + } +} + +void restoreWalletFromKeysSync( + {String path, + String password, + String language, + String address, + String viewKey, + String spendKey, + int nettype = 0, + int restoreHeight = 0}) { + final pathPointer = Utf8.toUtf8(path); + final passwordPointer = Utf8.toUtf8(password); + final languagePointer = Utf8.toUtf8(language); + final addressPointer = Utf8.toUtf8(address); + final viewKeyPointer = Utf8.toUtf8(viewKey); + final spendKeyPointer = Utf8.toUtf8(spendKey); + final errorMessagePointer = allocate(); + final isWalletRestored = restoreWalletFromKeysNative( + pathPointer, + passwordPointer, + languagePointer, + addressPointer, + viewKeyPointer, + spendKeyPointer, + nettype, + restoreHeight, + errorMessagePointer) != + 0; + + free(pathPointer); + free(passwordPointer); + free(languagePointer); + free(addressPointer); + free(viewKeyPointer); + free(spendKeyPointer); + + if (!isWalletRestored) { + throw WalletRestoreFromKeysException( + message: convertUTF8ToString(pointer: errorMessagePointer)); + } +} + +void loadWallet({String path, String password, int nettype = 0}) { + final pathPointer = Utf8.toUtf8(path); + final passwordPointer = Utf8.toUtf8(password); + final loaded = loadWalletNative(pathPointer, passwordPointer, nettype) != 0; + free(pathPointer); + free(passwordPointer); + + if (!loaded) { + throw WalletOpeningException( + message: convertUTF8ToString(pointer: errorStringNative())); + } +} + +void _createWallet(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final language = args['language'] as String; + + createWalletSync(path: path, password: password, language: language); +} + +void _restoreFromSeed(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final seed = args['seed'] as String; + final restoreHeight = args['restoreHeight'] as int; + + restoreWalletFromSeedSync( + path: path, password: password, seed: seed, restoreHeight: restoreHeight); +} + +void _restoreFromKeys(Map args) { + final path = args['path'] as String; + final password = args['password'] as String; + final language = args['language'] as String; + final restoreHeight = args['restoreHeight'] as int; + final address = args['address'] as String; + final viewKey = args['viewKey'] as String; + final spendKey = args['spendKey'] as String; + + restoreWalletFromKeysSync( + path: path, + password: password, + language: language, + restoreHeight: restoreHeight, + address: address, + viewKey: viewKey, + spendKey: spendKey); +} + +Future _openWallet(Map args) async => + loadWallet(path: args['path'], password: args['password']); + +bool _isWalletExist(String path) => isWalletExistSync(path: path); + +void openWallet({String path, String password, int nettype = 0}) async => + loadWallet(path: path, password: password, nettype: nettype); + +Future openWalletAsync(Map args) async => + compute(_openWallet, args); + +Future createWallet( + {String path, + String password, + String language, + int nettype = 0}) async => + compute(_createWallet, { + 'path': path, + 'password': password, + 'language': language, + 'nettype': nettype + }); + +Future restoreFromSeed( + {String path, + String password, + String seed, + int nettype = 0, + int restoreHeight = 0}) async => + compute, void>(_restoreFromSeed, { + 'path': path, + 'password': password, + 'seed': seed, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + +Future restoreFromKeys( + {String path, + String password, + String language, + String address, + String viewKey, + String spendKey, + int nettype = 0, + int restoreHeight = 0}) async => + compute, void>(_restoreFromKeys, { + 'path': path, + 'password': password, + 'language': language, + 'address': address, + 'viewKey': viewKey, + 'spendKey': spendKey, + 'nettype': nettype, + 'restoreHeight': restoreHeight + }); + +Future isWalletExist({String path}) => compute(_isWalletExist, path); diff --git a/cw_haven/lib/haven_account_list.dart b/cw_haven/lib/haven_account_list.dart new file mode 100644 index 000000000..ad1cd2a7b --- /dev/null +++ b/cw_haven/lib/haven_account_list.dart @@ -0,0 +1,84 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/account.dart'; +import 'package:cw_core/account_list.dart'; +import 'package:cw_haven/api/account_list.dart' as account_list; + +part 'haven_account_list.g.dart'; + +class HavenAccountList = HavenAccountListBase with _$HavenAccountList; + +abstract class HavenAccountListBase extends AccountList with Store { + HavenAccountListBase() + : accounts = ObservableList(), + _isRefreshing = false, + _isUpdating = false { + refresh(); + } + + @override + @observable + ObservableList accounts; + bool _isRefreshing; + bool _isUpdating; + + @override + void update() async { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(); + final accounts = getAll(); + + if (accounts.isNotEmpty) { + this.accounts.clear(); + this.accounts.addAll(accounts); + } + + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + @override + List getAll() => account_list + .getAllAccount() + .map((accountRow) => Account( + id: accountRow.getId(), + label: accountRow.getLabel())) + .toList(); + + @override + Future addAccount({String label}) async { + await account_list.addAccount(label: label); + update(); + } + + @override + Future setLabelAccount({int accountIndex, String label}) async { + await account_list.setLabelForAccount( + accountIndex: accountIndex, label: label); + update(); + } + + @override + void refresh() { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + account_list.refreshAccounts(); + _isRefreshing = false; + } catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } +} diff --git a/cw_haven/lib/haven_balance.dart b/cw_haven/lib/haven_balance.dart new file mode 100644 index 000000000..17172fa9c --- /dev/null +++ b/cw_haven/lib/haven_balance.dart @@ -0,0 +1,34 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_balance.dart'; +import 'package:cw_haven/api/balance_list.dart'; +import 'package:cw_haven/api/structs/haven_balance_row.dart'; + +const inactiveBalances = [ + CryptoCurrency.xcad, + CryptoCurrency.xjpy, + CryptoCurrency.xnok, + CryptoCurrency.xnzd]; + +Map getHavenBalance({int accountIndex}) { + final fullBalances = getHavenFullBalance(accountIndex: accountIndex); + final unlockedBalances = getHavenUnlockedBalance(accountIndex: accountIndex); + final havenBalances = {}; + final balancesLength = fullBalances.length; + + for (int i = 0; i < balancesLength; i++) { + final assetType = fullBalances[i].getAssetType(); + final fullBalance = fullBalances[i].getAmount(); + final unlockedBalance = unlockedBalances[i].getAmount(); + final moneroBalance = MoneroBalance( + fullBalance: fullBalance, unlockedBalance: unlockedBalance); + final currency = CryptoCurrency.fromString(assetType); + + if (inactiveBalances.indexOf(currency) >= 0) { + continue; + } + + havenBalances[currency] = moneroBalance; + } + + return havenBalances; +} \ No newline at end of file diff --git a/cw_haven/lib/haven_subaddress_list.dart b/cw_haven/lib/haven_subaddress_list.dart new file mode 100644 index 000000000..b6213d784 --- /dev/null +++ b/cw_haven/lib/haven_subaddress_list.dart @@ -0,0 +1,87 @@ +import 'package:cw_haven/api/structs/subaddress_row.dart'; +import 'package:flutter/services.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_haven/api/subaddress_list.dart' as subaddress_list; +import 'package:cw_core/subaddress.dart'; + +part 'haven_subaddress_list.g.dart'; + +class HavenSubaddressList = HavenSubaddressListBase + with _$HavenSubaddressList; + +abstract class HavenSubaddressListBase with Store { + HavenSubaddressListBase() { + _isRefreshing = false; + _isUpdating = false; + subaddresses = ObservableList(); + } + + @observable + ObservableList subaddresses; + + bool _isRefreshing; + bool _isUpdating; + + void update({int accountIndex}) { + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + subaddresses.addAll(getAll()); + _isUpdating = false; + } catch (e) { + _isUpdating = false; + rethrow; + } + } + + List getAll() { + var subaddresses = subaddress_list.getAllSubaddresses(); + + if (subaddresses.length > 2) { + final primary = subaddresses.first; + final rest = subaddresses.sublist(1).reversed; + subaddresses = [primary] + rest.toList(); + } + + return subaddresses + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getLabel())) + .toList(); + } + + Future addSubaddress({int accountIndex, String label}) async { + await subaddress_list.addSubaddress( + accountIndex: accountIndex, label: label); + update(accountIndex: accountIndex); + } + + Future setLabelSubaddress( + {int accountIndex, int addressIndex, String label}) async { + await subaddress_list.setLabelForSubaddress( + accountIndex: accountIndex, addressIndex: addressIndex, label: label); + update(accountIndex: accountIndex); + } + + void refresh({int accountIndex}) { + if (_isRefreshing) { + return; + } + + try { + _isRefreshing = true; + subaddress_list.refreshSubaddresses(accountIndex: accountIndex); + _isRefreshing = false; + } on PlatformException catch (e) { + _isRefreshing = false; + print(e); + rethrow; + } + } +} diff --git a/cw_haven/lib/haven_transaction_creation_credentials.dart b/cw_haven/lib/haven_transaction_creation_credentials.dart new file mode 100644 index 000000000..88525f0fd --- /dev/null +++ b/cw_haven/lib/haven_transaction_creation_credentials.dart @@ -0,0 +1,10 @@ +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class HavenTransactionCreationCredentials { + HavenTransactionCreationCredentials({this.outputs, this.priority, this.assetType}); + + final List outputs; + final MoneroTransactionPriority priority; + final String assetType; +} diff --git a/cw_haven/lib/haven_transaction_creation_exception.dart b/cw_haven/lib/haven_transaction_creation_exception.dart new file mode 100644 index 000000000..768abe7fa --- /dev/null +++ b/cw_haven/lib/haven_transaction_creation_exception.dart @@ -0,0 +1,8 @@ +class HavenTransactionCreationException implements Exception { + HavenTransactionCreationException(this.message); + + final String message; + + @override + String toString() => message; +} \ No newline at end of file diff --git a/cw_haven/lib/haven_transaction_history.dart b/cw_haven/lib/haven_transaction_history.dart new file mode 100644 index 000000000..b456174fe --- /dev/null +++ b/cw_haven/lib/haven_transaction_history.dart @@ -0,0 +1,27 @@ +import 'dart:core'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_haven/haven_transaction_info.dart'; + +part 'haven_transaction_history.g.dart'; + +class HavenTransactionHistory = HavenTransactionHistoryBase + with _$HavenTransactionHistory; + +abstract class HavenTransactionHistoryBase + extends TransactionHistoryBase with Store { + HavenTransactionHistoryBase() { + transactions = ObservableMap(); + } + + @override + Future save() async {} + + @override + void addOne(HavenTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); +} diff --git a/cw_haven/lib/haven_transaction_info.dart b/cw_haven/lib/haven_transaction_info.dart new file mode 100644 index 000000000..16f032541 --- /dev/null +++ b/cw_haven/lib/haven_transaction_info.dart @@ -0,0 +1,70 @@ +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_haven/api/structs/transaction_info_row.dart'; +import 'package:cw_core/parseBoolFromString.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_haven/api/transaction_history.dart'; + +class HavenTransactionInfo extends TransactionInfo { + HavenTransactionInfo(this.id, this.height, this.direction, this.date, + this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee); + + HavenTransactionInfo.fromMap(Map map) + : id = (map['hash'] ?? '') as String, + height = (map['height'] ?? 0) as int, + direction = + parseTransactionDirectionFromNumber(map['direction'] as String) ?? + TransactionDirection.incoming, + date = DateTime.fromMillisecondsSinceEpoch( + (int.parse(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, + key = getTxKey((map['hash'] ?? '') as String), + fee = map['fee'] as int ?? 0; + + HavenTransactionInfo.fromRow(TransactionInfoRow row) + : id = row.getHash(), + height = row.blockHeight, + direction = parseTransactionDirectionFromInt(row.direction) ?? + TransactionDirection.incoming, + date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), + isPending = row.isPending != 0, + amount = row.getAmount(), + accountIndex = row.subaddrAccount, + addressIndex = row.subaddrIndex, + key = null, //getTxKey(row.getHash()), + fee = row.fee, + assetType = row.getAssetType(); + + final String id; + final int height; + final TransactionDirection direction; + final DateTime date; + final int accountIndex; + final bool isPending; + final int amount; + final int fee; + final int addressIndex; + String recipientAddress; + String key; + String assetType; + + String _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount(moneroAmountToString(amount: amount))} $assetType'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => + '${formatAmount(moneroAmountToString(amount: fee))} $assetType'; +} diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart new file mode 100644 index 000000000..7fb8ed157 --- /dev/null +++ b/cw_haven/lib/haven_wallet.dart @@ -0,0 +1,388 @@ +import 'dart:async'; +import 'package:cw_core/crypto_currency.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'; +import 'package:cw_haven/haven_transaction_creation_exception.dart'; +import 'package:cw_haven/haven_transaction_info.dart'; +import 'package:cw_haven/haven_wallet_addresses.dart'; +import 'package:cw_core/monero_wallet_utils.dart'; +import 'package:cw_haven/api/structs/pending_transaction.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_haven/api/transaction_history.dart' + as haven_transaction_history; +//import 'package:cw_haven/wallet.dart'; +import 'package:cw_haven/api/wallet.dart' as haven_wallet; +import 'package:cw_haven/api/transaction_history.dart' as transaction_history; +import 'package:cw_haven/api/monero_output.dart'; +import 'package:cw_haven/pending_haven_transaction.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_balance.dart'; +import 'package:cw_haven/haven_transaction_history.dart'; +import 'package:cw_core/account.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_haven/haven_balance.dart'; + +part 'haven_wallet.g.dart'; + +const moneroBlockSize = 1000; + +class HavenWallet = HavenWalletBase with _$HavenWallet; + +abstract class HavenWalletBase extends WalletBase with Store { + HavenWalletBase({WalletInfo walletInfo}) + : super(walletInfo) { + transactionHistory = HavenTransactionHistory(); + balance = ObservableMap.of(getHavenBalance(accountIndex: 0)); + _isTransactionUpdating = false; + _hasSyncAfterStartup = false; + walletAddresses = HavenWalletAddresses(walletInfo); + _onAccountChangeReaction = reaction((_) => walletAddresses.account, + (Account account) { + balance.addAll(getHavenBalance(accountIndex: account.id)); + walletAddresses.updateSubaddressList(accountIndex: account.id); + }); + } + + static const int _autoSaveInterval = 30; + + @override + HavenWalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + ObservableMap balance; + + @override + String get seed => haven_wallet.getSeed(); + + @override + MoneroWalletKeys get keys => MoneroWalletKeys( + privateSpendKey: haven_wallet.getSecretSpendKey(), + privateViewKey: haven_wallet.getSecretViewKey(), + publicSpendKey: haven_wallet.getPublicSpendKey(), + publicViewKey: haven_wallet.getPublicViewKey()); + + haven_wallet.SyncListener _listener; + ReactionDisposer _onAccountChangeReaction; + bool _isTransactionUpdating; + bool _hasSyncAfterStartup; + Timer _autoSaveTimer; + + Future init() async { + await walletAddresses.init(); + balance.addAll(getHavenBalance(accountIndex: walletAddresses.account.id ?? 0)); + _setListeners(); + await updateTransactions(); + + if (walletInfo.isRecovery) { + haven_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); + + if (haven_wallet.getCurrentHeight() <= 1) { + haven_wallet.setRefreshFromBlockHeight( + height: walletInfo.restoreHeight); + } + } + + _autoSaveTimer = Timer.periodic( + Duration(seconds: _autoSaveInterval), + (_) async => await save()); + } + + @override + void close() { + _listener?.stop(); + _onAccountChangeReaction?.reaction?.dispose(); + _autoSaveTimer?.cancel(); + } + + @override + Future connectToNode({@required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + await haven_wallet.setupNode( + address: node.uriRaw, + login: node.login, + password: node.password, + useSSL: node.useSSL, + isLightWallet: false); // FIXME: hardcoded value + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + print(e); + } + } + + @override + Future startSync() async { + try { + _setInitialHeight(); + } catch (_) {} + + try { + syncStatus = StartingSyncStatus(); + haven_wallet.startRefresh(); + _setListeners(); + _listener?.start(); + } catch (e) { + syncStatus = FailedSyncStatus(); + print(e); + rethrow; + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as HavenTransactionCreationCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final assetType = CryptoCurrency.fromString(_credentials.assetType.toLowerCase()); + final balances = getHavenBalance(accountIndex: walletAddresses.account.id); + final unlockedBalance = balances[assetType].unlockedBalance; + + PendingTransactionDescription pendingTransactionDescription; + + if (!(syncStatus is SyncedSyncStatus)) { + throw HavenTransactionCreationException('The wallet is not synced.'); + } + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll + || item.formattedCryptoAmount <= 0)) { + throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + } + + final int totalAmount = outputs.fold(0, (acc, value) => + acc + value.formattedCryptoAmount); + + if (unlockedBalance < totalAmount) { + throw HavenTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + } + + final moneroOutputs = outputs.map((output) => + MoneroOutput( + address: output.address, + amount: output.cryptoAmount.replaceAll(',', '.'))) + .toList(); + + pendingTransactionDescription = + await transaction_history.createTransactionMultDest( + outputs: moneroOutputs, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account.id); + } else { + final output = outputs.first; + final address = output.address; + final amount = output.sendAll + ? null + : output.cryptoAmount.replaceAll(',', '.'); + final formattedAmount = output.sendAll + ? null + : output.formattedCryptoAmount; + + if ((formattedAmount != null && unlockedBalance < formattedAmount) || + (formattedAmount == null && unlockedBalance <= 0)) { + final formattedBalance = moneroAmountToString(amount: unlockedBalance); + + throw HavenTransactionCreationException( + 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.'); + } + + pendingTransactionDescription = + await transaction_history.createTransaction( + address: address, + assetType: _credentials.assetType, + amount: amount, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account.id); + } + + return PendingHavenTransaction(pendingTransactionDescription, assetType); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int amount) { + // FIXME: hardcoded value; + + if (priority is MoneroTransactionPriority) { + switch (priority) { + case MoneroTransactionPriority.slow: + return 24590000; + case MoneroTransactionPriority.regular: + return 123050000; + case MoneroTransactionPriority.medium: + return 245029999; + case MoneroTransactionPriority.fast: + return 614530000; + case MoneroTransactionPriority.fastest: + return 26021600000; + } + } + + return 0; + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + await backupWalletFiles(name); + await haven_wallet.store(); + } + + Future getNodeHeight() async => haven_wallet.getNodeHeight(); + + Future isConnected() async => haven_wallet.isConnected(); + + Future setAsRecovered() async { + walletInfo.isRecovery = false; + await walletInfo.save(); + } + + @override + Future rescan({int height}) async { + walletInfo.restoreHeight = height; + walletInfo.isRecovery = true; + haven_wallet.setRefreshFromBlockHeight(height: height); + haven_wallet.rescanBlockchainAsync(); + await startSync(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + await _askForUpdateTransactionHistory(); + await save(); + await walletInfo.save(); + } + + String getTransactionAddress(int accountIndex, int addressIndex) => + haven_wallet.getAddress( + accountIndex: accountIndex, + addressIndex: addressIndex); + + @override + Future> fetchTransactions() async { + haven_transaction_history.refreshTransactions(); + return _getAllTransactions(null).fold>( + {}, + (Map acc, HavenTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + List _getAllTransactions(dynamic _) => haven_transaction_history + .getAllTransations() + .map((row) => HavenTransactionInfo.fromRow(row)) + .toList(); + + void _setListeners() { + _listener?.stop(); + _listener = haven_wallet.setListeners(_onNewBlock, _onNewTransaction); + } + + void _setInitialHeight() { + if (walletInfo.isRecovery) { + return; + } + + final currentHeight = haven_wallet.getCurrentHeight(); + + if (currentHeight <= 1) { + final height = _getHeightByDate(walletInfo.date); + haven_wallet.setRecoveringFromSeed(isRecovery: true); + haven_wallet.setRefreshFromBlockHeight(height: height); + } + } + + int _getHeightDistance(DateTime date) { + final distance = + DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; + final daysTmp = (distance / 86400).round(); + final days = daysTmp < 1 ? 1 : daysTmp; + + return days * 1000; + } + + int _getHeightByDate(DateTime date) { + final nodeHeight = haven_wallet.getNodeHeightSync(); + final heightDistance = _getHeightDistance(date); + + if (nodeHeight <= 0) { + return 0; + } + + return nodeHeight - heightDistance; + } + + void _askForUpdateBalance() => + balance.addAll(getHavenBalance(accountIndex: walletAddresses.account.id)); + + Future _askForUpdateTransactionHistory() async => + await updateTransactions(); + + void _onNewBlock(int height, int blocksLeft, double ptc) async { + try { + if (walletInfo.isRecovery) { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + } + + if (blocksLeft < 1000) { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + walletAddresses.accountList.update(); + syncStatus = SyncedSyncStatus(); + + if (!_hasSyncAfterStartup) { + _hasSyncAfterStartup = true; + await save(); + } + + if (walletInfo.isRecovery) { + await setAsRecovered(); + } + } else { + syncStatus = SyncingSyncStatus(blocksLeft, ptc); + } + } catch (e) { + print(e.toString()); + } + } + + void _onNewTransaction() async { + try { + await _askForUpdateTransactionHistory(); + _askForUpdateBalance(); + await Future.delayed(Duration(seconds: 1)); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_haven/lib/haven_wallet_addresses.dart b/cw_haven/lib/haven_wallet_addresses.dart new file mode 100644 index 000000000..7afad6205 --- /dev/null +++ b/cw_haven/lib/haven_wallet_addresses.dart @@ -0,0 +1,86 @@ +import 'package:cw_core/wallet_addresses_with_account.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/account.dart'; +import 'package:cw_haven/haven_account_list.dart'; +import 'package:cw_haven/haven_subaddress_list.dart'; +import 'package:cw_core/subaddress.dart'; +import 'package:mobx/mobx.dart'; + +part 'haven_wallet_addresses.g.dart'; + +class HavenWalletAddresses = HavenWalletAddressesBase + with _$HavenWalletAddresses; + +abstract class HavenWalletAddressesBase extends WalletAddressesWithAccount with Store { + HavenWalletAddressesBase(WalletInfo walletInfo) : super(walletInfo) { + accountList = HavenAccountList(); + subaddressList = HavenSubaddressList(); + } + + @override + @observable + String address; + + @override + @observable + Account account; + + @observable + Subaddress subaddress; + + HavenSubaddressList subaddressList; + + HavenAccountList accountList; + + @override + Future init() async { + accountList.update(); + account = accountList.accounts.first; + updateSubaddressList(accountIndex: account.id ?? 0); + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + final _subaddressList = HavenSubaddressList(); + + addressesMap.clear(); + + accountList.accounts.forEach((account) { + _subaddressList.update(accountIndex: account.id); + _subaddressList.subaddresses.forEach((subaddress) { + addressesMap[subaddress.address] = subaddress.label; + }); + }); + + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } + + bool validate() { + accountList.update(); + final accountListLength = accountList.accounts?.length ?? 0; + + if (accountListLength <= 0) { + return false; + } + + subaddressList.update(accountIndex: accountList.accounts.first.id); + final subaddressListLength = subaddressList.subaddresses?.length ?? 0; + + if (subaddressListLength <= 0) { + return false; + } + + return true; + } + + void updateSubaddressList({int accountIndex}) { + subaddressList.update(accountIndex: accountIndex); + subaddress = subaddressList.subaddresses.first; + address = subaddress.address; + } +} \ No newline at end of file diff --git a/cw_haven/lib/haven_wallet_service.dart b/cw_haven/lib/haven_wallet_service.dart new file mode 100644 index 000000000..333f03191 --- /dev/null +++ b/cw_haven/lib/haven_wallet_service.dart @@ -0,0 +1,228 @@ +import 'dart:io'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/monero_wallet_utils.dart'; +import 'package:hive/hive.dart'; +import 'package:cw_haven/api/wallet_manager.dart' as haven_wallet_manager; +import 'package:cw_haven/api/wallet.dart' as haven_wallet; +import 'package:cw_haven/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_haven/haven_wallet.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; + +class HavenNewWalletCredentials extends WalletCredentials { + HavenNewWalletCredentials({String name, String password, this.language}) + : super(name: name, password: password); + + final String language; +} + +class HavenRestoreWalletFromSeedCredentials extends WalletCredentials { + HavenRestoreWalletFromSeedCredentials( + {String name, String password, int height, this.mnemonic}) + : super(name: name, password: password, height: height); + + final String mnemonic; +} + +class HavenWalletLoadingException implements Exception { + @override + String toString() => 'Failure to load the wallet.'; +} + +class HavenRestoreWalletFromKeysCredentials extends WalletCredentials { + HavenRestoreWalletFromKeysCredentials( + {String name, + String password, + this.language, + this.address, + this.viewKey, + this.spendKey, + int height}) + : super(name: name, password: password, height: height); + + final String language; + final String address; + final String viewKey; + final String spendKey; +} + +class HavenWalletService extends WalletService< + HavenNewWalletCredentials, + HavenRestoreWalletFromSeedCredentials, + HavenRestoreWalletFromKeysCredentials> { + HavenWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + static bool walletFilesExist(String path) => + !File(path).existsSync() && !File('$path.keys').existsSync(); + + @override + WalletType getType() => WalletType.haven; + + @override + Future create(HavenNewWalletCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + await haven_wallet_manager.createWallet( + path: path, + password: credentials.password, + language: credentials.language); + final wallet = HavenWallet(walletInfo: credentials.walletInfo); + await wallet.init(); + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('HavenWalletsManager Error: ${e.toString()}'); + rethrow; + } + } + + @override + Future isWalletExit(String name) async { + try { + final path = await pathForWallet(name: name, type: getType()); + return haven_wallet_manager.isWalletExist(path: path); + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('HavenWalletsManager Error: $e'); + rethrow; + } + } + + @override + Future openWallet(String name, String password) async { + try { + final path = await pathForWallet(name: name, type: getType()); + + if (walletFilesExist(path)) { + await repairOldAndroidWallet(name); + } + + await haven_wallet_manager + .openWalletAsync({'path': path, 'password': password}); + final walletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(name, getType()), + orElse: () => null); + final wallet = HavenWallet(walletInfo: walletInfo); + final isValid = wallet.walletAddresses.validate(); + + if (!isValid) { + await restoreOrResetWalletFiles(name); + wallet.close(); + return openWallet(name, password); + } + + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + + if ((e.toString().contains('bad_alloc') || + (e is WalletOpeningException && + (e.message == 'std::bad_alloc' || + e.message.contains('bad_alloc')))) || + (e.toString().contains('does not correspond') || + (e is WalletOpeningException && + e.message.contains('does not correspond')))) { + await restoreOrResetWalletFiles(name); + return openWallet(name, password); + } + + rethrow; + } + } + + @override + Future remove(String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + } + + @override + Future restoreFromKeys( + HavenRestoreWalletFromKeysCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + await haven_wallet_manager.restoreFromKeys( + path: path, + password: credentials.password, + language: credentials.language, + restoreHeight: credentials.height, + address: credentials.address, + viewKey: credentials.viewKey, + spendKey: credentials.spendKey); + final wallet = HavenWallet(walletInfo: credentials.walletInfo); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('HavenWalletsManager Error: $e'); + rethrow; + } + } + + @override + Future restoreFromSeed( + HavenRestoreWalletFromSeedCredentials credentials) async { + try { + final path = await pathForWallet(name: credentials.name, type: getType()); + await haven_wallet_manager.restoreFromSeed( + path: path, + password: credentials.password, + seed: credentials.mnemonic, + restoreHeight: credentials.height); + final wallet = HavenWallet(walletInfo: credentials.walletInfo); + await wallet.init(); + + return wallet; + } catch (e) { + // TODO: Implement Exception for wallet list service. + print('HavenWalletsManager Error: $e'); + rethrow; + } + } + + Future repairOldAndroidWallet(String name) async { + try { + if (!Platform.isAndroid) { + return; + } + + final oldAndroidWalletDirPath = + await outdatedAndroidPathForWalletDir(name: name); + final dir = Directory(oldAndroidWalletDirPath); + + if (!dir.existsSync()) { + return; + } + + final newWalletDirPath = + await pathForWalletDir(name: name, type: getType()); + + dir.listSync().forEach((f) { + final file = File(f.path); + final name = f.path.split('/').last; + final newPath = newWalletDirPath + '/$name'; + final newFile = File(newPath); + + if (!newFile.existsSync()) { + newFile.createSync(); + } + newFile.writeAsBytesSync(file.readAsBytesSync()); + }); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_haven/lib/mnemonics/chinese_simplified.dart b/cw_haven/lib/mnemonics/chinese_simplified.dart new file mode 100644 index 000000000..da3225041 --- /dev/null +++ b/cw_haven/lib/mnemonics/chinese_simplified.dart @@ -0,0 +1,1630 @@ +class ChineseSimplifiedMnemonics { + static const words = [ + "的", + "一", + "是", + "在", + "不", + "了", + "有", + "和", + "人", + "这", + "中", + "大", + "为", + "上", + "个", + "国", + "我", + "以", + "要", + "他", + "时", + "来", + "用", + "们", + "生", + "到", + "作", + "地", + "于", + "出", + "就", + "分", + "对", + "成", + "会", + "可", + "主", + "发", + "年", + "动", + "同", + "工", + "也", + "能", + "下", + "过", + "子", + "说", + "产", + "种", + "面", + "而", + "方", + "后", + "多", + "定", + "行", + "学", + "法", + "所", + "民", + "得", + "经", + "十", + "三", + "之", + "进", + "着", + "等", + "部", + "度", + "家", + "电", + "力", + "里", + "如", + "水", + "化", + "高", + "自", + "二", + "理", + "起", + "小", + "物", + "现", + "实", + "加", + "量", + "都", + "两", + "体", + "制", + "机", + "当", + "使", + "点", + "从", + "业", + "本", + "去", + "把", + "性", + "好", + "应", + "开", + "它", + "合", + "还", + "因", + "由", + "其", + "些", + "然", + "前", + "外", + "天", + "政", + "四", + "日", + "那", + "社", + "义", + "事", + "平", + "形", + "相", + "全", + "表", + "间", + "样", + "与", + "关", + "各", + "重", + "新", + "线", + "内", + "数", + "正", + "心", + "反", + "你", + "明", + "看", + "原", + "又", + "么", + "利", + "比", + "或", + "但", + "质", + "气", + "第", + "向", + "道", + "命", + "此", + "变", + "条", + "只", + "没", + "结", + "解", + "问", + "意", + "建", + "月", + "公", + "无", + "系", + "军", + "很", + "情", + "者", + "最", + "立", + "代", + "想", + "已", + "通", + "并", + "提", + "直", + "题", + "党", + "程", + "展", + "五", + "果", + "料", + "象", + "员", + "革", + "位", + "入", + "常", + "文", + "总", + "次", + "品", + "式", + "活", + "设", + "及", + "管", + "特", + "件", + "长", + "求", + "老", + "头", + "基", + "资", + "边", + "流", + "路", + "级", + "少", + "图", + "山", + "统", + "接", + "知", + "较", + "将", + "组", + "见", + "计", + "别", + "她", + "手", + "角", + "期", + "根", + "论", + "运", + "农", + "指", + "几", + "九", + "区", + "强", + "放", + "决", + "西", + "被", + "干", + "做", + "必", + "战", + "先", + "回", + "则", + "任", + "取", + "据", + "处", + "队", + "南", + "给", + "色", + "光", + "门", + "即", + "保", + "治", + "北", + "造", + "百", + "规", + "热", + "领", + "七", + "海", + "口", + "东", + "导", + "器", + "压", + "志", + "世", + "金", + "增", + "争", + "济", + "阶", + "油", + "思", + "术", + "极", + "交", + "受", + "联", + "什", + "认", + "六", + "共", + "权", + "收", + "证", + "改", + "清", + "美", + "再", + "采", + "转", + "更", + "单", + "风", + "切", + "打", + "白", + "教", + "速", + "花", + "带", + "安", + "场", + "身", + "车", + "例", + "真", + "务", + "具", + "万", + "每", + "目", + "至", + "达", + "走", + "积", + "示", + "议", + "声", + "报", + "斗", + "完", + "类", + "八", + "离", + "华", + "名", + "确", + "才", + "科", + "张", + "信", + "马", + "节", + "话", + "米", + "整", + "空", + "元", + "况", + "今", + "集", + "温", + "传", + "土", + "许", + "步", + "群", + "广", + "石", + "记", + "需", + "段", + "研", + "界", + "拉", + "林", + "律", + "叫", + "且", + "究", + "观", + "越", + "织", + "装", + "影", + "算", + "低", + "持", + "音", + "众", + "书", + "布", + "复", + "容", + "儿", + "须", + "际", + "商", + "非", + "验", + "连", + "断", + "深", + "难", + "近", + "矿", + "千", + "周", + "委", + "素", + "技", + "备", + "半", + "办", + "青", + "省", + "列", + "习", + "响", + "约", + "支", + "般", + "史", + "感", + "劳", + "便", + "团", + "往", + "酸", + "历", + "市", + "克", + "何", + "除", + "消", + "构", + "府", + "称", + "太", + "准", + "精", + "值", + "号", + "率", + "族", + "维", + "划", + "选", + "标", + "写", + "存", + "候", + "毛", + "亲", + "快", + "效", + "斯", + "院", + "查", + "江", + "型", + "眼", + "王", + "按", + "格", + "养", + "易", + "置", + "派", + "层", + "片", + "始", + "却", + "专", + "状", + "育", + "厂", + "京", + "识", + "适", + "属", + "圆", + "包", + "火", + "住", + "调", + "满", + "县", + "局", + "照", + "参", + "红", + "细", + "引", + "听", + "该", + "铁", + "价", + "严", + "首", + "底", + "液", + "官", + "德", + "随", + "病", + "苏", + "失", + "尔", + "死", + "讲", + "配", + "女", + "黄", + "推", + "显", + "谈", + "罪", + "神", + "艺", + "呢", + "席", + "含", + "企", + "望", + "密", + "批", + "营", + "项", + "防", + "举", + "球", + "英", + "氧", + "势", + "告", + "李", + "台", + "落", + "木", + "帮", + "轮", + "破", + "亚", + "师", + "围", + "注", + "远", + "字", + "材", + "排", + "供", + "河", + "态", + "封", + "另", + "施", + "减", + "树", + "溶", + "怎", + "止", + "案", + "言", + "士", + "均", + "武", + "固", + "叶", + "鱼", + "波", + "视", + "仅", + "费", + "紧", + "爱", + "左", + "章", + "早", + "朝", + "害", + "续", + "轻", + "服", + "试", + "食", + "充", + "兵", + "源", + "判", + "护", + "司", + "足", + "某", + "练", + "差", + "致", + "板", + "田", + "降", + "黑", + "犯", + "负", + "击", + "范", + "继", + "兴", + "似", + "余", + "坚", + "曲", + "输", + "修", + "故", + "城", + "夫", + "够", + "送", + "笔", + "船", + "占", + "右", + "财", + "吃", + "富", + "春", + "职", + "觉", + "汉", + "画", + "功", + "巴", + "跟", + "虽", + "杂", + "飞", + "检", + "吸", + "助", + "升", + "阳", + "互", + "初", + "创", + "抗", + "考", + "投", + "坏", + "策", + "古", + "径", + "换", + "未", + "跑", + "留", + "钢", + "曾", + "端", + "责", + "站", + "简", + "述", + "钱", + "副", + "尽", + "帝", + "射", + "草", + "冲", + "承", + "独", + "令", + "限", + "阿", + "宣", + "环", + "双", + "请", + "超", + "微", + "让", + "控", + "州", + "良", + "轴", + "找", + "否", + "纪", + "益", + "依", + "优", + "顶", + "础", + "载", + "倒", + "房", + "突", + "坐", + "粉", + "敌", + "略", + "客", + "袁", + "冷", + "胜", + "绝", + "析", + "块", + "剂", + "测", + "丝", + "协", + "诉", + "念", + "陈", + "仍", + "罗", + "盐", + "友", + "洋", + "错", + "苦", + "夜", + "刑", + "移", + "频", + "逐", + "靠", + "混", + "母", + "短", + "皮", + "终", + "聚", + "汽", + "村", + "云", + "哪", + "既", + "距", + "卫", + "停", + "烈", + "央", + "察", + "烧", + "迅", + "境", + "若", + "印", + "洲", + "刻", + "括", + "激", + "孔", + "搞", + "甚", + "室", + "待", + "核", + "校", + "散", + "侵", + "吧", + "甲", + "游", + "久", + "菜", + "味", + "旧", + "模", + "湖", + "货", + "损", + "预", + "阻", + "毫", + "普", + "稳", + "乙", + "妈", + "植", + "息", + "扩", + "银", + "语", + "挥", + "酒", + "守", + "拿", + "序", + "纸", + "医", + "缺", + "雨", + "吗", + "针", + "刘", + "啊", + "急", + "唱", + "误", + "训", + "愿", + "审", + "附", + "获", + "茶", + "鲜", + "粮", + "斤", + "孩", + "脱", + "硫", + "肥", + "善", + "龙", + "演", + "父", + "渐", + "血", + "欢", + "械", + "掌", + "歌", + "沙", + "刚", + "攻", + "谓", + "盾", + "讨", + "晚", + "粒", + "乱", + "燃", + "矛", + "乎", + "杀", + "药", + "宁", + "鲁", + "贵", + "钟", + "煤", + "读", + "班", + "伯", + "香", + "介", + "迫", + "句", + "丰", + "培", + "握", + "兰", + "担", + "弦", + "蛋", + "沉", + "假", + "穿", + "执", + "答", + "乐", + "谁", + "顺", + "烟", + "缩", + "征", + "脸", + "喜", + "松", + "脚", + "困", + "异", + "免", + "背", + "星", + "福", + "买", + "染", + "井", + "概", + "慢", + "怕", + "磁", + "倍", + "祖", + "皇", + "促", + "静", + "补", + "评", + "翻", + "肉", + "践", + "尼", + "衣", + "宽", + "扬", + "棉", + "希", + "伤", + "操", + "垂", + "秋", + "宜", + "氢", + "套", + "督", + "振", + "架", + "亮", + "末", + "宪", + "庆", + "编", + "牛", + "触", + "映", + "雷", + "销", + "诗", + "座", + "居", + "抓", + "裂", + "胞", + "呼", + "娘", + "景", + "威", + "绿", + "晶", + "厚", + "盟", + "衡", + "鸡", + "孙", + "延", + "危", + "胶", + "屋", + "乡", + "临", + "陆", + "顾", + "掉", + "呀", + "灯", + "岁", + "措", + "束", + "耐", + "剧", + "玉", + "赵", + "跳", + "哥", + "季", + "课", + "凯", + "胡", + "额", + "款", + "绍", + "卷", + "齐", + "伟", + "蒸", + "殖", + "永", + "宗", + "苗", + "川", + "炉", + "岩", + "弱", + "零", + "杨", + "奏", + "沿", + "露", + "杆", + "探", + "滑", + "镇", + "饭", + "浓", + "航", + "怀", + "赶", + "库", + "夺", + "伊", + "灵", + "税", + "途", + "灭", + "赛", + "归", + "召", + "鼓", + "播", + "盘", + "裁", + "险", + "康", + "唯", + "录", + "菌", + "纯", + "借", + "糖", + "盖", + "横", + "符", + "私", + "努", + "堂", + "域", + "枪", + "润", + "幅", + "哈", + "竟", + "熟", + "虫", + "泽", + "脑", + "壤", + "碳", + "欧", + "遍", + "侧", + "寨", + "敢", + "彻", + "虑", + "斜", + "薄", + "庭", + "纳", + "弹", + "饲", + "伸", + "折", + "麦", + "湿", + "暗", + "荷", + "瓦", + "塞", + "床", + "筑", + "恶", + "户", + "访", + "塔", + "奇", + "透", + "梁", + "刀", + "旋", + "迹", + "卡", + "氯", + "遇", + "份", + "毒", + "泥", + "退", + "洗", + "摆", + "灰", + "彩", + "卖", + "耗", + "夏", + "择", + "忙", + "铜", + "献", + "硬", + "予", + "繁", + "圈", + "雪", + "函", + "亦", + "抽", + "篇", + "阵", + "阴", + "丁", + "尺", + "追", + "堆", + "雄", + "迎", + "泛", + "爸", + "楼", + "避", + "谋", + "吨", + "野", + "猪", + "旗", + "累", + "偏", + "典", + "馆", + "索", + "秦", + "脂", + "潮", + "爷", + "豆", + "忽", + "托", + "惊", + "塑", + "遗", + "愈", + "朱", + "替", + "纤", + "粗", + "倾", + "尚", + "痛", + "楚", + "谢", + "奋", + "购", + "磨", + "君", + "池", + "旁", + "碎", + "骨", + "监", + "捕", + "弟", + "暴", + "割", + "贯", + "殊", + "释", + "词", + "亡", + "壁", + "顿", + "宝", + "午", + "尘", + "闻", + "揭", + "炮", + "残", + "冬", + "桥", + "妇", + "警", + "综", + "招", + "吴", + "付", + "浮", + "遭", + "徐", + "您", + "摇", + "谷", + "赞", + "箱", + "隔", + "订", + "男", + "吹", + "园", + "纷", + "唐", + "败", + "宋", + "玻", + "巨", + "耕", + "坦", + "荣", + "闭", + "湾", + "键", + "凡", + "驻", + "锅", + "救", + "恩", + "剥", + "凝", + "碱", + "齿", + "截", + "炼", + "麻", + "纺", + "禁", + "废", + "盛", + "版", + "缓", + "净", + "睛", + "昌", + "婚", + "涉", + "筒", + "嘴", + "插", + "岸", + "朗", + "庄", + "街", + "藏", + "姑", + "贸", + "腐", + "奴", + "啦", + "惯", + "乘", + "伙", + "恢", + "匀", + "纱", + "扎", + "辩", + "耳", + "彪", + "臣", + "亿", + "璃", + "抵", + "脉", + "秀", + "萨", + "俄", + "网", + "舞", + "店", + "喷", + "纵", + "寸", + "汗", + "挂", + "洪", + "贺", + "闪", + "柬", + "爆", + "烯", + "津", + "稻", + "墙", + "软", + "勇", + "像", + "滚", + "厘", + "蒙", + "芳", + "肯", + "坡", + "柱", + "荡", + "腿", + "仪", + "旅", + "尾", + "轧", + "冰", + "贡", + "登", + "黎", + "削", + "钻", + "勒", + "逃", + "障", + "氨", + "郭", + "峰", + "币", + "港", + "伏", + "轨", + "亩", + "毕", + "擦", + "莫", + "刺", + "浪", + "秘", + "援", + "株", + "健", + "售", + "股", + "岛", + "甘", + "泡", + "睡", + "童", + "铸", + "汤", + "阀", + "休", + "汇", + "舍", + "牧", + "绕", + "炸", + "哲", + "磷", + "绩", + "朋", + "淡", + "尖", + "启", + "陷", + "柴", + "呈", + "徒", + "颜", + "泪", + "稍", + "忘", + "泵", + "蓝", + "拖", + "洞", + "授", + "镜", + "辛", + "壮", + "锋", + "贫", + "虚", + "弯", + "摩", + "泰", + "幼", + "廷", + "尊", + "窗", + "纲", + "弄", + "隶", + "疑", + "氏", + "宫", + "姐", + "震", + "瑞", + "怪", + "尤", + "琴", + "循", + "描", + "膜", + "违", + "夹", + "腰", + "缘", + "珠", + "穷", + "森", + "枝", + "竹", + "沟", + "催", + "绳", + "忆", + "邦", + "剩", + "幸", + "浆", + "栏", + "拥", + "牙", + "贮", + "礼", + "滤", + "钠", + "纹", + "罢", + "拍", + "咱", + "喊", + "袖", + "埃", + "勤", + "罚", + "焦", + "潜", + "伍", + "墨", + "欲", + "缝", + "姓", + "刊", + "饱", + "仿", + "奖", + "铝", + "鬼", + "丽", + "跨", + "默", + "挖", + "链", + "扫", + "喝", + "袋", + "炭", + "污", + "幕", + "诸", + "弧", + "励", + "梅", + "奶", + "洁", + "灾", + "舟", + "鉴", + "苯", + "讼", + "抱", + "毁", + "懂", + "寒", + "智", + "埔", + "寄", + "届", + "跃", + "渡", + "挑", + "丹", + "艰", + "贝", + "碰", + "拔", + "爹", + "戴", + "码", + "梦", + "芽", + "熔", + "赤", + "渔", + "哭", + "敬", + "颗", + "奔", + "铅", + "仲", + "虎", + "稀", + "妹", + "乏", + "珍", + "申", + "桌", + "遵", + "允", + "隆", + "螺", + "仓", + "魏", + "锐", + "晓", + "氮", + "兼", + "隐", + "碍", + "赫", + "拨", + "忠", + "肃", + "缸", + "牵", + "抢", + "博", + "巧", + "壳", + "兄", + "杜", + "讯", + "诚", + "碧", + "祥", + "柯", + "页", + "巡", + "矩", + "悲", + "灌", + "龄", + "伦", + "票", + "寻", + "桂", + "铺", + "圣", + "恐", + "恰", + "郑", + "趣", + "抬", + "荒", + "腾", + "贴", + "柔", + "滴", + "猛", + "阔", + "辆", + "妻", + "填", + "撤", + "储", + "签", + "闹", + "扰", + "紫", + "砂", + "递", + "戏", + "吊", + "陶", + "伐", + "喂", + "疗", + "瓶", + "婆", + "抚", + "臂", + "摸", + "忍", + "虾", + "蜡", + "邻", + "胸", + "巩", + "挤", + "偶", + "弃", + "槽", + "劲", + "乳", + "邓", + "吉", + "仁", + "烂", + "砖", + "租", + "乌", + "舰", + "伴", + "瓜", + "浅", + "丙", + "暂", + "燥", + "橡", + "柳", + "迷", + "暖", + "牌", + "秧", + "胆", + "详", + "簧", + "踏", + "瓷", + "谱", + "呆", + "宾", + "糊", + "洛", + "辉", + "愤", + "竞", + "隙", + "怒", + "粘", + "乃", + "绪", + "肩", + "籍", + "敏", + "涂", + "熙", + "皆", + "侦", + "悬", + "掘", + "享", + "纠", + "醒", + "狂", + "锁", + "淀", + "恨", + "牲", + "霸", + "爬", + "赏", + "逆", + "玩", + "陵", + "祝", + "秒", + "浙", + "貌" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/dutch.dart b/cw_haven/lib/mnemonics/dutch.dart new file mode 100644 index 000000000..3a1d00cfc --- /dev/null +++ b/cw_haven/lib/mnemonics/dutch.dart @@ -0,0 +1,1630 @@ +class DutchMnemonics { + static const words = [ + "aalglad", + "aalscholver", + "aambeeld", + "aangeef", + "aanlandig", + "aanvaard", + "aanwakker", + "aapmens", + "aarten", + "abdicatie", + "abnormaal", + "abrikoos", + "accu", + "acuut", + "adjudant", + "admiraal", + "advies", + "afbidding", + "afdracht", + "affaire", + "affiche", + "afgang", + "afkick", + "afknap", + "aflees", + "afmijner", + "afname", + "afpreekt", + "afrader", + "afspeel", + "aftocht", + "aftrek", + "afzijdig", + "ahornboom", + "aktetas", + "akzo", + "alchemist", + "alcohol", + "aldaar", + "alexander", + "alfabet", + "alfredo", + "alice", + "alikruik", + "allrisk", + "altsax", + "alufolie", + "alziend", + "amai", + "ambacht", + "ambieer", + "amina", + "amnestie", + "amok", + "ampul", + "amuzikaal", + "angela", + "aniek", + "antje", + "antwerpen", + "anya", + "aorta", + "apache", + "apekool", + "appelaar", + "arganolie", + "argeloos", + "armoede", + "arrenslee", + "artritis", + "arubaan", + "asbak", + "ascii", + "asgrauw", + "asjes", + "asml", + "aspunt", + "asurn", + "asveld", + "aterling", + "atomair", + "atrium", + "atsma", + "atypisch", + "auping", + "aura", + "avifauna", + "axiaal", + "azoriaan", + "azteek", + "azuur", + "bachelor", + "badderen", + "badhotel", + "badmantel", + "badsteden", + "balie", + "ballans", + "balvers", + "bamibal", + "banneling", + "barracuda", + "basaal", + "batelaan", + "batje", + "beambte", + "bedlamp", + "bedwelmd", + "befaamd", + "begierd", + "begraaf", + "behield", + "beijaard", + "bejaagd", + "bekaaid", + "beks", + "bektas", + "belaad", + "belboei", + "belderbos", + "beloerd", + "beluchten", + "bemiddeld", + "benadeeld", + "benijd", + "berechten", + "beroemd", + "besef", + "besseling", + "best", + "betichten", + "bevind", + "bevochten", + "bevraagd", + "bewust", + "bidplaats", + "biefstuk", + "biemans", + "biezen", + "bijbaan", + "bijeenkom", + "bijfiguur", + "bijkaart", + "bijlage", + "bijpaard", + "bijtgaar", + "bijweg", + "bimmel", + "binck", + "bint", + "biobak", + "biotisch", + "biseks", + "bistro", + "bitter", + "bitumen", + "bizar", + "blad", + "bleken", + "blender", + "bleu", + "blief", + "blijven", + "blozen", + "bock", + "boef", + "boei", + "boks", + "bolder", + "bolus", + "bolvormig", + "bomaanval", + "bombarde", + "bomma", + "bomtapijt", + "bookmaker", + "boos", + "borg", + "bosbes", + "boshuizen", + "bosloop", + "botanicus", + "bougie", + "bovag", + "boxspring", + "braad", + "brasem", + "brevet", + "brigade", + "brinckman", + "bruid", + "budget", + "buffel", + "buks", + "bulgaar", + "buma", + "butaan", + "butler", + "buuf", + "cactus", + "cafeetje", + "camcorder", + "cannabis", + "canyon", + "capoeira", + "capsule", + "carkit", + "casanova", + "catalaan", + "ceintuur", + "celdeling", + "celplasma", + "cement", + "censeren", + "ceramisch", + "cerberus", + "cerebraal", + "cesium", + "cirkel", + "citeer", + "civiel", + "claxon", + "clenbuterol", + "clicheren", + "clijsen", + "coalitie", + "coassistentschap", + "coaxiaal", + "codetaal", + "cofinanciering", + "cognac", + "coltrui", + "comfort", + "commandant", + "condensaat", + "confectie", + "conifeer", + "convector", + "copier", + "corfu", + "correct", + "coup", + "couvert", + "creatie", + "credit", + "crematie", + "cricket", + "croupier", + "cruciaal", + "cruijff", + "cuisine", + "culemborg", + "culinair", + "curve", + "cyrano", + "dactylus", + "dading", + "dagblind", + "dagje", + "daglicht", + "dagprijs", + "dagranden", + "dakdekker", + "dakpark", + "dakterras", + "dalgrond", + "dambord", + "damkat", + "damlengte", + "damman", + "danenberg", + "debbie", + "decibel", + "defect", + "deformeer", + "degelijk", + "degradant", + "dejonghe", + "dekken", + "deppen", + "derek", + "derf", + "derhalve", + "detineren", + "devalueer", + "diaken", + "dicht", + "dictaat", + "dief", + "digitaal", + "dijbreuk", + "dijkmans", + "dimbaar", + "dinsdag", + "diode", + "dirigeer", + "disbalans", + "dobermann", + "doenbaar", + "doerak", + "dogma", + "dokhaven", + "dokwerker", + "doling", + "dolphijn", + "dolven", + "dombo", + "dooraderd", + "dopeling", + "doping", + "draderig", + "drama", + "drenkbak", + "dreumes", + "drol", + "drug", + "duaal", + "dublin", + "duplicaat", + "durven", + "dusdanig", + "dutchbat", + "dutje", + "dutten", + "duur", + "duwwerk", + "dwaal", + "dweil", + "dwing", + "dyslexie", + "ecostroom", + "ecotaks", + "educatie", + "eeckhout", + "eede", + "eemland", + "eencellig", + "eeneiig", + "eenruiter", + "eenwinter", + "eerenberg", + "eerrover", + "eersel", + "eetmaal", + "efteling", + "egaal", + "egtberts", + "eickhoff", + "eidooier", + "eiland", + "eind", + "eisden", + "ekster", + "elburg", + "elevatie", + "elfkoppig", + "elfrink", + "elftal", + "elimineer", + "elleboog", + "elma", + "elodie", + "elsa", + "embleem", + "embolie", + "emoe", + "emonds", + "emplooi", + "enduro", + "enfin", + "engageer", + "entourage", + "entstof", + "epileer", + "episch", + "eppo", + "erasmus", + "erboven", + "erebaan", + "erelijst", + "ereronden", + "ereteken", + "erfhuis", + "erfwet", + "erger", + "erica", + "ermitage", + "erna", + "ernie", + "erts", + "ertussen", + "eruitzien", + "ervaar", + "erven", + "erwt", + "esbeek", + "escort", + "esdoorn", + "essing", + "etage", + "eter", + "ethanol", + "ethicus", + "etholoog", + "eufonisch", + "eurocent", + "evacuatie", + "exact", + "examen", + "executant", + "exen", + "exit", + "exogeen", + "exotherm", + "expeditie", + "expletief", + "expres", + "extase", + "extinctie", + "faal", + "faam", + "fabel", + "facultair", + "fakir", + "fakkel", + "faliekant", + "fallisch", + "famke", + "fanclub", + "fase", + "fatsoen", + "fauna", + "federaal", + "feedback", + "feest", + "feilbaar", + "feitelijk", + "felblauw", + "figurante", + "fiod", + "fitheid", + "fixeer", + "flap", + "fleece", + "fleur", + "flexibel", + "flits", + "flos", + "flow", + "fluweel", + "foezelen", + "fokkelman", + "fokpaard", + "fokvee", + "folder", + "follikel", + "folmer", + "folteraar", + "fooi", + "foolen", + "forfait", + "forint", + "formule", + "fornuis", + "fosfaat", + "foxtrot", + "foyer", + "fragiel", + "frater", + "freak", + "freddie", + "fregat", + "freon", + "frijnen", + "fructose", + "frunniken", + "fuiven", + "funshop", + "furieus", + "fysica", + "gadget", + "galder", + "galei", + "galg", + "galvlieg", + "galzuur", + "ganesh", + "gaswet", + "gaza", + "gazelle", + "geaaid", + "gebiecht", + "gebufferd", + "gedijd", + "geef", + "geflanst", + "gefreesd", + "gegaan", + "gegijzeld", + "gegniffel", + "gegraaid", + "gehikt", + "gehobbeld", + "gehucht", + "geiser", + "geiten", + "gekaakt", + "gekheid", + "gekijf", + "gekmakend", + "gekocht", + "gekskap", + "gekte", + "gelubberd", + "gemiddeld", + "geordend", + "gepoederd", + "gepuft", + "gerda", + "gerijpt", + "geseald", + "geshockt", + "gesierd", + "geslaagd", + "gesnaaid", + "getracht", + "getwijfel", + "geuit", + "gevecht", + "gevlagd", + "gewicht", + "gezaagd", + "gezocht", + "ghanees", + "giebelen", + "giechel", + "giepmans", + "gips", + "giraal", + "gistachtig", + "gitaar", + "glaasje", + "gletsjer", + "gleuf", + "glibberen", + "glijbaan", + "gloren", + "gluipen", + "gluren", + "gluur", + "gnoe", + "goddelijk", + "godgans", + "godschalk", + "godzalig", + "goeierd", + "gogme", + "goklustig", + "gokwereld", + "gonggrijp", + "gonje", + "goor", + "grabbel", + "graf", + "graveer", + "grif", + "grolleman", + "grom", + "groosman", + "grubben", + "gruijs", + "grut", + "guacamole", + "guido", + "guppy", + "haazen", + "hachelijk", + "haex", + "haiku", + "hakhout", + "hakken", + "hanegem", + "hans", + "hanteer", + "harrie", + "hazebroek", + "hedonist", + "heil", + "heineken", + "hekhuis", + "hekman", + "helbig", + "helga", + "helwegen", + "hengelaar", + "herkansen", + "hermafrodiet", + "hertaald", + "hiaat", + "hikspoors", + "hitachi", + "hitparade", + "hobo", + "hoeve", + "holocaust", + "hond", + "honnepon", + "hoogacht", + "hotelbed", + "hufter", + "hugo", + "huilbier", + "hulk", + "humus", + "huwbaar", + "huwelijk", + "hype", + "iconisch", + "idema", + "ideogram", + "idolaat", + "ietje", + "ijker", + "ijkheid", + "ijklijn", + "ijkmaat", + "ijkwezen", + "ijmuiden", + "ijsbox", + "ijsdag", + "ijselijk", + "ijskoud", + "ilse", + "immuun", + "impliceer", + "impuls", + "inbijten", + "inbuigen", + "indijken", + "induceer", + "indy", + "infecteer", + "inhaak", + "inkijk", + "inluiden", + "inmijnen", + "inoefenen", + "inpolder", + "inrijden", + "inslaan", + "invitatie", + "inwaaien", + "ionisch", + "isaac", + "isolatie", + "isotherm", + "isra", + "italiaan", + "ivoor", + "jacobs", + "jakob", + "jammen", + "jampot", + "jarig", + "jehova", + "jenever", + "jezus", + "joana", + "jobdienst", + "josua", + "joule", + "juich", + "jurk", + "juut", + "kaas", + "kabelaar", + "kabinet", + "kagenaar", + "kajuit", + "kalebas", + "kalm", + "kanjer", + "kapucijn", + "karregat", + "kart", + "katvanger", + "katwijk", + "kegelaar", + "keiachtig", + "keizer", + "kenletter", + "kerdijk", + "keus", + "kevlar", + "kezen", + "kickback", + "kieviet", + "kijken", + "kikvors", + "kilheid", + "kilobit", + "kilsdonk", + "kipschnitzel", + "kissebis", + "klad", + "klagelijk", + "klak", + "klapbaar", + "klaver", + "klene", + "klets", + "klijnhout", + "klit", + "klok", + "klonen", + "klotefilm", + "kluif", + "klumper", + "klus", + "knabbel", + "knagen", + "knaven", + "kneedbaar", + "knmi", + "knul", + "knus", + "kokhals", + "komiek", + "komkommer", + "kompaan", + "komrij", + "komvormig", + "koning", + "kopbal", + "kopklep", + "kopnagel", + "koppejan", + "koptekst", + "kopwand", + "koraal", + "kosmisch", + "kostbaar", + "kram", + "kraneveld", + "kras", + "kreling", + "krengen", + "kribbe", + "krik", + "kruid", + "krulbol", + "kuijper", + "kuipbank", + "kuit", + "kuiven", + "kutsmoes", + "kuub", + "kwak", + "kwatong", + "kwetsbaar", + "kwezelaar", + "kwijnen", + "kwik", + "kwinkslag", + "kwitantie", + "lading", + "lakbeits", + "lakken", + "laklaag", + "lakmoes", + "lakwijk", + "lamheid", + "lamp", + "lamsbout", + "lapmiddel", + "larve", + "laser", + "latijn", + "latuw", + "lawaai", + "laxeerpil", + "lebberen", + "ledeboer", + "leefbaar", + "leeman", + "lefdoekje", + "lefhebber", + "legboor", + "legsel", + "leguaan", + "leiplaat", + "lekdicht", + "lekrijden", + "leksteen", + "lenen", + "leraar", + "lesbienne", + "leugenaar", + "leut", + "lexicaal", + "lezing", + "lieten", + "liggeld", + "lijdzaam", + "lijk", + "lijmstang", + "lijnschip", + "likdoorn", + "likken", + "liksteen", + "limburg", + "link", + "linoleum", + "lipbloem", + "lipman", + "lispelen", + "lissabon", + "litanie", + "liturgie", + "lochem", + "loempia", + "loesje", + "logheid", + "lonen", + "lonneke", + "loom", + "loos", + "losbaar", + "loslaten", + "losplaats", + "loting", + "lotnummer", + "lots", + "louie", + "lourdes", + "louter", + "lowbudget", + "luijten", + "luikenaar", + "luilak", + "luipaard", + "luizenbos", + "lulkoek", + "lumen", + "lunzen", + "lurven", + "lutjeboer", + "luttel", + "lutz", + "luuk", + "luwte", + "luyendijk", + "lyceum", + "lynx", + "maakbaar", + "magdalena", + "malheid", + "manchet", + "manfred", + "manhaftig", + "mank", + "mantel", + "marion", + "marxist", + "masmeijer", + "massaal", + "matsen", + "matverf", + "matze", + "maude", + "mayonaise", + "mechanica", + "meifeest", + "melodie", + "meppelink", + "midvoor", + "midweeks", + "midzomer", + "miezel", + "mijnraad", + "minus", + "mirck", + "mirte", + "mispakken", + "misraden", + "miswassen", + "mitella", + "moker", + "molecule", + "mombakkes", + "moonen", + "mopperaar", + "moraal", + "morgana", + "mormel", + "mosselaar", + "motregen", + "mouw", + "mufheid", + "mutueel", + "muzelman", + "naaidoos", + "naald", + "nadeel", + "nadruk", + "nagy", + "nahon", + "naima", + "nairobi", + "napalm", + "napels", + "napijn", + "napoleon", + "narigheid", + "narratief", + "naseizoen", + "nasibal", + "navigatie", + "nawijn", + "negatief", + "nekletsel", + "nekwervel", + "neolatijn", + "neonataal", + "neptunus", + "nerd", + "nest", + "neuzelaar", + "nihiliste", + "nijenhuis", + "nijging", + "nijhoff", + "nijl", + "nijptang", + "nippel", + "nokkenas", + "noordam", + "noren", + "normaal", + "nottelman", + "notulant", + "nout", + "nuance", + "nuchter", + "nudorp", + "nulde", + "nullijn", + "nulmeting", + "nunspeet", + "nylon", + "obelisk", + "object", + "oblie", + "obsceen", + "occlusie", + "oceaan", + "ochtend", + "ockhuizen", + "oerdom", + "oergezond", + "oerlaag", + "oester", + "okhuijsen", + "olifant", + "olijfboer", + "omaans", + "ombudsman", + "omdat", + "omdijken", + "omdoen", + "omgebouwd", + "omkeer", + "omkomen", + "ommegaand", + "ommuren", + "omroep", + "omruil", + "omslaan", + "omsmeden", + "omvaar", + "onaardig", + "onedel", + "onenig", + "onheilig", + "onrecht", + "onroerend", + "ontcijfer", + "onthaal", + "ontvallen", + "ontzadeld", + "onzacht", + "onzin", + "onzuiver", + "oogappel", + "ooibos", + "ooievaar", + "ooit", + "oorarts", + "oorhanger", + "oorijzer", + "oorklep", + "oorschelp", + "oorworm", + "oorzaak", + "opdagen", + "opdien", + "opdweilen", + "opel", + "opgebaard", + "opinie", + "opjutten", + "opkijken", + "opklaar", + "opkuisen", + "opkwam", + "opnaaien", + "opossum", + "opsieren", + "opsmeer", + "optreden", + "opvijzel", + "opvlammen", + "opwind", + "oraal", + "orchidee", + "orkest", + "ossuarium", + "ostendorf", + "oublie", + "oudachtig", + "oudbakken", + "oudnoors", + "oudshoorn", + "oudtante", + "oven", + "over", + "oxidant", + "pablo", + "pacht", + "paktafel", + "pakzadel", + "paljas", + "panharing", + "papfles", + "paprika", + "parochie", + "paus", + "pauze", + "paviljoen", + "peek", + "pegel", + "peigeren", + "pekela", + "pendant", + "penibel", + "pepmiddel", + "peptalk", + "periferie", + "perron", + "pessarium", + "peter", + "petfles", + "petgat", + "peuk", + "pfeifer", + "picknick", + "pief", + "pieneman", + "pijlkruid", + "pijnacker", + "pijpelink", + "pikdonker", + "pikeer", + "pilaar", + "pionier", + "pipet", + "piscine", + "pissebed", + "pitchen", + "pixel", + "plamuren", + "plan", + "plausibel", + "plegen", + "plempen", + "pleonasme", + "plezant", + "podoloog", + "pofmouw", + "pokdalig", + "ponywagen", + "popachtig", + "popidool", + "porren", + "positie", + "potten", + "pralen", + "prezen", + "prijzen", + "privaat", + "proef", + "prooi", + "prozawerk", + "pruik", + "prul", + "publiceer", + "puck", + "puilen", + "pukkelig", + "pulveren", + "pupil", + "puppy", + "purmerend", + "pustjens", + "putemmer", + "puzzelaar", + "queenie", + "quiche", + "raam", + "raar", + "raat", + "raes", + "ralf", + "rally", + "ramona", + "ramselaar", + "ranonkel", + "rapen", + "rapunzel", + "rarekiek", + "rarigheid", + "rattenhol", + "ravage", + "reactie", + "recreant", + "redacteur", + "redster", + "reewild", + "regie", + "reijnders", + "rein", + "replica", + "revanche", + "rigide", + "rijbaan", + "rijdansen", + "rijgen", + "rijkdom", + "rijles", + "rijnwijn", + "rijpma", + "rijstafel", + "rijtaak", + "rijzwepen", + "rioleer", + "ripdeal", + "riphagen", + "riskant", + "rits", + "rivaal", + "robbedoes", + "robot", + "rockact", + "rodijk", + "rogier", + "rohypnol", + "rollaag", + "rolpaal", + "roltafel", + "roof", + "roon", + "roppen", + "rosbief", + "rosharig", + "rosielle", + "rotan", + "rotleven", + "rotten", + "rotvaart", + "royaal", + "royeer", + "rubato", + "ruby", + "ruche", + "rudge", + "ruggetje", + "rugnummer", + "rugpijn", + "rugtitel", + "rugzak", + "ruilbaar", + "ruis", + "ruit", + "rukwind", + "rulijs", + "rumoeren", + "rumsdorp", + "rumtaart", + "runnen", + "russchen", + "ruwkruid", + "saboteer", + "saksisch", + "salade", + "salpeter", + "sambabal", + "samsam", + "satelliet", + "satineer", + "saus", + "scampi", + "scarabee", + "scenario", + "schobben", + "schubben", + "scout", + "secessie", + "secondair", + "seculair", + "sediment", + "seeland", + "settelen", + "setwinst", + "sheriff", + "shiatsu", + "siciliaan", + "sidderaal", + "sigma", + "sijben", + "silvana", + "simkaart", + "sinds", + "situatie", + "sjaak", + "sjardijn", + "sjezen", + "sjor", + "skinhead", + "skylab", + "slamixen", + "sleijpen", + "slijkerig", + "slordig", + "slowaak", + "sluieren", + "smadelijk", + "smiecht", + "smoel", + "smos", + "smukken", + "snackcar", + "snavel", + "sneaker", + "sneu", + "snijdbaar", + "snit", + "snorder", + "soapbox", + "soetekouw", + "soigneren", + "sojaboon", + "solo", + "solvabel", + "somber", + "sommatie", + "soort", + "soppen", + "sopraan", + "soundbar", + "spanen", + "spawater", + "spijgat", + "spinaal", + "spionage", + "spiraal", + "spleet", + "splijt", + "spoed", + "sporen", + "spul", + "spuug", + "spuw", + "stalen", + "standaard", + "star", + "stefan", + "stencil", + "stijf", + "stil", + "stip", + "stopdas", + "stoten", + "stoven", + "straat", + "strobbe", + "strubbel", + "stucadoor", + "stuif", + "stukadoor", + "subhoofd", + "subregent", + "sudoku", + "sukade", + "sulfaat", + "surinaams", + "suus", + "syfilis", + "symboliek", + "sympathie", + "synagoge", + "synchroon", + "synergie", + "systeem", + "taanderij", + "tabak", + "tachtig", + "tackelen", + "taiwanees", + "talman", + "tamheid", + "tangaslip", + "taps", + "tarkan", + "tarwe", + "tasman", + "tatjana", + "taxameter", + "teil", + "teisman", + "telbaar", + "telco", + "telganger", + "telstar", + "tenant", + "tepel", + "terzet", + "testament", + "ticket", + "tiesinga", + "tijdelijk", + "tika", + "tiksel", + "tilleman", + "timbaal", + "tinsteen", + "tiplijn", + "tippelaar", + "tjirpen", + "toezeggen", + "tolbaas", + "tolgeld", + "tolhek", + "tolo", + "tolpoort", + "toltarief", + "tolvrij", + "tomaat", + "tondeuse", + "toog", + "tooi", + "toonbaar", + "toos", + "topclub", + "toppen", + "toptalent", + "topvrouw", + "toque", + "torment", + "tornado", + "tosti", + "totdat", + "toucheer", + "toulouse", + "tournedos", + "tout", + "trabant", + "tragedie", + "trailer", + "traject", + "traktaat", + "trauma", + "tray", + "trechter", + "tred", + "tref", + "treur", + "troebel", + "tros", + "trucage", + "truffel", + "tsaar", + "tucht", + "tuenter", + "tuitelig", + "tukje", + "tuktuk", + "tulp", + "tuma", + "tureluurs", + "twijfel", + "twitteren", + "tyfoon", + "typograaf", + "ugandees", + "uiachtig", + "uier", + "uisnipper", + "ultiem", + "unitair", + "uranium", + "urbaan", + "urendag", + "ursula", + "uurcirkel", + "uurglas", + "uzelf", + "vaat", + "vakantie", + "vakleraar", + "valbijl", + "valpartij", + "valreep", + "valuatie", + "vanmiddag", + "vanonder", + "varaan", + "varken", + "vaten", + "veenbes", + "veeteler", + "velgrem", + "vellekoop", + "velvet", + "veneberg", + "venlo", + "vent", + "venusberg", + "venw", + "veredeld", + "verf", + "verhaaf", + "vermaak", + "vernaaid", + "verraad", + "vers", + "veruit", + "verzaagd", + "vetachtig", + "vetlok", + "vetmesten", + "veto", + "vetrek", + "vetstaart", + "vetten", + "veurink", + "viaduct", + "vibrafoon", + "vicariaat", + "vieux", + "vieveen", + "vijfvoud", + "villa", + "vilt", + "vimmetje", + "vindbaar", + "vips", + "virtueel", + "visdieven", + "visee", + "visie", + "vlaag", + "vleugel", + "vmbo", + "vocht", + "voesenek", + "voicemail", + "voip", + "volg", + "vork", + "vorselaar", + "voyeur", + "vracht", + "vrekkig", + "vreten", + "vrije", + "vrozen", + "vrucht", + "vucht", + "vugt", + "vulkaan", + "vulmiddel", + "vulva", + "vuren", + "waas", + "wacht", + "wadvogel", + "wafel", + "waffel", + "walhalla", + "walnoot", + "walraven", + "wals", + "walvis", + "wandaad", + "wanen", + "wanmolen", + "want", + "warklomp", + "warm", + "wasachtig", + "wasteil", + "watt", + "webhandel", + "weblog", + "webpagina", + "webzine", + "wedereis", + "wedstrijd", + "weeda", + "weert", + "wegmaaien", + "wegscheer", + "wekelijks", + "wekken", + "wekroep", + "wektoon", + "weldaad", + "welwater", + "wendbaar", + "wenkbrauw", + "wens", + "wentelaar", + "wervel", + "wesseling", + "wetboek", + "wetmatig", + "whirlpool", + "wijbrands", + "wijdbeens", + "wijk", + "wijnbes", + "wijting", + "wild", + "wimpelen", + "wingebied", + "winplaats", + "winter", + "winzucht", + "wipstaart", + "wisgerhof", + "withaar", + "witmaker", + "wokkel", + "wolf", + "wonenden", + "woning", + "worden", + "worp", + "wortel", + "wrat", + "wrijf", + "wringen", + "yoghurt", + "ypsilon", + "zaaijer", + "zaak", + "zacharias", + "zakelijk", + "zakkam", + "zakwater", + "zalf", + "zalig", + "zaniken", + "zebracode", + "zeeblauw", + "zeef", + "zeegaand", + "zeeuw", + "zege", + "zegje", + "zeil", + "zesbaans", + "zesenhalf", + "zeskantig", + "zesmaal", + "zetbaas", + "zetpil", + "zeulen", + "ziezo", + "zigzag", + "zijaltaar", + "zijbeuk", + "zijlijn", + "zijmuur", + "zijn", + "zijwaarts", + "zijzelf", + "zilt", + "zimmerman", + "zinledig", + "zinnelijk", + "zionist", + "zitdag", + "zitruimte", + "zitzak", + "zoal", + "zodoende", + "zoekbots", + "zoem", + "zoiets", + "zojuist", + "zondaar", + "zotskap", + "zottebol", + "zucht", + "zuivel", + "zulk", + "zult", + "zuster", + "zuur", + "zweedijk", + "zwendel", + "zwepen", + "zwiep", + "zwijmel", + "zworen" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/english.dart b/cw_haven/lib/mnemonics/english.dart new file mode 100644 index 000000000..fb464d04e --- /dev/null +++ b/cw_haven/lib/mnemonics/english.dart @@ -0,0 +1,1630 @@ +class EnglishMnemonics { + static const words = [ + "abbey", + "abducts", + "ability", + "ablaze", + "abnormal", + "abort", + "abrasive", + "absorb", + "abyss", + "academy", + "aces", + "aching", + "acidic", + "acoustic", + "acquire", + "across", + "actress", + "acumen", + "adapt", + "addicted", + "adept", + "adhesive", + "adjust", + "adopt", + "adrenalin", + "adult", + "adventure", + "aerial", + "afar", + "affair", + "afield", + "afloat", + "afoot", + "afraid", + "after", + "against", + "agenda", + "aggravate", + "agile", + "aglow", + "agnostic", + "agony", + "agreed", + "ahead", + "aided", + "ailments", + "aimless", + "airport", + "aisle", + "ajar", + "akin", + "alarms", + "album", + "alchemy", + "alerts", + "algebra", + "alkaline", + "alley", + "almost", + "aloof", + "alpine", + "already", + "also", + "altitude", + "alumni", + "always", + "amaze", + "ambush", + "amended", + "amidst", + "ammo", + "amnesty", + "among", + "amply", + "amused", + "anchor", + "android", + "anecdote", + "angled", + "ankle", + "annoyed", + "answers", + "antics", + "anvil", + "anxiety", + "anybody", + "apart", + "apex", + "aphid", + "aplomb", + "apology", + "apply", + "apricot", + "aptitude", + "aquarium", + "arbitrary", + "archer", + "ardent", + "arena", + "argue", + "arises", + "army", + "around", + "arrow", + "arsenic", + "artistic", + "ascend", + "ashtray", + "aside", + "asked", + "asleep", + "aspire", + "assorted", + "asylum", + "athlete", + "atlas", + "atom", + "atrium", + "attire", + "auburn", + "auctions", + "audio", + "august", + "aunt", + "austere", + "autumn", + "avatar", + "avidly", + "avoid", + "awakened", + "awesome", + "awful", + "awkward", + "awning", + "awoken", + "axes", + "axis", + "axle", + "aztec", + "azure", + "baby", + "bacon", + "badge", + "baffles", + "bagpipe", + "bailed", + "bakery", + "balding", + "bamboo", + "banjo", + "baptism", + "basin", + "batch", + "bawled", + "bays", + "because", + "beer", + "befit", + "begun", + "behind", + "being", + "below", + "bemused", + "benches", + "berries", + "bested", + "betting", + "bevel", + "beware", + "beyond", + "bias", + "bicycle", + "bids", + "bifocals", + "biggest", + "bikini", + "bimonthly", + "binocular", + "biology", + "biplane", + "birth", + "biscuit", + "bite", + "biweekly", + "blender", + "blip", + "bluntly", + "boat", + "bobsled", + "bodies", + "bogeys", + "boil", + "boldly", + "bomb", + "border", + "boss", + "both", + "bounced", + "bovine", + "bowling", + "boxes", + "boyfriend", + "broken", + "brunt", + "bubble", + "buckets", + "budget", + "buffet", + "bugs", + "building", + "bulb", + "bumper", + "bunch", + "business", + "butter", + "buying", + "buzzer", + "bygones", + "byline", + "bypass", + "cabin", + "cactus", + "cadets", + "cafe", + "cage", + "cajun", + "cake", + "calamity", + "camp", + "candy", + "casket", + "catch", + "cause", + "cavernous", + "cease", + "cedar", + "ceiling", + "cell", + "cement", + "cent", + "certain", + "chlorine", + "chrome", + "cider", + "cigar", + "cinema", + "circle", + "cistern", + "citadel", + "civilian", + "claim", + "click", + "clue", + "coal", + "cobra", + "cocoa", + "code", + "coexist", + "coffee", + "cogs", + "cohesive", + "coils", + "colony", + "comb", + "cool", + "copy", + "corrode", + "costume", + "cottage", + "cousin", + "cowl", + "criminal", + "cube", + "cucumber", + "cuddled", + "cuffs", + "cuisine", + "cunning", + "cupcake", + "custom", + "cycling", + "cylinder", + "cynical", + "dabbing", + "dads", + "daft", + "dagger", + "daily", + "damp", + "dangerous", + "dapper", + "darted", + "dash", + "dating", + "dauntless", + "dawn", + "daytime", + "dazed", + "debut", + "decay", + "dedicated", + "deepest", + "deftly", + "degrees", + "dehydrate", + "deity", + "dejected", + "delayed", + "demonstrate", + "dented", + "deodorant", + "depth", + "desk", + "devoid", + "dewdrop", + "dexterity", + "dialect", + "dice", + "diet", + "different", + "digit", + "dilute", + "dime", + "dinner", + "diode", + "diplomat", + "directed", + "distance", + "ditch", + "divers", + "dizzy", + "doctor", + "dodge", + "does", + "dogs", + "doing", + "dolphin", + "domestic", + "donuts", + "doorway", + "dormant", + "dosage", + "dotted", + "double", + "dove", + "down", + "dozen", + "dreams", + "drinks", + "drowning", + "drunk", + "drying", + "dual", + "dubbed", + "duckling", + "dude", + "duets", + "duke", + "dullness", + "dummy", + "dunes", + "duplex", + "duration", + "dusted", + "duties", + "dwarf", + "dwelt", + "dwindling", + "dying", + "dynamite", + "dyslexic", + "each", + "eagle", + "earth", + "easy", + "eating", + "eavesdrop", + "eccentric", + "echo", + "eclipse", + "economics", + "ecstatic", + "eden", + "edgy", + "edited", + "educated", + "eels", + "efficient", + "eggs", + "egotistic", + "eight", + "either", + "eject", + "elapse", + "elbow", + "eldest", + "eleven", + "elite", + "elope", + "else", + "eluded", + "emails", + "ember", + "emerge", + "emit", + "emotion", + "empty", + "emulate", + "energy", + "enforce", + "enhanced", + "enigma", + "enjoy", + "enlist", + "enmity", + "enough", + "enraged", + "ensign", + "entrance", + "envy", + "epoxy", + "equip", + "erase", + "erected", + "erosion", + "error", + "eskimos", + "espionage", + "essential", + "estate", + "etched", + "eternal", + "ethics", + "etiquette", + "evaluate", + "evenings", + "evicted", + "evolved", + "examine", + "excess", + "exhale", + "exit", + "exotic", + "exquisite", + "extra", + "exult", + "fabrics", + "factual", + "fading", + "fainted", + "faked", + "fall", + "family", + "fancy", + "farming", + "fatal", + "faulty", + "fawns", + "faxed", + "fazed", + "feast", + "february", + "federal", + "feel", + "feline", + "females", + "fences", + "ferry", + "festival", + "fetches", + "fever", + "fewest", + "fiat", + "fibula", + "fictional", + "fidget", + "fierce", + "fifteen", + "fight", + "films", + "firm", + "fishing", + "fitting", + "five", + "fixate", + "fizzle", + "fleet", + "flippant", + "flying", + "foamy", + "focus", + "foes", + "foggy", + "foiled", + "folding", + "fonts", + "foolish", + "fossil", + "fountain", + "fowls", + "foxes", + "foyer", + "framed", + "friendly", + "frown", + "fruit", + "frying", + "fudge", + "fuel", + "fugitive", + "fully", + "fuming", + "fungal", + "furnished", + "fuselage", + "future", + "fuzzy", + "gables", + "gadget", + "gags", + "gained", + "galaxy", + "gambit", + "gang", + "gasp", + "gather", + "gauze", + "gave", + "gawk", + "gaze", + "gearbox", + "gecko", + "geek", + "gels", + "gemstone", + "general", + "geometry", + "germs", + "gesture", + "getting", + "geyser", + "ghetto", + "ghost", + "giant", + "giddy", + "gifts", + "gigantic", + "gills", + "gimmick", + "ginger", + "girth", + "giving", + "glass", + "gleeful", + "glide", + "gnaw", + "gnome", + "goat", + "goblet", + "godfather", + "goes", + "goggles", + "going", + "goldfish", + "gone", + "goodbye", + "gopher", + "gorilla", + "gossip", + "gotten", + "gourmet", + "governing", + "gown", + "greater", + "grunt", + "guarded", + "guest", + "guide", + "gulp", + "gumball", + "guru", + "gusts", + "gutter", + "guys", + "gymnast", + "gypsy", + "gyrate", + "habitat", + "hacksaw", + "haggled", + "hairy", + "hamburger", + "happens", + "hashing", + "hatchet", + "haunted", + "having", + "hawk", + "haystack", + "hazard", + "hectare", + "hedgehog", + "heels", + "hefty", + "height", + "hemlock", + "hence", + "heron", + "hesitate", + "hexagon", + "hickory", + "hiding", + "highway", + "hijack", + "hiker", + "hills", + "himself", + "hinder", + "hippo", + "hire", + "history", + "hitched", + "hive", + "hoax", + "hobby", + "hockey", + "hoisting", + "hold", + "honked", + "hookup", + "hope", + "hornet", + "hospital", + "hotel", + "hounded", + "hover", + "howls", + "hubcaps", + "huddle", + "huge", + "hull", + "humid", + "hunter", + "hurried", + "husband", + "huts", + "hybrid", + "hydrogen", + "hyper", + "iceberg", + "icing", + "icon", + "identity", + "idiom", + "idled", + "idols", + "igloo", + "ignore", + "iguana", + "illness", + "imagine", + "imbalance", + "imitate", + "impel", + "inactive", + "inbound", + "incur", + "industrial", + "inexact", + "inflamed", + "ingested", + "initiate", + "injury", + "inkling", + "inline", + "inmate", + "innocent", + "inorganic", + "input", + "inquest", + "inroads", + "insult", + "intended", + "inundate", + "invoke", + "inwardly", + "ionic", + "irate", + "iris", + "irony", + "irritate", + "island", + "isolated", + "issued", + "italics", + "itches", + "items", + "itinerary", + "itself", + "ivory", + "jabbed", + "jackets", + "jaded", + "jagged", + "jailed", + "jamming", + "january", + "jargon", + "jaunt", + "javelin", + "jaws", + "jazz", + "jeans", + "jeers", + "jellyfish", + "jeopardy", + "jerseys", + "jester", + "jetting", + "jewels", + "jigsaw", + "jingle", + "jittery", + "jive", + "jobs", + "jockey", + "jogger", + "joining", + "joking", + "jolted", + "jostle", + "journal", + "joyous", + "jubilee", + "judge", + "juggled", + "juicy", + "jukebox", + "july", + "jump", + "junk", + "jury", + "justice", + "juvenile", + "kangaroo", + "karate", + "keep", + "kennel", + "kept", + "kernels", + "kettle", + "keyboard", + "kickoff", + "kidneys", + "king", + "kiosk", + "kisses", + "kitchens", + "kiwi", + "knapsack", + "knee", + "knife", + "knowledge", + "knuckle", + "koala", + "laboratory", + "ladder", + "lagoon", + "lair", + "lakes", + "lamb", + "language", + "laptop", + "large", + "last", + "later", + "launching", + "lava", + "lawsuit", + "layout", + "lazy", + "lectures", + "ledge", + "leech", + "left", + "legion", + "leisure", + "lemon", + "lending", + "leopard", + "lesson", + "lettuce", + "lexicon", + "liar", + "library", + "licks", + "lids", + "lied", + "lifestyle", + "light", + "likewise", + "lilac", + "limits", + "linen", + "lion", + "lipstick", + "liquid", + "listen", + "lively", + "loaded", + "lobster", + "locker", + "lodge", + "lofty", + "logic", + "loincloth", + "long", + "looking", + "lopped", + "lordship", + "losing", + "lottery", + "loudly", + "love", + "lower", + "loyal", + "lucky", + "luggage", + "lukewarm", + "lullaby", + "lumber", + "lunar", + "lurk", + "lush", + "luxury", + "lymph", + "lynx", + "lyrics", + "macro", + "madness", + "magically", + "mailed", + "major", + "makeup", + "malady", + "mammal", + "maps", + "masterful", + "match", + "maul", + "maverick", + "maximum", + "mayor", + "maze", + "meant", + "mechanic", + "medicate", + "meeting", + "megabyte", + "melting", + "memoir", + "menu", + "merger", + "mesh", + "metro", + "mews", + "mice", + "midst", + "mighty", + "mime", + "mirror", + "misery", + "mittens", + "mixture", + "moat", + "mobile", + "mocked", + "mohawk", + "moisture", + "molten", + "moment", + "money", + "moon", + "mops", + "morsel", + "mostly", + "motherly", + "mouth", + "movement", + "mowing", + "much", + "muddy", + "muffin", + "mugged", + "mullet", + "mumble", + "mundane", + "muppet", + "mural", + "musical", + "muzzle", + "myriad", + "mystery", + "myth", + "nabbing", + "nagged", + "nail", + "names", + "nanny", + "napkin", + "narrate", + "nasty", + "natural", + "nautical", + "navy", + "nearby", + "necklace", + "needed", + "negative", + "neither", + "neon", + "nephew", + "nerves", + "nestle", + "network", + "neutral", + "never", + "newt", + "nexus", + "nibs", + "niche", + "niece", + "nifty", + "nightly", + "nimbly", + "nineteen", + "nirvana", + "nitrogen", + "nobody", + "nocturnal", + "nodes", + "noises", + "nomad", + "noodles", + "northern", + "nostril", + "noted", + "nouns", + "novelty", + "nowhere", + "nozzle", + "nuance", + "nucleus", + "nudged", + "nugget", + "nuisance", + "null", + "number", + "nuns", + "nurse", + "nutshell", + "nylon", + "oaks", + "oars", + "oasis", + "oatmeal", + "obedient", + "object", + "obliged", + "obnoxious", + "observant", + "obtains", + "obvious", + "occur", + "ocean", + "october", + "odds", + "odometer", + "offend", + "often", + "oilfield", + "ointment", + "okay", + "older", + "olive", + "olympics", + "omega", + "omission", + "omnibus", + "onboard", + "oncoming", + "oneself", + "ongoing", + "onion", + "online", + "onslaught", + "onto", + "onward", + "oozed", + "opacity", + "opened", + "opposite", + "optical", + "opus", + "orange", + "orbit", + "orchid", + "orders", + "organs", + "origin", + "ornament", + "orphans", + "oscar", + "ostrich", + "otherwise", + "otter", + "ouch", + "ought", + "ounce", + "ourselves", + "oust", + "outbreak", + "oval", + "oven", + "owed", + "owls", + "owner", + "oxidant", + "oxygen", + "oyster", + "ozone", + "pact", + "paddles", + "pager", + "pairing", + "palace", + "pamphlet", + "pancakes", + "paper", + "paradise", + "pastry", + "patio", + "pause", + "pavements", + "pawnshop", + "payment", + "peaches", + "pebbles", + "peculiar", + "pedantic", + "peeled", + "pegs", + "pelican", + "pencil", + "people", + "pepper", + "perfect", + "pests", + "petals", + "phase", + "pheasants", + "phone", + "phrases", + "physics", + "piano", + "picked", + "pierce", + "pigment", + "piloted", + "pimple", + "pinched", + "pioneer", + "pipeline", + "pirate", + "pistons", + "pitched", + "pivot", + "pixels", + "pizza", + "playful", + "pledge", + "pliers", + "plotting", + "plus", + "plywood", + "poaching", + "pockets", + "podcast", + "poetry", + "point", + "poker", + "polar", + "ponies", + "pool", + "popular", + "portents", + "possible", + "potato", + "pouch", + "poverty", + "powder", + "pram", + "present", + "pride", + "problems", + "pruned", + "prying", + "psychic", + "public", + "puck", + "puddle", + "puffin", + "pulp", + "pumpkins", + "punch", + "puppy", + "purged", + "push", + "putty", + "puzzled", + "pylons", + "pyramid", + "python", + "queen", + "quick", + "quote", + "rabbits", + "racetrack", + "radar", + "rafts", + "rage", + "railway", + "raking", + "rally", + "ramped", + "randomly", + "rapid", + "rarest", + "rash", + "rated", + "ravine", + "rays", + "razor", + "react", + "rebel", + "recipe", + "reduce", + "reef", + "refer", + "regular", + "reheat", + "reinvest", + "rejoices", + "rekindle", + "relic", + "remedy", + "renting", + "reorder", + "repent", + "request", + "reruns", + "rest", + "return", + "reunion", + "revamp", + "rewind", + "rhino", + "rhythm", + "ribbon", + "richly", + "ridges", + "rift", + "rigid", + "rims", + "ringing", + "riots", + "ripped", + "rising", + "ritual", + "river", + "roared", + "robot", + "rockets", + "rodent", + "rogue", + "roles", + "romance", + "roomy", + "roped", + "roster", + "rotate", + "rounded", + "rover", + "rowboat", + "royal", + "ruby", + "rudely", + "ruffled", + "rugged", + "ruined", + "ruling", + "rumble", + "runway", + "rural", + "rustled", + "ruthless", + "sabotage", + "sack", + "sadness", + "safety", + "saga", + "sailor", + "sake", + "salads", + "sample", + "sanity", + "sapling", + "sarcasm", + "sash", + "satin", + "saucepan", + "saved", + "sawmill", + "saxophone", + "sayings", + "scamper", + "scenic", + "school", + "science", + "scoop", + "scrub", + "scuba", + "seasons", + "second", + "sedan", + "seeded", + "segments", + "seismic", + "selfish", + "semifinal", + "sensible", + "september", + "sequence", + "serving", + "session", + "setup", + "seventh", + "sewage", + "shackles", + "shelter", + "shipped", + "shocking", + "shrugged", + "shuffled", + "shyness", + "siblings", + "sickness", + "sidekick", + "sieve", + "sifting", + "sighting", + "silk", + "simplest", + "sincerely", + "sipped", + "siren", + "situated", + "sixteen", + "sizes", + "skater", + "skew", + "skirting", + "skulls", + "skydive", + "slackens", + "sleepless", + "slid", + "slower", + "slug", + "smash", + "smelting", + "smidgen", + "smog", + "smuggled", + "snake", + "sneeze", + "sniff", + "snout", + "snug", + "soapy", + "sober", + "soccer", + "soda", + "software", + "soggy", + "soil", + "solved", + "somewhere", + "sonic", + "soothe", + "soprano", + "sorry", + "southern", + "sovereign", + "sowed", + "soya", + "space", + "speedy", + "sphere", + "spiders", + "splendid", + "spout", + "sprig", + "spud", + "spying", + "square", + "stacking", + "stellar", + "stick", + "stockpile", + "strained", + "stunning", + "stylishly", + "subtly", + "succeed", + "suddenly", + "suede", + "suffice", + "sugar", + "suitcase", + "sulking", + "summon", + "sunken", + "superior", + "surfer", + "sushi", + "suture", + "swagger", + "swept", + "swiftly", + "sword", + "swung", + "syllabus", + "symptoms", + "syndrome", + "syringe", + "system", + "taboo", + "tacit", + "tadpoles", + "tagged", + "tail", + "taken", + "talent", + "tamper", + "tanks", + "tapestry", + "tarnished", + "tasked", + "tattoo", + "taunts", + "tavern", + "tawny", + "taxi", + "teardrop", + "technical", + "tedious", + "teeming", + "tell", + "template", + "tender", + "tepid", + "tequila", + "terminal", + "testing", + "tether", + "textbook", + "thaw", + "theatrics", + "thirsty", + "thorn", + "threaten", + "thumbs", + "thwart", + "ticket", + "tidy", + "tiers", + "tiger", + "tilt", + "timber", + "tinted", + "tipsy", + "tirade", + "tissue", + "titans", + "toaster", + "tobacco", + "today", + "toenail", + "toffee", + "together", + "toilet", + "token", + "tolerant", + "tomorrow", + "tonic", + "toolbox", + "topic", + "torch", + "tossed", + "total", + "touchy", + "towel", + "toxic", + "toyed", + "trash", + "trendy", + "tribal", + "trolling", + "truth", + "trying", + "tsunami", + "tubes", + "tucks", + "tudor", + "tuesday", + "tufts", + "tugs", + "tuition", + "tulips", + "tumbling", + "tunnel", + "turnip", + "tusks", + "tutor", + "tuxedo", + "twang", + "tweezers", + "twice", + "twofold", + "tycoon", + "typist", + "tyrant", + "ugly", + "ulcers", + "ultimate", + "umbrella", + "umpire", + "unafraid", + "unbending", + "uncle", + "under", + "uneven", + "unfit", + "ungainly", + "unhappy", + "union", + "unjustly", + "unknown", + "unlikely", + "unmask", + "unnoticed", + "unopened", + "unplugs", + "unquoted", + "unrest", + "unsafe", + "until", + "unusual", + "unveil", + "unwind", + "unzip", + "upbeat", + "upcoming", + "update", + "upgrade", + "uphill", + "upkeep", + "upload", + "upon", + "upper", + "upright", + "upstairs", + "uptight", + "upwards", + "urban", + "urchins", + "urgent", + "usage", + "useful", + "usher", + "using", + "usual", + "utensils", + "utility", + "utmost", + "utopia", + "uttered", + "vacation", + "vague", + "vain", + "value", + "vampire", + "vane", + "vapidly", + "vary", + "vastness", + "vats", + "vaults", + "vector", + "veered", + "vegan", + "vehicle", + "vein", + "velvet", + "venomous", + "verification", + "vessel", + "veteran", + "vexed", + "vials", + "vibrate", + "victim", + "video", + "viewpoint", + "vigilant", + "viking", + "village", + "vinegar", + "violin", + "vipers", + "virtual", + "visited", + "vitals", + "vivid", + "vixen", + "vocal", + "vogue", + "voice", + "volcano", + "vortex", + "voted", + "voucher", + "vowels", + "voyage", + "vulture", + "wade", + "waffle", + "wagtail", + "waist", + "waking", + "wallets", + "wanted", + "warped", + "washing", + "water", + "waveform", + "waxing", + "wayside", + "weavers", + "website", + "wedge", + "weekday", + "weird", + "welders", + "went", + "wept", + "were", + "western", + "wetsuit", + "whale", + "when", + "whipped", + "whole", + "wickets", + "width", + "wield", + "wife", + "wiggle", + "wildly", + "winter", + "wipeout", + "wiring", + "wise", + "withdrawn", + "wives", + "wizard", + "wobbly", + "woes", + "woken", + "wolf", + "womanly", + "wonders", + "woozy", + "worry", + "wounded", + "woven", + "wrap", + "wrist", + "wrong", + "yacht", + "yahoo", + "yanks", + "yard", + "yawning", + "yearbook", + "yellow", + "yesterday", + "yeti", + "yields", + "yodel", + "yoga", + "younger", + "yoyo", + "zapped", + "zeal", + "zebra", + "zero", + "zesty", + "zigzags", + "zinger", + "zippers", + "zodiac", + "zombie", + "zones", + "zoom" + ]; +} diff --git a/cw_haven/lib/mnemonics/french.dart b/cw_haven/lib/mnemonics/french.dart new file mode 100644 index 000000000..76d556f6a --- /dev/null +++ b/cw_haven/lib/mnemonics/french.dart @@ -0,0 +1,1630 @@ +class FrenchMnemonics { + static const words = [ + "abandon", + "abattre", + "aboi", + "abolir", + "aborder", + "abri", + "absence", + "absolu", + "abuser", + "acacia", + "acajou", + "accent", + "accord", + "accrocher", + "accuser", + "acerbe", + "achat", + "acheter", + "acide", + "acier", + "acquis", + "acte", + "action", + "adage", + "adepte", + "adieu", + "admettre", + "admis", + "adorer", + "adresser", + "aduler", + "affaire", + "affirmer", + "afin", + "agacer", + "agent", + "agir", + "agiter", + "agonie", + "agrafe", + "agrume", + "aider", + "aigle", + "aigre", + "aile", + "ailleurs", + "aimant", + "aimer", + "ainsi", + "aise", + "ajouter", + "alarme", + "album", + "alcool", + "alerte", + "algue", + "alibi", + "aller", + "allumer", + "alors", + "amande", + "amener", + "amie", + "amorcer", + "amour", + "ample", + "amuser", + "ananas", + "ancien", + "anglais", + "angoisse", + "animal", + "anneau", + "annoncer", + "apercevoir", + "apparence", + "appel", + "apporter", + "apprendre", + "appuyer", + "arbre", + "arcade", + "arceau", + "arche", + "ardeur", + "argent", + "argile", + "aride", + "arme", + "armure", + "arracher", + "arriver", + "article", + "asile", + "aspect", + "assaut", + "assez", + "assister", + "assurer", + "astre", + "astuce", + "atlas", + "atroce", + "attacher", + "attente", + "attirer", + "aube", + "aucun", + "audace", + "auparavant", + "auquel", + "aurore", + "aussi", + "autant", + "auteur", + "autoroute", + "autre", + "aval", + "avant", + "avec", + "avenir", + "averse", + "aveu", + "avide", + "avion", + "avis", + "avoir", + "avouer", + "avril", + "azote", + "azur", + "badge", + "bagage", + "bague", + "bain", + "baisser", + "balai", + "balcon", + "balise", + "balle", + "bambou", + "banane", + "banc", + "bandage", + "banjo", + "banlieue", + "bannir", + "banque", + "baobab", + "barbe", + "barque", + "barrer", + "bassine", + "bataille", + "bateau", + "battre", + "baver", + "bavoir", + "bazar", + "beau", + "beige", + "berger", + "besoin", + "beurre", + "biais", + "biceps", + "bidule", + "bien", + "bijou", + "bilan", + "billet", + "blanc", + "blason", + "bleu", + "bloc", + "blond", + "bocal", + "boire", + "boiserie", + "boiter", + "bonbon", + "bondir", + "bonheur", + "bordure", + "borgne", + "borner", + "bosse", + "bouche", + "bouder", + "bouger", + "boule", + "bourse", + "bout", + "boxe", + "brader", + "braise", + "branche", + "braquer", + "bras", + "brave", + "brebis", + "brevet", + "brider", + "briller", + "brin", + "brique", + "briser", + "broche", + "broder", + "bronze", + "brosser", + "brouter", + "bruit", + "brute", + "budget", + "buffet", + "bulle", + "bureau", + "buriner", + "buste", + "buter", + "butiner", + "cabas", + "cabinet", + "cabri", + "cacao", + "cacher", + "cadeau", + "cadre", + "cage", + "caisse", + "caler", + "calme", + "camarade", + "camion", + "campagne", + "canal", + "canif", + "capable", + "capot", + "carat", + "caresser", + "carie", + "carpe", + "cartel", + "casier", + "casque", + "casserole", + "cause", + "cavale", + "cave", + "ceci", + "cela", + "celui", + "cendre", + "cent", + "cependant", + "cercle", + "cerise", + "cerner", + "certes", + "cerveau", + "cesser", + "chacun", + "chair", + "chaleur", + "chamois", + "chanson", + "chaque", + "charge", + "chasse", + "chat", + "chaud", + "chef", + "chemin", + "cheveu", + "chez", + "chicane", + "chien", + "chiffre", + "chiner", + "chiot", + "chlore", + "choc", + "choix", + "chose", + "chou", + "chute", + "cibler", + "cidre", + "ciel", + "cigale", + "cinq", + "cintre", + "cirage", + "cirque", + "ciseau", + "citation", + "citer", + "citron", + "civet", + "clairon", + "clan", + "classe", + "clavier", + "clef", + "climat", + "cloche", + "cloner", + "clore", + "clos", + "clou", + "club", + "cobra", + "cocon", + "coiffer", + "coin", + "colline", + "colon", + "combat", + "comme", + "compte", + "conclure", + "conduire", + "confier", + "connu", + "conseil", + "contre", + "convenir", + "copier", + "cordial", + "cornet", + "corps", + "cosmos", + "coton", + "couche", + "coude", + "couler", + "coupure", + "cour", + "couteau", + "couvrir", + "crabe", + "crainte", + "crampe", + "cran", + "creuser", + "crever", + "crier", + "crime", + "crin", + "crise", + "crochet", + "croix", + "cruel", + "cuisine", + "cuite", + "culot", + "culte", + "cumul", + "cure", + "curieux", + "cuve", + "dame", + "danger", + "dans", + "davantage", + "debout", + "dedans", + "dehors", + "delta", + "demain", + "demeurer", + "demi", + "dense", + "dent", + "depuis", + "dernier", + "descendre", + "dessus", + "destin", + "dette", + "deuil", + "deux", + "devant", + "devenir", + "devin", + "devoir", + "dicton", + "dieu", + "difficile", + "digestion", + "digue", + "diluer", + "dimanche", + "dinde", + "diode", + "dire", + "diriger", + "discours", + "disposer", + "distance", + "divan", + "divers", + "docile", + "docteur", + "dodu", + "dogme", + "doigt", + "dominer", + "donation", + "donjon", + "donner", + "dopage", + "dorer", + "dormir", + "doseur", + "douane", + "double", + "douche", + "douleur", + "doute", + "doux", + "douzaine", + "draguer", + "drame", + "drap", + "dresser", + "droit", + "duel", + "dune", + "duper", + "durant", + "durcir", + "durer", + "eaux", + "effacer", + "effet", + "effort", + "effrayant", + "elle", + "embrasser", + "emmener", + "emparer", + "empire", + "employer", + "emporter", + "enclos", + "encore", + "endive", + "endormir", + "endroit", + "enduit", + "enfant", + "enfermer", + "enfin", + "enfler", + "enfoncer", + "enfuir", + "engager", + "engin", + "enjeu", + "enlever", + "ennemi", + "ennui", + "ensemble", + "ensuite", + "entamer", + "entendre", + "entier", + "entourer", + "entre", + "envelopper", + "envie", + "envoyer", + "erreur", + "escalier", + "espace", + "espoir", + "esprit", + "essai", + "essor", + "essuyer", + "estimer", + "exact", + "examiner", + "excuse", + "exemple", + "exiger", + "exil", + "exister", + "exode", + "expliquer", + "exposer", + "exprimer", + "extase", + "fable", + "facette", + "facile", + "fade", + "faible", + "faim", + "faire", + "fait", + "falloir", + "famille", + "faner", + "farce", + "farine", + "fatigue", + "faucon", + "faune", + "faute", + "faux", + "faveur", + "favori", + "faxer", + "feinter", + "femme", + "fendre", + "fente", + "ferme", + "festin", + "feuille", + "feutre", + "fiable", + "fibre", + "ficher", + "fier", + "figer", + "figure", + "filet", + "fille", + "filmer", + "fils", + "filtre", + "final", + "finesse", + "finir", + "fiole", + "firme", + "fixe", + "flacon", + "flair", + "flamme", + "flan", + "flaque", + "fleur", + "flocon", + "flore", + "flot", + "flou", + "fluide", + "fluor", + "flux", + "focus", + "foin", + "foire", + "foison", + "folie", + "fonction", + "fondre", + "fonte", + "force", + "forer", + "forger", + "forme", + "fort", + "fosse", + "fouet", + "fouine", + "foule", + "four", + "foyer", + "frais", + "franc", + "frapper", + "freiner", + "frimer", + "friser", + "frite", + "froid", + "froncer", + "fruit", + "fugue", + "fuir", + "fuite", + "fumer", + "fureur", + "furieux", + "fuser", + "fusil", + "futile", + "futur", + "gagner", + "gain", + "gala", + "galet", + "galop", + "gamme", + "gant", + "garage", + "garde", + "garer", + "gauche", + "gaufre", + "gaule", + "gaver", + "gazon", + "geler", + "genou", + "genre", + "gens", + "gercer", + "germer", + "geste", + "gibier", + "gicler", + "gilet", + "girafe", + "givre", + "glace", + "glisser", + "globe", + "gloire", + "gluant", + "gober", + "golf", + "gommer", + "gorge", + "gosier", + "goutte", + "grain", + "gramme", + "grand", + "gras", + "grave", + "gredin", + "griffure", + "griller", + "gris", + "gronder", + "gros", + "grotte", + "groupe", + "grue", + "guerrier", + "guetter", + "guider", + "guise", + "habiter", + "hache", + "haie", + "haine", + "halte", + "hamac", + "hanche", + "hangar", + "hanter", + "haras", + "hareng", + "harpe", + "hasard", + "hausse", + "haut", + "havre", + "herbe", + "heure", + "hibou", + "hier", + "histoire", + "hiver", + "hochet", + "homme", + "honneur", + "honte", + "horde", + "horizon", + "hormone", + "houle", + "housse", + "hublot", + "huile", + "huit", + "humain", + "humble", + "humide", + "humour", + "hurler", + "idole", + "igloo", + "ignorer", + "illusion", + "image", + "immense", + "immobile", + "imposer", + "impression", + "incapable", + "inconnu", + "index", + "indiquer", + "infime", + "injure", + "inox", + "inspirer", + "instant", + "intention", + "intime", + "inutile", + "inventer", + "inviter", + "iode", + "iris", + "issue", + "ivre", + "jade", + "jadis", + "jamais", + "jambe", + "janvier", + "jardin", + "jauge", + "jaunisse", + "jeter", + "jeton", + "jeudi", + "jeune", + "joie", + "joindre", + "joli", + "joueur", + "journal", + "judo", + "juge", + "juillet", + "juin", + "jument", + "jungle", + "jupe", + "jupon", + "jurer", + "juron", + "jury", + "jusque", + "juste", + "kayak", + "ketchup", + "kilo", + "kiwi", + "koala", + "label", + "lacet", + "lacune", + "laine", + "laisse", + "lait", + "lame", + "lancer", + "lande", + "laque", + "lard", + "largeur", + "larme", + "larve", + "lasso", + "laver", + "lendemain", + "lentement", + "lequel", + "lettre", + "leur", + "lever", + "levure", + "liane", + "libre", + "lien", + "lier", + "lieutenant", + "ligne", + "ligoter", + "liguer", + "limace", + "limer", + "limite", + "lingot", + "lion", + "lire", + "lisser", + "litre", + "livre", + "lobe", + "local", + "logis", + "loin", + "loisir", + "long", + "loque", + "lors", + "lotus", + "louer", + "loup", + "lourd", + "louve", + "loyer", + "lubie", + "lucide", + "lueur", + "luge", + "luire", + "lundi", + "lune", + "lustre", + "lutin", + "lutte", + "luxe", + "machine", + "madame", + "magie", + "magnifique", + "magot", + "maigre", + "main", + "mairie", + "maison", + "malade", + "malheur", + "malin", + "manche", + "manger", + "manier", + "manoir", + "manquer", + "marche", + "mardi", + "marge", + "mariage", + "marquer", + "mars", + "masque", + "masse", + "matin", + "mauvais", + "meilleur", + "melon", + "membre", + "menacer", + "mener", + "mensonge", + "mentir", + "menu", + "merci", + "merlu", + "mesure", + "mettre", + "meuble", + "meunier", + "meute", + "miche", + "micro", + "midi", + "miel", + "miette", + "mieux", + "milieu", + "mille", + "mimer", + "mince", + "mineur", + "ministre", + "minute", + "mirage", + "miroir", + "miser", + "mite", + "mixte", + "mobile", + "mode", + "module", + "moins", + "mois", + "moment", + "momie", + "monde", + "monsieur", + "monter", + "moquer", + "moral", + "morceau", + "mordre", + "morose", + "morse", + "mortier", + "morue", + "motif", + "motte", + "moudre", + "moule", + "mourir", + "mousse", + "mouton", + "mouvement", + "moyen", + "muer", + "muette", + "mugir", + "muguet", + "mulot", + "multiple", + "munir", + "muret", + "muse", + "musique", + "muter", + "nacre", + "nager", + "nain", + "naissance", + "narine", + "narrer", + "naseau", + "nasse", + "nation", + "nature", + "naval", + "navet", + "naviguer", + "navrer", + "neige", + "nerf", + "nerveux", + "neuf", + "neutre", + "neuve", + "neveu", + "niche", + "nier", + "niveau", + "noble", + "noce", + "nocif", + "noir", + "nomade", + "nombre", + "nommer", + "nord", + "norme", + "notaire", + "notice", + "notre", + "nouer", + "nougat", + "nourrir", + "nous", + "nouveau", + "novice", + "noyade", + "noyer", + "nuage", + "nuance", + "nuire", + "nuit", + "nulle", + "nuque", + "oasis", + "objet", + "obliger", + "obscur", + "observer", + "obtenir", + "obus", + "occasion", + "occuper", + "ocre", + "octet", + "odeur", + "odorat", + "offense", + "officier", + "offrir", + "ogive", + "oiseau", + "olive", + "ombre", + "onctueux", + "onduler", + "ongle", + "onze", + "opter", + "option", + "orageux", + "oral", + "orange", + "orbite", + "ordinaire", + "ordre", + "oreille", + "organe", + "orgie", + "orgueil", + "orient", + "origan", + "orner", + "orteil", + "ortie", + "oser", + "osselet", + "otage", + "otarie", + "ouate", + "oublier", + "ouest", + "ours", + "outil", + "outre", + "ouvert", + "ouvrir", + "ovale", + "ozone", + "pacte", + "page", + "paille", + "pain", + "paire", + "paix", + "palace", + "palissade", + "palmier", + "palpiter", + "panda", + "panneau", + "papa", + "papier", + "paquet", + "parc", + "pardi", + "parfois", + "parler", + "parmi", + "parole", + "partir", + "parvenir", + "passer", + "pastel", + "patin", + "patron", + "paume", + "pause", + "pauvre", + "paver", + "pavot", + "payer", + "pays", + "peau", + "peigne", + "peinture", + "pelage", + "pelote", + "pencher", + "pendre", + "penser", + "pente", + "percer", + "perdu", + "perle", + "permettre", + "personne", + "perte", + "peser", + "pesticide", + "petit", + "peuple", + "peur", + "phase", + "photo", + "phrase", + "piano", + "pied", + "pierre", + "pieu", + "pile", + "pilier", + "pilote", + "pilule", + "piment", + "pincer", + "pinson", + "pinte", + "pion", + "piquer", + "pirate", + "pire", + "piste", + "piton", + "pitre", + "pivot", + "pizza", + "placer", + "plage", + "plaire", + "plan", + "plaque", + "plat", + "plein", + "pleurer", + "pliage", + "plier", + "plonger", + "plot", + "pluie", + "plume", + "plus", + "pneu", + "poche", + "podium", + "poids", + "poil", + "point", + "poire", + "poison", + "poitrine", + "poivre", + "police", + "pollen", + "pomme", + "pompier", + "poncer", + "pondre", + "pont", + "portion", + "poser", + "position", + "possible", + "poste", + "potage", + "potin", + "pouce", + "poudre", + "poulet", + "poumon", + "poupe", + "pour", + "pousser", + "poutre", + "pouvoir", + "prairie", + "premier", + "prendre", + "presque", + "preuve", + "prier", + "primeur", + "prince", + "prison", + "priver", + "prix", + "prochain", + "produire", + "profond", + "proie", + "projet", + "promener", + "prononcer", + "propre", + "prose", + "prouver", + "prune", + "public", + "puce", + "pudeur", + "puiser", + "pull", + "pulpe", + "puma", + "punir", + "purge", + "putois", + "quand", + "quartier", + "quasi", + "quatre", + "quel", + "question", + "queue", + "quiche", + "quille", + "quinze", + "quitter", + "quoi", + "rabais", + "raboter", + "race", + "racheter", + "racine", + "racler", + "raconter", + "radar", + "radio", + "rafale", + "rage", + "ragot", + "raideur", + "raie", + "rail", + "raison", + "ramasser", + "ramener", + "rampe", + "rance", + "rang", + "rapace", + "rapide", + "rapport", + "rarement", + "rasage", + "raser", + "rasoir", + "rassurer", + "rater", + "ratio", + "rature", + "ravage", + "ravir", + "rayer", + "rayon", + "rebond", + "recevoir", + "recherche", + "record", + "reculer", + "redevenir", + "refuser", + "regard", + "regretter", + "rein", + "rejeter", + "rejoindre", + "relation", + "relever", + "religion", + "remarquer", + "remettre", + "remise", + "remonter", + "remplir", + "remuer", + "rencontre", + "rendre", + "renier", + "renoncer", + "rentrer", + "renverser", + "repas", + "repli", + "reposer", + "reproche", + "requin", + "respect", + "ressembler", + "reste", + "retard", + "retenir", + "retirer", + "retour", + "retrouver", + "revenir", + "revoir", + "revue", + "rhume", + "ricaner", + "riche", + "rideau", + "ridicule", + "rien", + "rigide", + "rincer", + "rire", + "risquer", + "rituel", + "rivage", + "rive", + "robe", + "robot", + "robuste", + "rocade", + "roche", + "rodeur", + "rogner", + "roman", + "rompre", + "ronce", + "rondeur", + "ronger", + "roque", + "rose", + "rosir", + "rotation", + "rotule", + "roue", + "rouge", + "rouler", + "route", + "ruban", + "rubis", + "ruche", + "rude", + "ruelle", + "ruer", + "rugby", + "rugir", + "ruine", + "rumeur", + "rural", + "ruse", + "rustre", + "sable", + "sabot", + "sabre", + "sacre", + "sage", + "saint", + "saisir", + "salade", + "salive", + "salle", + "salon", + "salto", + "salut", + "salve", + "samba", + "sandale", + "sanguin", + "sapin", + "sarcasme", + "satisfaire", + "sauce", + "sauf", + "sauge", + "saule", + "sauna", + "sauter", + "sauver", + "savoir", + "science", + "scoop", + "score", + "second", + "secret", + "secte", + "seigneur", + "sein", + "seize", + "selle", + "selon", + "semaine", + "sembler", + "semer", + "semis", + "sensuel", + "sentir", + "sept", + "serpe", + "serrer", + "sertir", + "service", + "seuil", + "seulement", + "short", + "sien", + "sigle", + "signal", + "silence", + "silo", + "simple", + "singe", + "sinon", + "sinus", + "sioux", + "sirop", + "site", + "situation", + "skier", + "snob", + "sobre", + "social", + "socle", + "sodium", + "soigner", + "soir", + "soixante", + "soja", + "solaire", + "soldat", + "soleil", + "solide", + "solo", + "solvant", + "sombre", + "somme", + "somnoler", + "sondage", + "songeur", + "sonner", + "sorte", + "sosie", + "sottise", + "souci", + "soudain", + "souffrir", + "souhaiter", + "soulever", + "soumettre", + "soupe", + "sourd", + "soustraire", + "soutenir", + "souvent", + "soyeux", + "spectacle", + "sport", + "stade", + "stagiaire", + "stand", + "star", + "statue", + "stock", + "stop", + "store", + "style", + "suave", + "subir", + "sucre", + "suer", + "suffire", + "suie", + "suite", + "suivre", + "sujet", + "sulfite", + "supposer", + "surf", + "surprendre", + "surtout", + "surveiller", + "tabac", + "table", + "tabou", + "tache", + "tacler", + "tacot", + "tact", + "taie", + "taille", + "taire", + "talon", + "talus", + "tandis", + "tango", + "tanin", + "tant", + "taper", + "tapis", + "tard", + "tarif", + "tarot", + "tarte", + "tasse", + "taureau", + "taux", + "taverne", + "taxer", + "taxi", + "tellement", + "temple", + "tendre", + "tenir", + "tenter", + "tenu", + "terme", + "ternir", + "terre", + "test", + "texte", + "thym", + "tibia", + "tiers", + "tige", + "tipi", + "tique", + "tirer", + "tissu", + "titre", + "toast", + "toge", + "toile", + "toiser", + "toiture", + "tomber", + "tome", + "tonne", + "tonte", + "toque", + "torse", + "tortue", + "totem", + "toucher", + "toujours", + "tour", + "tousser", + "tout", + "toux", + "trace", + "train", + "trame", + "tranquille", + "travail", + "trembler", + "trente", + "tribu", + "trier", + "trio", + "tripe", + "triste", + "troc", + "trois", + "tromper", + "tronc", + "trop", + "trotter", + "trouer", + "truc", + "truite", + "tuba", + "tuer", + "tuile", + "turbo", + "tutu", + "tuyau", + "type", + "union", + "unique", + "unir", + "unisson", + "untel", + "urne", + "usage", + "user", + "usiner", + "usure", + "utile", + "vache", + "vague", + "vaincre", + "valeur", + "valoir", + "valser", + "valve", + "vampire", + "vaseux", + "vaste", + "veau", + "veille", + "veine", + "velours", + "velu", + "vendre", + "venir", + "vent", + "venue", + "verbe", + "verdict", + "version", + "vertige", + "verve", + "veste", + "veto", + "vexer", + "vice", + "victime", + "vide", + "vieil", + "vieux", + "vigie", + "vigne", + "ville", + "vingt", + "violent", + "virer", + "virus", + "visage", + "viser", + "visite", + "visuel", + "vitamine", + "vitrine", + "vivant", + "vivre", + "vocal", + "vodka", + "vogue", + "voici", + "voile", + "voir", + "voisin", + "voiture", + "volaille", + "volcan", + "voler", + "volt", + "votant", + "votre", + "vouer", + "vouloir", + "vous", + "voyage", + "voyou", + "vrac", + "vrai", + "yacht", + "yeti", + "yeux", + "yoga", + "zeste", + "zinc", + "zone", + "zoom" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/german.dart b/cw_haven/lib/mnemonics/german.dart new file mode 100644 index 000000000..1491c9b0e --- /dev/null +++ b/cw_haven/lib/mnemonics/german.dart @@ -0,0 +1,1630 @@ +class GermanMnemonics { + static const words = [ + "Abakus", + "Abart", + "abbilden", + "Abbruch", + "Abdrift", + "Abendrot", + "Abfahrt", + "abfeuern", + "Abflug", + "abfragen", + "Abglanz", + "abhärten", + "abheben", + "Abhilfe", + "Abitur", + "Abkehr", + "Ablauf", + "ablecken", + "Ablösung", + "Abnehmer", + "abnutzen", + "Abonnent", + "Abrasion", + "Abrede", + "abrüsten", + "Absicht", + "Absprung", + "Abstand", + "absuchen", + "Abteil", + "Abundanz", + "abwarten", + "Abwurf", + "Abzug", + "Achse", + "Achtung", + "Acker", + "Aderlass", + "Adler", + "Admiral", + "Adresse", + "Affe", + "Affront", + "Afrika", + "Aggregat", + "Agilität", + "ähneln", + "Ahnung", + "Ahorn", + "Akazie", + "Akkord", + "Akrobat", + "Aktfoto", + "Aktivist", + "Albatros", + "Alchimie", + "Alemanne", + "Alibi", + "Alkohol", + "Allee", + "Allüre", + "Almosen", + "Almweide", + "Aloe", + "Alpaka", + "Alpental", + "Alphabet", + "Alpinist", + "Alraune", + "Altbier", + "Alter", + "Altflöte", + "Altruist", + "Alublech", + "Aludose", + "Amateur", + "Amazonas", + "Ameise", + "Amnesie", + "Amok", + "Ampel", + "Amphibie", + "Ampulle", + "Amsel", + "Amulett", + "Anakonda", + "Analogie", + "Ananas", + "Anarchie", + "Anatomie", + "Anbau", + "Anbeginn", + "anbieten", + "Anblick", + "ändern", + "andocken", + "Andrang", + "anecken", + "Anflug", + "Anfrage", + "Anführer", + "Angebot", + "Angler", + "Anhalter", + "Anhöhe", + "Animator", + "Anis", + "Anker", + "ankleben", + "Ankunft", + "Anlage", + "anlocken", + "Anmut", + "Annahme", + "Anomalie", + "Anonymus", + "Anorak", + "anpeilen", + "Anrecht", + "Anruf", + "Ansage", + "Anschein", + "Ansicht", + "Ansporn", + "Anteil", + "Antlitz", + "Antrag", + "Antwort", + "Anwohner", + "Aorta", + "Apfel", + "Appetit", + "Applaus", + "Aquarium", + "Arbeit", + "Arche", + "Argument", + "Arktis", + "Armband", + "Aroma", + "Asche", + "Askese", + "Asphalt", + "Asteroid", + "Ästhetik", + "Astronom", + "Atelier", + "Athlet", + "Atlantik", + "Atmung", + "Audienz", + "aufatmen", + "Auffahrt", + "aufholen", + "aufregen", + "Aufsatz", + "Auftritt", + "Aufwand", + "Augapfel", + "Auktion", + "Ausbruch", + "Ausflug", + "Ausgabe", + "Aushilfe", + "Ausland", + "Ausnahme", + "Aussage", + "Autobahn", + "Avocado", + "Axthieb", + "Bach", + "backen", + "Badesee", + "Bahnhof", + "Balance", + "Balkon", + "Ballett", + "Balsam", + "Banane", + "Bandage", + "Bankett", + "Barbar", + "Barde", + "Barett", + "Bargeld", + "Barkasse", + "Barriere", + "Bart", + "Bass", + "Bastler", + "Batterie", + "Bauch", + "Bauer", + "Bauholz", + "Baujahr", + "Baum", + "Baustahl", + "Bauteil", + "Bauweise", + "Bazar", + "beachten", + "Beatmung", + "beben", + "Becher", + "Becken", + "bedanken", + "beeilen", + "beenden", + "Beere", + "befinden", + "Befreier", + "Begabung", + "Begierde", + "begrüßen", + "Beiboot", + "Beichte", + "Beifall", + "Beigabe", + "Beil", + "Beispiel", + "Beitrag", + "beizen", + "bekommen", + "beladen", + "Beleg", + "bellen", + "belohnen", + "Bemalung", + "Bengel", + "Benutzer", + "Benzin", + "beraten", + "Bereich", + "Bergluft", + "Bericht", + "Bescheid", + "Besitz", + "besorgen", + "Bestand", + "Besuch", + "betanken", + "beten", + "betören", + "Bett", + "Beule", + "Beute", + "Bewegung", + "bewirken", + "Bewohner", + "bezahlen", + "Bezug", + "biegen", + "Biene", + "Bierzelt", + "bieten", + "Bikini", + "Bildung", + "Billard", + "binden", + "Biobauer", + "Biologe", + "Bionik", + "Biotop", + "Birke", + "Bison", + "Bitte", + "Biwak", + "Bizeps", + "blasen", + "Blatt", + "Blauwal", + "Blende", + "Blick", + "Blitz", + "Blockade", + "Blödelei", + "Blondine", + "Blues", + "Blume", + "Blut", + "Bodensee", + "Bogen", + "Boje", + "Bollwerk", + "Bonbon", + "Bonus", + "Boot", + "Bordarzt", + "Börse", + "Böschung", + "Boudoir", + "Boxkampf", + "Boykott", + "Brahms", + "Brandung", + "Brauerei", + "Brecher", + "Breitaxt", + "Bremse", + "brennen", + "Brett", + "Brief", + "Brigade", + "Brillanz", + "bringen", + "brodeln", + "Brosche", + "Brötchen", + "Brücke", + "Brunnen", + "Brüste", + "Brutofen", + "Buch", + "Büffel", + "Bugwelle", + "Bühne", + "Buletten", + "Bullauge", + "Bumerang", + "bummeln", + "Buntglas", + "Bürde", + "Burgherr", + "Bursche", + "Busen", + "Buslinie", + "Bussard", + "Butangas", + "Butter", + "Cabrio", + "campen", + "Captain", + "Cartoon", + "Cello", + "Chalet", + "Charisma", + "Chefarzt", + "Chiffon", + "Chipsatz", + "Chirurg", + "Chor", + "Chronik", + "Chuzpe", + "Clubhaus", + "Cockpit", + "Codewort", + "Cognac", + "Coladose", + "Computer", + "Coupon", + "Cousin", + "Cracking", + "Crash", + "Curry", + "Dach", + "Dackel", + "daddeln", + "daliegen", + "Dame", + "Dammbau", + "Dämon", + "Dampflok", + "Dank", + "Darm", + "Datei", + "Datsche", + "Datteln", + "Datum", + "Dauer", + "Daunen", + "Deckel", + "Decoder", + "Defekt", + "Degen", + "Dehnung", + "Deiche", + "Dekade", + "Dekor", + "Delfin", + "Demut", + "denken", + "Deponie", + "Design", + "Desktop", + "Dessert", + "Detail", + "Detektiv", + "Dezibel", + "Diadem", + "Diagnose", + "Dialekt", + "Diamant", + "Dichter", + "Dickicht", + "Diesel", + "Diktat", + "Diplom", + "Direktor", + "Dirne", + "Diskurs", + "Distanz", + "Docht", + "Dohle", + "Dolch", + "Domäne", + "Donner", + "Dorade", + "Dorf", + "Dörrobst", + "Dorsch", + "Dossier", + "Dozent", + "Drachen", + "Draht", + "Drama", + "Drang", + "Drehbuch", + "Dreieck", + "Dressur", + "Drittel", + "Drossel", + "Druck", + "Duell", + "Duft", + "Düne", + "Dünung", + "dürfen", + "Duschbad", + "Düsenjet", + "Dynamik", + "Ebbe", + "Echolot", + "Echse", + "Eckball", + "Edding", + "Edelweiß", + "Eden", + "Edition", + "Efeu", + "Effekte", + "Egoismus", + "Ehre", + "Eiablage", + "Eiche", + "Eidechse", + "Eidotter", + "Eierkopf", + "Eigelb", + "Eiland", + "Eilbote", + "Eimer", + "einatmen", + "Einband", + "Eindruck", + "Einfall", + "Eingang", + "Einkauf", + "einladen", + "Einöde", + "Einrad", + "Eintopf", + "Einwurf", + "Einzug", + "Eisbär", + "Eisen", + "Eishöhle", + "Eismeer", + "Eiweiß", + "Ekstase", + "Elan", + "Elch", + "Elefant", + "Eleganz", + "Element", + "Elfe", + "Elite", + "Elixier", + "Ellbogen", + "Eloquenz", + "Emigrant", + "Emission", + "Emotion", + "Empathie", + "Empfang", + "Endzeit", + "Energie", + "Engpass", + "Enkel", + "Enklave", + "Ente", + "entheben", + "Entität", + "entladen", + "Entwurf", + "Episode", + "Epoche", + "erachten", + "Erbauer", + "erblühen", + "Erdbeere", + "Erde", + "Erdgas", + "Erdkunde", + "Erdnuss", + "Erdöl", + "Erdteil", + "Ereignis", + "Eremit", + "erfahren", + "Erfolg", + "erfreuen", + "erfüllen", + "Ergebnis", + "erhitzen", + "erkalten", + "erkennen", + "erleben", + "Erlösung", + "ernähren", + "erneuern", + "Ernte", + "Eroberer", + "eröffnen", + "Erosion", + "Erotik", + "Erpel", + "erraten", + "Erreger", + "erröten", + "Ersatz", + "Erstflug", + "Ertrag", + "Eruption", + "erwarten", + "erwidern", + "Erzbau", + "Erzeuger", + "erziehen", + "Esel", + "Eskimo", + "Eskorte", + "Espe", + "Espresso", + "essen", + "Etage", + "Etappe", + "Etat", + "Ethik", + "Etikett", + "Etüde", + "Eule", + "Euphorie", + "Europa", + "Everest", + "Examen", + "Exil", + "Exodus", + "Extrakt", + "Fabel", + "Fabrik", + "Fachmann", + "Fackel", + "Faden", + "Fagott", + "Fahne", + "Faible", + "Fairness", + "Fakt", + "Fakultät", + "Falke", + "Fallobst", + "Fälscher", + "Faltboot", + "Familie", + "Fanclub", + "Fanfare", + "Fangarm", + "Fantasie", + "Farbe", + "Farmhaus", + "Farn", + "Fasan", + "Faser", + "Fassung", + "fasten", + "Faulheit", + "Fauna", + "Faust", + "Favorit", + "Faxgerät", + "Fazit", + "fechten", + "Federboa", + "Fehler", + "Feier", + "Feige", + "feilen", + "Feinripp", + "Feldbett", + "Felge", + "Fellpony", + "Felswand", + "Ferien", + "Ferkel", + "Fernweh", + "Ferse", + "Fest", + "Fettnapf", + "Feuer", + "Fiasko", + "Fichte", + "Fiktion", + "Film", + "Filter", + "Filz", + "Finanzen", + "Findling", + "Finger", + "Fink", + "Finnwal", + "Fisch", + "Fitness", + "Fixpunkt", + "Fixstern", + "Fjord", + "Flachbau", + "Flagge", + "Flamenco", + "Flanke", + "Flasche", + "Flaute", + "Fleck", + "Flegel", + "flehen", + "Fleisch", + "fliegen", + "Flinte", + "Flirt", + "Flocke", + "Floh", + "Floskel", + "Floß", + "Flöte", + "Flugzeug", + "Flunder", + "Flusstal", + "Flutung", + "Fockmast", + "Fohlen", + "Föhnlage", + "Fokus", + "folgen", + "Foliant", + "Folklore", + "Fontäne", + "Förde", + "Forelle", + "Format", + "Forscher", + "Fortgang", + "Forum", + "Fotograf", + "Frachter", + "Fragment", + "Fraktion", + "fräsen", + "Frauenpo", + "Freak", + "Fregatte", + "Freiheit", + "Freude", + "Frieden", + "Frohsinn", + "Frosch", + "Frucht", + "Frühjahr", + "Fuchs", + "Fügung", + "fühlen", + "Füller", + "Fundbüro", + "Funkboje", + "Funzel", + "Furnier", + "Fürsorge", + "Fusel", + "Fußbad", + "Futteral", + "Gabelung", + "gackern", + "Gage", + "gähnen", + "Galaxie", + "Galeere", + "Galopp", + "Gameboy", + "Gamsbart", + "Gandhi", + "Gang", + "Garage", + "Gardine", + "Garküche", + "Garten", + "Gasthaus", + "Gattung", + "gaukeln", + "Gazelle", + "Gebäck", + "Gebirge", + "Gebräu", + "Geburt", + "Gedanke", + "Gedeck", + "Gedicht", + "Gefahr", + "Gefieder", + "Geflügel", + "Gefühl", + "Gegend", + "Gehirn", + "Gehöft", + "Gehweg", + "Geige", + "Geist", + "Gelage", + "Geld", + "Gelenk", + "Gelübde", + "Gemälde", + "Gemeinde", + "Gemüse", + "genesen", + "Genuss", + "Gepäck", + "Geranie", + "Gericht", + "Germane", + "Geruch", + "Gesang", + "Geschenk", + "Gesetz", + "Gesindel", + "Gesöff", + "Gespan", + "Gestade", + "Gesuch", + "Getier", + "Getränk", + "Getümmel", + "Gewand", + "Geweih", + "Gewitter", + "Gewölbe", + "Geysir", + "Giftzahn", + "Gipfel", + "Giraffe", + "Gitarre", + "glänzen", + "Glasauge", + "Glatze", + "Gleis", + "Globus", + "Glück", + "glühen", + "Glutofen", + "Goldzahn", + "Gondel", + "gönnen", + "Gottheit", + "graben", + "Grafik", + "Grashalm", + "Graugans", + "greifen", + "Grenze", + "grillen", + "Groschen", + "Grotte", + "Grube", + "Grünalge", + "Gruppe", + "gruseln", + "Gulasch", + "Gummibär", + "Gurgel", + "Gürtel", + "Güterzug", + "Haarband", + "Habicht", + "hacken", + "hadern", + "Hafen", + "Hagel", + "Hähnchen", + "Haifisch", + "Haken", + "Halbaffe", + "Halsader", + "halten", + "Halunke", + "Handbuch", + "Hanf", + "Harfe", + "Harnisch", + "härten", + "Harz", + "Hasenohr", + "Haube", + "hauchen", + "Haupt", + "Haut", + "Havarie", + "Hebamme", + "hecheln", + "Heck", + "Hedonist", + "Heiler", + "Heimat", + "Heizung", + "Hektik", + "Held", + "helfen", + "Helium", + "Hemd", + "hemmen", + "Hengst", + "Herd", + "Hering", + "Herkunft", + "Hermelin", + "Herrchen", + "Herzdame", + "Heulboje", + "Hexe", + "Hilfe", + "Himbeere", + "Himmel", + "Hingabe", + "hinhören", + "Hinweis", + "Hirsch", + "Hirte", + "Hitzkopf", + "Hobel", + "Hochform", + "Hocker", + "hoffen", + "Hofhund", + "Hofnarr", + "Höhenzug", + "Hohlraum", + "Hölle", + "Holzboot", + "Honig", + "Honorar", + "horchen", + "Hörprobe", + "Höschen", + "Hotel", + "Hubraum", + "Hufeisen", + "Hügel", + "huldigen", + "Hülle", + "Humbug", + "Hummer", + "Humor", + "Hund", + "Hunger", + "Hupe", + "Hürde", + "Hurrikan", + "Hydrant", + "Hypnose", + "Ibis", + "Idee", + "Idiot", + "Igel", + "Illusion", + "Imitat", + "impfen", + "Import", + "Inferno", + "Ingwer", + "Inhalte", + "Inland", + "Insekt", + "Ironie", + "Irrfahrt", + "Irrtum", + "Isolator", + "Istwert", + "Jacke", + "Jade", + "Jagdhund", + "Jäger", + "Jaguar", + "Jahr", + "Jähzorn", + "Jazzfest", + "Jetpilot", + "jobben", + "Jochbein", + "jodeln", + "Jodsalz", + "Jolle", + "Journal", + "Jubel", + "Junge", + "Junimond", + "Jupiter", + "Jutesack", + "Juwel", + "Kabarett", + "Kabine", + "Kabuff", + "Käfer", + "Kaffee", + "Kahlkopf", + "Kaimauer", + "Kajüte", + "Kaktus", + "Kaliber", + "Kaltluft", + "Kamel", + "kämmen", + "Kampagne", + "Kanal", + "Känguru", + "Kanister", + "Kanone", + "Kante", + "Kanu", + "kapern", + "Kapitän", + "Kapuze", + "Karneval", + "Karotte", + "Käsebrot", + "Kasper", + "Kastanie", + "Katalog", + "Kathode", + "Katze", + "kaufen", + "Kaugummi", + "Kauz", + "Kehle", + "Keilerei", + "Keksdose", + "Kellner", + "Keramik", + "Kerze", + "Kessel", + "Kette", + "keuchen", + "kichern", + "Kielboot", + "Kindheit", + "Kinnbart", + "Kinosaal", + "Kiosk", + "Kissen", + "Klammer", + "Klang", + "Klapprad", + "Klartext", + "kleben", + "Klee", + "Kleinod", + "Klima", + "Klingel", + "Klippe", + "Klischee", + "Kloster", + "Klugheit", + "Klüngel", + "kneten", + "Knie", + "Knöchel", + "knüpfen", + "Kobold", + "Kochbuch", + "Kohlrabi", + "Koje", + "Kokosöl", + "Kolibri", + "Kolumne", + "Kombüse", + "Komiker", + "kommen", + "Konto", + "Konzept", + "Kopfkino", + "Kordhose", + "Korken", + "Korsett", + "Kosename", + "Krabbe", + "Krach", + "Kraft", + "Krähe", + "Kralle", + "Krapfen", + "Krater", + "kraulen", + "Kreuz", + "Krokodil", + "Kröte", + "Kugel", + "Kuhhirt", + "Kühnheit", + "Künstler", + "Kurort", + "Kurve", + "Kurzfilm", + "kuscheln", + "küssen", + "Kutter", + "Labor", + "lachen", + "Lackaffe", + "Ladeluke", + "Lagune", + "Laib", + "Lakritze", + "Lammfell", + "Land", + "Langmut", + "Lappalie", + "Last", + "Laterne", + "Latzhose", + "Laubsäge", + "laufen", + "Laune", + "Lausbub", + "Lavasee", + "Leben", + "Leder", + "Leerlauf", + "Lehm", + "Lehrer", + "leihen", + "Lektüre", + "Lenker", + "Lerche", + "Leseecke", + "Leuchter", + "Lexikon", + "Libelle", + "Libido", + "Licht", + "Liebe", + "liefern", + "Liftboy", + "Limonade", + "Lineal", + "Linoleum", + "List", + "Liveband", + "Lobrede", + "locken", + "Löffel", + "Logbuch", + "Logik", + "Lohn", + "Loipe", + "Lokal", + "Lorbeer", + "Lösung", + "löten", + "Lottofee", + "Löwe", + "Luchs", + "Luder", + "Luftpost", + "Luke", + "Lümmel", + "Lunge", + "lutschen", + "Luxus", + "Macht", + "Magazin", + "Magier", + "Magnet", + "mähen", + "Mahlzeit", + "Mahnmal", + "Maibaum", + "Maisbrei", + "Makel", + "malen", + "Mammut", + "Maniküre", + "Mantel", + "Marathon", + "Marder", + "Marine", + "Marke", + "Marmor", + "Märzluft", + "Maske", + "Maßanzug", + "Maßkrug", + "Mastkorb", + "Material", + "Matratze", + "Mauerbau", + "Maulkorb", + "Mäuschen", + "Mäzen", + "Medium", + "Meinung", + "melden", + "Melodie", + "Mensch", + "Merkmal", + "Messe", + "Metall", + "Meteor", + "Methode", + "Metzger", + "Mieze", + "Milchkuh", + "Mimose", + "Minirock", + "Minute", + "mischen", + "Missetat", + "mitgehen", + "Mittag", + "Mixtape", + "Möbel", + "Modul", + "mögen", + "Möhre", + "Molch", + "Moment", + "Monat", + "Mondflug", + "Monitor", + "Monokini", + "Monster", + "Monument", + "Moorhuhn", + "Moos", + "Möpse", + "Moral", + "Mörtel", + "Motiv", + "Motorrad", + "Möwe", + "Mühe", + "Mulatte", + "Müller", + "Mumie", + "Mund", + "Münze", + "Muschel", + "Muster", + "Mythos", + "Nabel", + "Nachtzug", + "Nackedei", + "Nagel", + "Nähe", + "Nähnadel", + "Namen", + "Narbe", + "Narwal", + "Nasenbär", + "Natur", + "Nebel", + "necken", + "Neffe", + "Neigung", + "Nektar", + "Nenner", + "Neptun", + "Nerz", + "Nessel", + "Nestbau", + "Netz", + "Neubau", + "Neuerung", + "Neugier", + "nicken", + "Niere", + "Nilpferd", + "nisten", + "Nocke", + "Nomade", + "Nordmeer", + "Notdurft", + "Notstand", + "Notwehr", + "Nudismus", + "Nuss", + "Nutzhanf", + "Oase", + "Obdach", + "Oberarzt", + "Objekt", + "Oboe", + "Obsthain", + "Ochse", + "Odyssee", + "Ofenholz", + "öffnen", + "Ohnmacht", + "Ohrfeige", + "Ohrwurm", + "Ökologie", + "Oktave", + "Ölberg", + "Olive", + "Ölkrise", + "Omelett", + "Onkel", + "Oper", + "Optiker", + "Orange", + "Orchidee", + "ordnen", + "Orgasmus", + "Orkan", + "Ortskern", + "Ortung", + "Ostasien", + "Ozean", + "Paarlauf", + "Packeis", + "paddeln", + "Paket", + "Palast", + "Pandabär", + "Panik", + "Panorama", + "Panther", + "Papagei", + "Papier", + "Paprika", + "Paradies", + "Parka", + "Parodie", + "Partner", + "Passant", + "Patent", + "Patzer", + "Pause", + "Pavian", + "Pedal", + "Pegel", + "peilen", + "Perle", + "Person", + "Pfad", + "Pfau", + "Pferd", + "Pfleger", + "Physik", + "Pier", + "Pilotwal", + "Pinzette", + "Piste", + "Plakat", + "Plankton", + "Platin", + "Plombe", + "plündern", + "Pobacke", + "Pokal", + "polieren", + "Popmusik", + "Porträt", + "Posaune", + "Postamt", + "Pottwal", + "Pracht", + "Pranke", + "Preis", + "Primat", + "Prinzip", + "Protest", + "Proviant", + "Prüfung", + "Pubertät", + "Pudding", + "Pullover", + "Pulsader", + "Punkt", + "Pute", + "Putsch", + "Puzzle", + "Python", + "quaken", + "Qualle", + "Quark", + "Quellsee", + "Querkopf", + "Quitte", + "Quote", + "Rabauke", + "Rache", + "Radclub", + "Radhose", + "Radio", + "Radtour", + "Rahmen", + "Rampe", + "Randlage", + "Ranzen", + "Rapsöl", + "Raserei", + "rasten", + "Rasur", + "Rätsel", + "Raubtier", + "Raumzeit", + "Rausch", + "Reaktor", + "Realität", + "Rebell", + "Rede", + "Reetdach", + "Regatta", + "Regen", + "Rehkitz", + "Reifen", + "Reim", + "Reise", + "Reizung", + "Rekord", + "Relevanz", + "Rennboot", + "Respekt", + "Restmüll", + "retten", + "Reue", + "Revolte", + "Rhetorik", + "Rhythmus", + "Richtung", + "Riegel", + "Rindvieh", + "Rippchen", + "Ritter", + "Robbe", + "Roboter", + "Rockband", + "Rohdaten", + "Roller", + "Roman", + "röntgen", + "Rose", + "Rosskur", + "Rost", + "Rotahorn", + "Rotglut", + "Rotznase", + "Rubrik", + "Rückweg", + "Rufmord", + "Ruhe", + "Ruine", + "Rumpf", + "Runde", + "Rüstung", + "rütteln", + "Saaltür", + "Saatguts", + "Säbel", + "Sachbuch", + "Sack", + "Saft", + "sagen", + "Sahneeis", + "Salat", + "Salbe", + "Salz", + "Sammlung", + "Samt", + "Sandbank", + "Sanftmut", + "Sardine", + "Satire", + "Sattel", + "Satzbau", + "Sauerei", + "Saum", + "Säure", + "Schall", + "Scheitel", + "Schiff", + "Schlager", + "Schmied", + "Schnee", + "Scholle", + "Schrank", + "Schulbus", + "Schwan", + "Seeadler", + "Seefahrt", + "Seehund", + "Seeufer", + "segeln", + "Sehnerv", + "Seide", + "Seilzug", + "Senf", + "Sessel", + "Seufzer", + "Sexgott", + "Sichtung", + "Signal", + "Silber", + "singen", + "Sinn", + "Sirup", + "Sitzbank", + "Skandal", + "Skikurs", + "Skipper", + "Skizze", + "Smaragd", + "Socke", + "Sohn", + "Sommer", + "Songtext", + "Sorte", + "Spagat", + "Spannung", + "Spargel", + "Specht", + "Speiseöl", + "Spiegel", + "Sport", + "spülen", + "Stadtbus", + "Stall", + "Stärke", + "Stativ", + "staunen", + "Stern", + "Stiftung", + "Stollen", + "Strömung", + "Sturm", + "Substanz", + "Südalpen", + "Sumpf", + "surfen", + "Tabak", + "Tafel", + "Tagebau", + "takeln", + "Taktung", + "Talsohle", + "Tand", + "Tanzbär", + "Tapir", + "Tarantel", + "Tarnname", + "Tasse", + "Tatnacht", + "Tatsache", + "Tatze", + "Taube", + "tauchen", + "Taufpate", + "Taumel", + "Teelicht", + "Teich", + "teilen", + "Tempo", + "Tenor", + "Terrasse", + "Testflug", + "Theater", + "Thermik", + "ticken", + "Tiefflug", + "Tierart", + "Tigerhai", + "Tinte", + "Tischler", + "toben", + "Toleranz", + "Tölpel", + "Tonband", + "Topf", + "Topmodel", + "Torbogen", + "Torlinie", + "Torte", + "Tourist", + "Tragesel", + "trampeln", + "Trapez", + "Traum", + "treffen", + "Trennung", + "Treue", + "Trick", + "trimmen", + "Trödel", + "Trost", + "Trumpf", + "tüfteln", + "Turban", + "Turm", + "Übermut", + "Ufer", + "Uhrwerk", + "umarmen", + "Umbau", + "Umfeld", + "Umgang", + "Umsturz", + "Unart", + "Unfug", + "Unimog", + "Unruhe", + "Unwucht", + "Uranerz", + "Urlaub", + "Urmensch", + "Utopie", + "Vakuum", + "Valuta", + "Vandale", + "Vase", + "Vektor", + "Ventil", + "Verb", + "Verdeck", + "Verfall", + "Vergaser", + "verhexen", + "Verlag", + "Vers", + "Vesper", + "Vieh", + "Viereck", + "Vinyl", + "Virus", + "Vitrine", + "Vollblut", + "Vorbote", + "Vorrat", + "Vorsicht", + "Vulkan", + "Wachstum", + "Wade", + "Wagemut", + "Wahlen", + "Wahrheit", + "Wald", + "Walhai", + "Wallach", + "Walnuss", + "Walzer", + "wandeln", + "Wanze", + "wärmen", + "Warnruf", + "Wäsche", + "Wasser", + "Weberei", + "wechseln", + "Wegegeld", + "wehren", + "Weiher", + "Weinglas", + "Weißbier", + "Weitwurf", + "Welle", + "Weltall", + "Werkbank", + "Werwolf", + "Wetter", + "wiehern", + "Wildgans", + "Wind", + "Wohl", + "Wohnort", + "Wolf", + "Wollust", + "Wortlaut", + "Wrack", + "Wunder", + "Wurfaxt", + "Wurst", + "Yacht", + "Yeti", + "Zacke", + "Zahl", + "zähmen", + "Zahnfee", + "Zäpfchen", + "Zaster", + "Zaumzeug", + "Zebra", + "zeigen", + "Zeitlupe", + "Zellkern", + "Zeltdach", + "Zensor", + "Zerfall", + "Zeug", + "Ziege", + "Zielfoto", + "Zimteis", + "Zobel", + "Zollhund", + "Zombie", + "Zöpfe", + "Zucht", + "Zufahrt", + "Zugfahrt", + "Zugvogel", + "Zündung", + "Zweck", + "Zyklop" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/italian.dart b/cw_haven/lib/mnemonics/italian.dart new file mode 100644 index 000000000..275f85bf4 --- /dev/null +++ b/cw_haven/lib/mnemonics/italian.dart @@ -0,0 +1,1630 @@ +class ItalianMnemonics { + static const words = [ + "abbinare", + "abbonato", + "abisso", + "abitare", + "abominio", + "accadere", + "accesso", + "acciaio", + "accordo", + "accumulo", + "acido", + "acqua", + "acrobata", + "acustico", + "adattare", + "addetto", + "addio", + "addome", + "adeguato", + "aderire", + "adorare", + "adottare", + "adozione", + "adulto", + "aereo", + "aerobica", + "affare", + "affetto", + "affidare", + "affogato", + "affronto", + "africano", + "afrodite", + "agenzia", + "aggancio", + "aggeggio", + "aggiunta", + "agio", + "agire", + "agitare", + "aglio", + "agnello", + "agosto", + "aiutare", + "albero", + "albo", + "alce", + "alchimia", + "alcool", + "alfabeto", + "algebra", + "alimento", + "allarme", + "alleanza", + "allievo", + "alloggio", + "alluce", + "alpi", + "alterare", + "altro", + "aluminio", + "amante", + "amarezza", + "ambiente", + "ambrosia", + "america", + "amico", + "ammalare", + "ammirare", + "amnesia", + "amnistia", + "amore", + "ampliare", + "amputare", + "analisi", + "anamnesi", + "ananas", + "anarchia", + "anatra", + "anca", + "ancorato", + "andare", + "androide", + "aneddoto", + "anello", + "angelo", + "angolino", + "anguilla", + "anidride", + "anima", + "annegare", + "anno", + "annuncio", + "anomalia", + "antenna", + "anticipo", + "aperto", + "apostolo", + "appalto", + "appello", + "appiglio", + "applauso", + "appoggio", + "appurare", + "aprile", + "aquila", + "arabo", + "arachidi", + "aragosta", + "arancia", + "arbitrio", + "archivio", + "arco", + "argento", + "argilla", + "aria", + "ariete", + "arma", + "armonia", + "aroma", + "arrivare", + "arrosto", + "arsenale", + "arte", + "artiglio", + "asfalto", + "asfissia", + "asino", + "asparagi", + "aspirina", + "assalire", + "assegno", + "assolto", + "assurdo", + "asta", + "astratto", + "atlante", + "atletica", + "atomo", + "atropina", + "attacco", + "attesa", + "attico", + "atto", + "attrarre", + "auguri", + "aula", + "aumento", + "aurora", + "auspicio", + "autista", + "auto", + "autunno", + "avanzare", + "avarizia", + "avere", + "aviatore", + "avido", + "avorio", + "avvenire", + "avviso", + "avvocato", + "azienda", + "azione", + "azzardo", + "azzurro", + "babbuino", + "bacio", + "badante", + "baffi", + "bagaglio", + "bagliore", + "bagno", + "balcone", + "balena", + "ballare", + "balordo", + "balsamo", + "bambola", + "bancomat", + "banda", + "barato", + "barba", + "barista", + "barriera", + "basette", + "basilico", + "bassista", + "bastare", + "battello", + "bavaglio", + "beccare", + "beduino", + "bellezza", + "bene", + "benzina", + "berretto", + "bestia", + "bevitore", + "bianco", + "bibbia", + "biberon", + "bibita", + "bici", + "bidone", + "bilancia", + "biliardo", + "binario", + "binocolo", + "biologia", + "biondina", + "biopsia", + "biossido", + "birbante", + "birra", + "biscotto", + "bisogno", + "bistecca", + "bivio", + "blindare", + "bloccare", + "bocca", + "bollire", + "bombola", + "bonifico", + "borghese", + "borsa", + "bottino", + "botulino", + "braccio", + "bradipo", + "branco", + "bravo", + "bresaola", + "bretelle", + "brevetto", + "briciola", + "brigante", + "brillare", + "brindare", + "brivido", + "broccoli", + "brontolo", + "bruciare", + "brufolo", + "bucare", + "buddista", + "budino", + "bufera", + "buffo", + "bugiardo", + "buio", + "buono", + "burrone", + "bussola", + "bustina", + "buttare", + "cabernet", + "cabina", + "cacao", + "cacciare", + "cactus", + "cadavere", + "caffe", + "calamari", + "calcio", + "caldaia", + "calmare", + "calunnia", + "calvario", + "calzone", + "cambiare", + "camera", + "camion", + "cammello", + "campana", + "canarino", + "cancello", + "candore", + "cane", + "canguro", + "cannone", + "canoa", + "cantare", + "canzone", + "caos", + "capanna", + "capello", + "capire", + "capo", + "capperi", + "capra", + "capsula", + "caraffa", + "carbone", + "carciofo", + "cardigan", + "carenza", + "caricare", + "carota", + "carrello", + "carta", + "casa", + "cascare", + "caserma", + "cashmere", + "casino", + "cassetta", + "castello", + "catalogo", + "catena", + "catorcio", + "cattivo", + "causa", + "cauzione", + "cavallo", + "caverna", + "caviglia", + "cavo", + "cazzotto", + "celibato", + "cemento", + "cenare", + "centrale", + "ceramica", + "cercare", + "ceretta", + "cerniera", + "certezza", + "cervello", + "cessione", + "cestino", + "cetriolo", + "chiave", + "chiedere", + "chilo", + "chimera", + "chiodo", + "chirurgo", + "chitarra", + "chiudere", + "ciabatta", + "ciao", + "cibo", + "ciccia", + "cicerone", + "ciclone", + "cicogna", + "cielo", + "cifra", + "cigno", + "ciliegia", + "cimitero", + "cinema", + "cinque", + "cintura", + "ciondolo", + "ciotola", + "cipolla", + "cippato", + "circuito", + "cisterna", + "citofono", + "ciuccio", + "civetta", + "civico", + "clausola", + "cliente", + "clima", + "clinica", + "cobra", + "coccole", + "cocktail", + "cocomero", + "codice", + "coesione", + "cogliere", + "cognome", + "colla", + "colomba", + "colpire", + "coltello", + "comando", + "comitato", + "commedia", + "comodino", + "compagna", + "comune", + "concerto", + "condotto", + "conforto", + "congiura", + "coniglio", + "consegna", + "conto", + "convegno", + "coperta", + "copia", + "coprire", + "corazza", + "corda", + "corleone", + "cornice", + "corona", + "corpo", + "corrente", + "corsa", + "cortesia", + "corvo", + "coso", + "costume", + "cotone", + "cottura", + "cozza", + "crampo", + "cratere", + "cravatta", + "creare", + "credere", + "crema", + "crescere", + "crimine", + "criterio", + "croce", + "crollare", + "cronaca", + "crostata", + "croupier", + "cubetto", + "cucciolo", + "cucina", + "cultura", + "cuoco", + "cuore", + "cupido", + "cupola", + "cura", + "curva", + "cuscino", + "custode", + "danzare", + "data", + "decennio", + "decidere", + "decollo", + "dedicare", + "dedurre", + "definire", + "delegare", + "delfino", + "delitto", + "demone", + "dentista", + "denuncia", + "deposito", + "derivare", + "deserto", + "designer", + "destino", + "detonare", + "dettagli", + "diagnosi", + "dialogo", + "diamante", + "diario", + "diavolo", + "dicembre", + "difesa", + "digerire", + "digitare", + "diluvio", + "dinamica", + "dipinto", + "diploma", + "diramare", + "dire", + "dirigere", + "dirupo", + "discesa", + "disdetta", + "disegno", + "disporre", + "dissenso", + "distacco", + "dito", + "ditta", + "diva", + "divenire", + "dividere", + "divorare", + "docente", + "dolcetto", + "dolore", + "domatore", + "domenica", + "dominare", + "donatore", + "donna", + "dorato", + "dormire", + "dorso", + "dosaggio", + "dottore", + "dovere", + "download", + "dragone", + "dramma", + "dubbio", + "dubitare", + "duetto", + "durata", + "ebbrezza", + "eccesso", + "eccitare", + "eclissi", + "economia", + "edera", + "edificio", + "editore", + "edizione", + "educare", + "effetto", + "egitto", + "egiziano", + "elastico", + "elefante", + "eleggere", + "elemento", + "elenco", + "elezione", + "elmetto", + "elogio", + "embrione", + "emergere", + "emettere", + "eminenza", + "emisfero", + "emozione", + "empatia", + "energia", + "enfasi", + "enigma", + "entrare", + "enzima", + "epidemia", + "epilogo", + "episodio", + "epoca", + "equivoco", + "erba", + "erede", + "eroe", + "erotico", + "errore", + "eruzione", + "esaltare", + "esame", + "esaudire", + "eseguire", + "esempio", + "esigere", + "esistere", + "esito", + "esperto", + "espresso", + "essere", + "estasi", + "esterno", + "estrarre", + "eterno", + "etica", + "euforico", + "europa", + "evacuare", + "evasione", + "evento", + "evidenza", + "evitare", + "evolvere", + "fabbrica", + "facciata", + "fagiano", + "fagotto", + "falco", + "fame", + "famiglia", + "fanale", + "fango", + "fantasia", + "farfalla", + "farmacia", + "faro", + "fase", + "fastidio", + "faticare", + "fatto", + "favola", + "febbre", + "femmina", + "femore", + "fenomeno", + "fermata", + "feromoni", + "ferrari", + "fessura", + "festa", + "fiaba", + "fiamma", + "fianco", + "fiat", + "fibbia", + "fidare", + "fieno", + "figa", + "figlio", + "figura", + "filetto", + "filmato", + "filosofo", + "filtrare", + "finanza", + "finestra", + "fingere", + "finire", + "finta", + "finzione", + "fiocco", + "fioraio", + "firewall", + "firmare", + "fisico", + "fissare", + "fittizio", + "fiume", + "flacone", + "flagello", + "flirtare", + "flusso", + "focaccia", + "foglio", + "fognario", + "follia", + "fonderia", + "fontana", + "forbici", + "forcella", + "foresta", + "forgiare", + "formare", + "fornace", + "foro", + "fortuna", + "forzare", + "fosforo", + "fotoni", + "fracasso", + "fragola", + "frantumi", + "fratello", + "frazione", + "freccia", + "freddo", + "frenare", + "fresco", + "friggere", + "frittata", + "frivolo", + "frizione", + "fronte", + "frullato", + "frumento", + "frusta", + "frutto", + "fucile", + "fuggire", + "fulmine", + "fumare", + "funzione", + "fuoco", + "furbizia", + "furgone", + "furia", + "furore", + "fusibile", + "fuso", + "futuro", + "gabbiano", + "galassia", + "gallina", + "gamba", + "gancio", + "garanzia", + "garofano", + "gasolio", + "gatto", + "gazebo", + "gazzetta", + "gelato", + "gemelli", + "generare", + "genitori", + "gennaio", + "geologia", + "germania", + "gestire", + "gettare", + "ghepardo", + "ghiaccio", + "giaccone", + "giaguaro", + "giallo", + "giappone", + "giardino", + "gigante", + "gioco", + "gioiello", + "giorno", + "giovane", + "giraffa", + "giudizio", + "giurare", + "giusto", + "globo", + "gloria", + "glucosio", + "gnocca", + "gocciola", + "godere", + "gomito", + "gomma", + "gonfiare", + "gorilla", + "governo", + "gradire", + "graffiti", + "granchio", + "grappolo", + "grasso", + "grattare", + "gridare", + "grissino", + "grondaia", + "grugnito", + "gruppo", + "guadagno", + "guaio", + "guancia", + "guardare", + "gufo", + "guidare", + "guscio", + "gusto", + "icona", + "idea", + "identico", + "idolo", + "idoneo", + "idrante", + "idrogeno", + "igiene", + "ignoto", + "imbarco", + "immagine", + "immobile", + "imparare", + "impedire", + "impianto", + "importo", + "impresa", + "impulso", + "incanto", + "incendio", + "incidere", + "incontro", + "incrocia", + "incubo", + "indagare", + "indice", + "indotto", + "infanzia", + "inferno", + "infinito", + "infranto", + "ingerire", + "inglese", + "ingoiare", + "ingresso", + "iniziare", + "innesco", + "insalata", + "inserire", + "insicuro", + "insonnia", + "insulto", + "interno", + "introiti", + "invasori", + "inverno", + "invito", + "invocare", + "ipnosi", + "ipocrita", + "ipotesi", + "ironia", + "irrigare", + "iscritto", + "isola", + "ispirare", + "isterico", + "istinto", + "istruire", + "italiano", + "jazz", + "labbra", + "labrador", + "ladro", + "lago", + "lamento", + "lampone", + "lancetta", + "lanterna", + "lapide", + "larva", + "lasagne", + "lasciare", + "lastra", + "latte", + "laurea", + "lavagna", + "lavorare", + "leccare", + "legare", + "leggere", + "lenzuolo", + "leone", + "lepre", + "letargo", + "lettera", + "levare", + "levitare", + "lezione", + "liberare", + "libidine", + "libro", + "licenza", + "lievito", + "limite", + "lince", + "lingua", + "liquore", + "lire", + "listino", + "litigare", + "litro", + "locale", + "lottare", + "lucciola", + "lucidare", + "luglio", + "luna", + "macchina", + "madama", + "madre", + "maestro", + "maggio", + "magico", + "maglione", + "magnolia", + "mago", + "maialino", + "maionese", + "malattia", + "male", + "malloppo", + "mancare", + "mandorla", + "mangiare", + "manico", + "manopola", + "mansarda", + "mantello", + "manubrio", + "manzo", + "mappa", + "mare", + "margine", + "marinaio", + "marmotta", + "marocco", + "martello", + "marzo", + "maschera", + "matrice", + "maturare", + "mazzetta", + "meandri", + "medaglia", + "medico", + "medusa", + "megafono", + "melone", + "membrana", + "menta", + "mercato", + "meritare", + "merluzzo", + "mese", + "mestiere", + "metafora", + "meteo", + "metodo", + "mettere", + "miele", + "miglio", + "miliardo", + "mimetica", + "minatore", + "minuto", + "miracolo", + "mirtillo", + "missile", + "mistero", + "misura", + "mito", + "mobile", + "moda", + "moderare", + "moglie", + "molecola", + "molle", + "momento", + "moneta", + "mongolia", + "monologo", + "montagna", + "morale", + "morbillo", + "mordere", + "mosaico", + "mosca", + "mostro", + "motivare", + "moto", + "mulino", + "mulo", + "muovere", + "muraglia", + "muscolo", + "museo", + "musica", + "mutande", + "nascere", + "nastro", + "natale", + "natura", + "nave", + "navigare", + "negare", + "negozio", + "nemico", + "nero", + "nervo", + "nessuno", + "nettare", + "neutroni", + "neve", + "nevicare", + "nicotina", + "nido", + "nipote", + "nocciola", + "noleggio", + "nome", + "nonno", + "norvegia", + "notare", + "notizia", + "nove", + "nucleo", + "nuda", + "nuotare", + "nutrire", + "obbligo", + "occhio", + "occupare", + "oceano", + "odissea", + "odore", + "offerta", + "officina", + "offrire", + "oggetto", + "oggi", + "olfatto", + "olio", + "oliva", + "ombelico", + "ombrello", + "omuncolo", + "ondata", + "onore", + "opera", + "opinione", + "opuscolo", + "opzione", + "orario", + "orbita", + "orchidea", + "ordine", + "orecchio", + "orgasmo", + "orgoglio", + "origine", + "orologio", + "oroscopo", + "orso", + "oscurare", + "ospedale", + "ospite", + "ossigeno", + "ostacolo", + "ostriche", + "ottenere", + "ottimo", + "ottobre", + "ovest", + "pacco", + "pace", + "pacifico", + "padella", + "pagare", + "pagina", + "pagnotta", + "palazzo", + "palestra", + "palpebre", + "pancetta", + "panfilo", + "panino", + "pannello", + "panorama", + "papa", + "paperino", + "paradiso", + "parcella", + "parente", + "parlare", + "parodia", + "parrucca", + "partire", + "passare", + "pasta", + "patata", + "patente", + "patogeno", + "patriota", + "pausa", + "pazienza", + "peccare", + "pecora", + "pedalare", + "pelare", + "pena", + "pendenza", + "penisola", + "pennello", + "pensare", + "pentirsi", + "percorso", + "perdono", + "perfetto", + "perizoma", + "perla", + "permesso", + "persona", + "pesare", + "pesce", + "peso", + "petardo", + "petrolio", + "pezzo", + "piacere", + "pianeta", + "piastra", + "piatto", + "piazza", + "piccolo", + "piede", + "piegare", + "pietra", + "pigiama", + "pigliare", + "pigrizia", + "pilastro", + "pilota", + "pinguino", + "pioggia", + "piombo", + "pionieri", + "piovra", + "pipa", + "pirata", + "pirolisi", + "piscina", + "pisolino", + "pista", + "pitone", + "piumino", + "pizza", + "plastica", + "platino", + "poesia", + "poiana", + "polaroid", + "polenta", + "polimero", + "pollo", + "polmone", + "polpetta", + "poltrona", + "pomodoro", + "pompa", + "popolo", + "porco", + "porta", + "porzione", + "possesso", + "postino", + "potassio", + "potere", + "poverino", + "pranzo", + "prato", + "prefisso", + "prelievo", + "premio", + "prendere", + "prestare", + "pretesa", + "prezzo", + "primario", + "privacy", + "problema", + "processo", + "prodotto", + "profeta", + "progetto", + "promessa", + "pronto", + "proposta", + "proroga", + "prossimo", + "proteina", + "prova", + "prudenza", + "pubblico", + "pudore", + "pugilato", + "pulire", + "pulsante", + "puntare", + "pupazzo", + "puzzle", + "quaderno", + "qualcuno", + "quarzo", + "quercia", + "quintale", + "rabbia", + "racconto", + "radice", + "raffica", + "ragazza", + "ragione", + "rammento", + "ramo", + "rana", + "randagio", + "rapace", + "rapinare", + "rapporto", + "rasatura", + "ravioli", + "reagire", + "realista", + "reattore", + "reazione", + "recitare", + "recluso", + "record", + "recupero", + "redigere", + "regalare", + "regina", + "regola", + "relatore", + "reliquia", + "remare", + "rendere", + "reparto", + "resina", + "resto", + "rete", + "retorica", + "rettile", + "revocare", + "riaprire", + "ribadire", + "ribelle", + "ricambio", + "ricetta", + "richiamo", + "ricordo", + "ridurre", + "riempire", + "riferire", + "riflesso", + "righello", + "rilancio", + "rilevare", + "rilievo", + "rimanere", + "rimborso", + "rinforzo", + "rinuncia", + "riparo", + "ripetere", + "riposare", + "ripulire", + "risalita", + "riscatto", + "riserva", + "riso", + "rispetto", + "ritaglio", + "ritmo", + "ritorno", + "ritratto", + "rituale", + "riunione", + "riuscire", + "riva", + "robotica", + "rondine", + "rosa", + "rospo", + "rosso", + "rotonda", + "rotta", + "roulotte", + "rubare", + "rubrica", + "ruffiano", + "rumore", + "ruota", + "ruscello", + "sabbia", + "sacco", + "saggio", + "sale", + "salire", + "salmone", + "salto", + "salutare", + "salvia", + "sangue", + "sanzioni", + "sapere", + "sapienza", + "sarcasmo", + "sardine", + "sartoria", + "sbalzo", + "sbarcare", + "sberla", + "sborsare", + "scadenza", + "scafo", + "scala", + "scambio", + "scappare", + "scarpa", + "scatola", + "scelta", + "scena", + "sceriffo", + "scheggia", + "schiuma", + "sciarpa", + "scienza", + "scimmia", + "sciopero", + "scivolo", + "sclerare", + "scolpire", + "sconto", + "scopa", + "scordare", + "scossa", + "scrivere", + "scrupolo", + "scuderia", + "scultore", + "scuola", + "scusare", + "sdraiare", + "secolo", + "sedativo", + "sedere", + "sedia", + "segare", + "segreto", + "seguire", + "semaforo", + "seme", + "senape", + "seno", + "sentiero", + "separare", + "sepolcro", + "sequenza", + "serata", + "serpente", + "servizio", + "sesso", + "seta", + "settore", + "sfamare", + "sfera", + "sfidare", + "sfiorare", + "sfogare", + "sgabello", + "sicuro", + "siepe", + "sigaro", + "silenzio", + "silicone", + "simbiosi", + "simpatia", + "simulare", + "sinapsi", + "sindrome", + "sinergia", + "sinonimo", + "sintonia", + "sirena", + "siringa", + "sistema", + "sito", + "smalto", + "smentire", + "smontare", + "soccorso", + "socio", + "soffitto", + "software", + "soggetto", + "sogliola", + "sognare", + "soldi", + "sole", + "sollievo", + "solo", + "sommario", + "sondare", + "sonno", + "sorpresa", + "sorriso", + "sospiro", + "sostegno", + "sovrano", + "spaccare", + "spada", + "spagnolo", + "spalla", + "sparire", + "spavento", + "spazio", + "specchio", + "spedire", + "spegnere", + "spendere", + "speranza", + "spessore", + "spezzare", + "spiaggia", + "spiccare", + "spiegare", + "spiffero", + "spingere", + "sponda", + "sporcare", + "spostare", + "spremuta", + "spugna", + "spumante", + "spuntare", + "squadra", + "squillo", + "staccare", + "stadio", + "stagione", + "stallone", + "stampa", + "stancare", + "starnuto", + "statura", + "stella", + "stendere", + "sterzo", + "stilista", + "stimolo", + "stinco", + "stiva", + "stoffa", + "storia", + "strada", + "stregone", + "striscia", + "studiare", + "stufa", + "stupendo", + "subire", + "successo", + "sudare", + "suono", + "superare", + "supporto", + "surfista", + "sussurro", + "svelto", + "svenire", + "sviluppo", + "svolta", + "svuotare", + "tabacco", + "tabella", + "tabu", + "tacchino", + "tacere", + "taglio", + "talento", + "tangente", + "tappeto", + "tartufo", + "tassello", + "tastiera", + "tavolo", + "tazza", + "teatro", + "tedesco", + "telaio", + "telefono", + "tema", + "temere", + "tempo", + "tendenza", + "tenebre", + "tensione", + "tentare", + "teologia", + "teorema", + "termica", + "terrazzo", + "teschio", + "tesi", + "tesoro", + "tessera", + "testa", + "thriller", + "tifoso", + "tigre", + "timbrare", + "timido", + "tinta", + "tirare", + "tisana", + "titano", + "titolo", + "toccare", + "togliere", + "topolino", + "torcia", + "torrente", + "tovaglia", + "traffico", + "tragitto", + "training", + "tramonto", + "transito", + "trapezio", + "trasloco", + "trattore", + "trazione", + "treccia", + "tregua", + "treno", + "triciclo", + "tridente", + "trilogia", + "tromba", + "troncare", + "trota", + "trovare", + "trucco", + "tubo", + "tulipano", + "tumulto", + "tunisia", + "tuono", + "turista", + "tuta", + "tutelare", + "tutore", + "ubriaco", + "uccello", + "udienza", + "udito", + "uffa", + "umanoide", + "umore", + "unghia", + "unguento", + "unicorno", + "unione", + "universo", + "uomo", + "uragano", + "uranio", + "urlare", + "uscire", + "utente", + "utilizzo", + "vacanza", + "vacca", + "vaglio", + "vagonata", + "valle", + "valore", + "valutare", + "valvola", + "vampiro", + "vaniglia", + "vanto", + "vapore", + "variante", + "vasca", + "vaselina", + "vassoio", + "vedere", + "vegetale", + "veglia", + "veicolo", + "vela", + "veleno", + "velivolo", + "velluto", + "vendere", + "venerare", + "venire", + "vento", + "veranda", + "verbo", + "verdura", + "vergine", + "verifica", + "vernice", + "vero", + "verruca", + "versare", + "vertebra", + "vescica", + "vespaio", + "vestito", + "vesuvio", + "veterano", + "vetro", + "vetta", + "viadotto", + "viaggio", + "vibrare", + "vicenda", + "vichingo", + "vietare", + "vigilare", + "vigneto", + "villa", + "vincere", + "violino", + "vipera", + "virgola", + "virtuoso", + "visita", + "vita", + "vitello", + "vittima", + "vivavoce", + "vivere", + "viziato", + "voglia", + "volare", + "volpe", + "volto", + "volume", + "vongole", + "voragine", + "vortice", + "votare", + "vulcano", + "vuotare", + "zabaione", + "zaffiro", + "zainetto", + "zampa", + "zanzara", + "zattera", + "zavorra", + "zenzero", + "zero", + "zingaro", + "zittire", + "zoccolo", + "zolfo", + "zombie", + "zucchero" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/japanese.dart b/cw_haven/lib/mnemonics/japanese.dart new file mode 100644 index 000000000..5d17fdb14 --- /dev/null +++ b/cw_haven/lib/mnemonics/japanese.dart @@ -0,0 +1,1630 @@ +class JapaneseMnemonics { + static const words = [ + "あいこくしん", + "あいさつ", + "あいだ", + "あおぞら", + "あかちゃん", + "あきる", + "あけがた", + "あける", + "あこがれる", + "あさい", + "あさひ", + "あしあと", + "あじわう", + "あずかる", + "あずき", + "あそぶ", + "あたえる", + "あたためる", + "あたりまえ", + "あたる", + "あつい", + "あつかう", + "あっしゅく", + "あつまり", + "あつめる", + "あてな", + "あてはまる", + "あひる", + "あぶら", + "あぶる", + "あふれる", + "あまい", + "あまど", + "あまやかす", + "あまり", + "あみもの", + "あめりか", + "あやまる", + "あゆむ", + "あらいぐま", + "あらし", + "あらすじ", + "あらためる", + "あらゆる", + "あらわす", + "ありがとう", + "あわせる", + "あわてる", + "あんい", + "あんがい", + "あんこ", + "あんぜん", + "あんてい", + "あんない", + "あんまり", + "いいだす", + "いおん", + "いがい", + "いがく", + "いきおい", + "いきなり", + "いきもの", + "いきる", + "いくじ", + "いくぶん", + "いけばな", + "いけん", + "いこう", + "いこく", + "いこつ", + "いさましい", + "いさん", + "いしき", + "いじゅう", + "いじょう", + "いじわる", + "いずみ", + "いずれ", + "いせい", + "いせえび", + "いせかい", + "いせき", + "いぜん", + "いそうろう", + "いそがしい", + "いだい", + "いだく", + "いたずら", + "いたみ", + "いたりあ", + "いちおう", + "いちじ", + "いちど", + "いちば", + "いちぶ", + "いちりゅう", + "いつか", + "いっしゅん", + "いっせい", + "いっそう", + "いったん", + "いっち", + "いってい", + "いっぽう", + "いてざ", + "いてん", + "いどう", + "いとこ", + "いない", + "いなか", + "いねむり", + "いのち", + "いのる", + "いはつ", + "いばる", + "いはん", + "いびき", + "いひん", + "いふく", + "いへん", + "いほう", + "いみん", + "いもうと", + "いもたれ", + "いもり", + "いやがる", + "いやす", + "いよかん", + "いよく", + "いらい", + "いらすと", + "いりぐち", + "いりょう", + "いれい", + "いれもの", + "いれる", + "いろえんぴつ", + "いわい", + "いわう", + "いわかん", + "いわば", + "いわゆる", + "いんげんまめ", + "いんさつ", + "いんしょう", + "いんよう", + "うえき", + "うえる", + "うおざ", + "うがい", + "うかぶ", + "うかべる", + "うきわ", + "うくらいな", + "うくれれ", + "うけたまわる", + "うけつけ", + "うけとる", + "うけもつ", + "うける", + "うごかす", + "うごく", + "うこん", + "うさぎ", + "うしなう", + "うしろがみ", + "うすい", + "うすぎ", + "うすぐらい", + "うすめる", + "うせつ", + "うちあわせ", + "うちがわ", + "うちき", + "うちゅう", + "うっかり", + "うつくしい", + "うったえる", + "うつる", + "うどん", + "うなぎ", + "うなじ", + "うなずく", + "うなる", + "うねる", + "うのう", + "うぶげ", + "うぶごえ", + "うまれる", + "うめる", + "うもう", + "うやまう", + "うよく", + "うらがえす", + "うらぐち", + "うらない", + "うりあげ", + "うりきれ", + "うるさい", + "うれしい", + "うれゆき", + "うれる", + "うろこ", + "うわき", + "うわさ", + "うんこう", + "うんちん", + "うんてん", + "うんどう", + "えいえん", + "えいが", + "えいきょう", + "えいご", + "えいせい", + "えいぶん", + "えいよう", + "えいわ", + "えおり", + "えがお", + "えがく", + "えきたい", + "えくせる", + "えしゃく", + "えすて", + "えつらん", + "えのぐ", + "えほうまき", + "えほん", + "えまき", + "えもじ", + "えもの", + "えらい", + "えらぶ", + "えりあ", + "えんえん", + "えんかい", + "えんぎ", + "えんげき", + "えんしゅう", + "えんぜつ", + "えんそく", + "えんちょう", + "えんとつ", + "おいかける", + "おいこす", + "おいしい", + "おいつく", + "おうえん", + "おうさま", + "おうじ", + "おうせつ", + "おうたい", + "おうふく", + "おうべい", + "おうよう", + "おえる", + "おおい", + "おおう", + "おおどおり", + "おおや", + "おおよそ", + "おかえり", + "おかず", + "おがむ", + "おかわり", + "おぎなう", + "おきる", + "おくさま", + "おくじょう", + "おくりがな", + "おくる", + "おくれる", + "おこす", + "おこなう", + "おこる", + "おさえる", + "おさない", + "おさめる", + "おしいれ", + "おしえる", + "おじぎ", + "おじさん", + "おしゃれ", + "おそらく", + "おそわる", + "おたがい", + "おたく", + "おだやか", + "おちつく", + "おっと", + "おつり", + "おでかけ", + "おとしもの", + "おとなしい", + "おどり", + "おどろかす", + "おばさん", + "おまいり", + "おめでとう", + "おもいで", + "おもう", + "おもたい", + "おもちゃ", + "おやつ", + "おやゆび", + "およぼす", + "おらんだ", + "おろす", + "おんがく", + "おんけい", + "おんしゃ", + "おんせん", + "おんだん", + "おんちゅう", + "おんどけい", + "かあつ", + "かいが", + "がいき", + "がいけん", + "がいこう", + "かいさつ", + "かいしゃ", + "かいすいよく", + "かいぜん", + "かいぞうど", + "かいつう", + "かいてん", + "かいとう", + "かいふく", + "がいへき", + "かいほう", + "かいよう", + "がいらい", + "かいわ", + "かえる", + "かおり", + "かかえる", + "かがく", + "かがし", + "かがみ", + "かくご", + "かくとく", + "かざる", + "がぞう", + "かたい", + "かたち", + "がちょう", + "がっきゅう", + "がっこう", + "がっさん", + "がっしょう", + "かなざわし", + "かのう", + "がはく", + "かぶか", + "かほう", + "かほご", + "かまう", + "かまぼこ", + "かめれおん", + "かゆい", + "かようび", + "からい", + "かるい", + "かろう", + "かわく", + "かわら", + "がんか", + "かんけい", + "かんこう", + "かんしゃ", + "かんそう", + "かんたん", + "かんち", + "がんばる", + "きあい", + "きあつ", + "きいろ", + "ぎいん", + "きうい", + "きうん", + "きえる", + "きおう", + "きおく", + "きおち", + "きおん", + "きかい", + "きかく", + "きかんしゃ", + "ききて", + "きくばり", + "きくらげ", + "きけんせい", + "きこう", + "きこえる", + "きこく", + "きさい", + "きさく", + "きさま", + "きさらぎ", + "ぎじかがく", + "ぎしき", + "ぎじたいけん", + "ぎじにってい", + "ぎじゅつしゃ", + "きすう", + "きせい", + "きせき", + "きせつ", + "きそう", + "きぞく", + "きぞん", + "きたえる", + "きちょう", + "きつえん", + "ぎっちり", + "きつつき", + "きつね", + "きてい", + "きどう", + "きどく", + "きない", + "きなが", + "きなこ", + "きぬごし", + "きねん", + "きのう", + "きのした", + "きはく", + "きびしい", + "きひん", + "きふく", + "きぶん", + "きぼう", + "きほん", + "きまる", + "きみつ", + "きむずかしい", + "きめる", + "きもだめし", + "きもち", + "きもの", + "きゃく", + "きやく", + "ぎゅうにく", + "きよう", + "きょうりゅう", + "きらい", + "きらく", + "きりん", + "きれい", + "きれつ", + "きろく", + "ぎろん", + "きわめる", + "ぎんいろ", + "きんかくじ", + "きんじょ", + "きんようび", + "ぐあい", + "くいず", + "くうかん", + "くうき", + "くうぐん", + "くうこう", + "ぐうせい", + "くうそう", + "ぐうたら", + "くうふく", + "くうぼ", + "くかん", + "くきょう", + "くげん", + "ぐこう", + "くさい", + "くさき", + "くさばな", + "くさる", + "くしゃみ", + "くしょう", + "くすのき", + "くすりゆび", + "くせげ", + "くせん", + "ぐたいてき", + "くださる", + "くたびれる", + "くちこみ", + "くちさき", + "くつした", + "ぐっすり", + "くつろぐ", + "くとうてん", + "くどく", + "くなん", + "くねくね", + "くのう", + "くふう", + "くみあわせ", + "くみたてる", + "くめる", + "くやくしょ", + "くらす", + "くらべる", + "くるま", + "くれる", + "くろう", + "くわしい", + "ぐんかん", + "ぐんしょく", + "ぐんたい", + "ぐんて", + "けあな", + "けいかく", + "けいけん", + "けいこ", + "けいさつ", + "げいじゅつ", + "けいたい", + "げいのうじん", + "けいれき", + "けいろ", + "けおとす", + "けおりもの", + "げきか", + "げきげん", + "げきだん", + "げきちん", + "げきとつ", + "げきは", + "げきやく", + "げこう", + "げこくじょう", + "げざい", + "けさき", + "げざん", + "けしき", + "けしごむ", + "けしょう", + "げすと", + "けたば", + "けちゃっぷ", + "けちらす", + "けつあつ", + "けつい", + "けつえき", + "けっこん", + "けつじょ", + "けっせき", + "けってい", + "けつまつ", + "げつようび", + "げつれい", + "けつろん", + "げどく", + "けとばす", + "けとる", + "けなげ", + "けなす", + "けなみ", + "けぬき", + "げねつ", + "けねん", + "けはい", + "げひん", + "けぶかい", + "げぼく", + "けまり", + "けみかる", + "けむし", + "けむり", + "けもの", + "けらい", + "けろけろ", + "けわしい", + "けんい", + "けんえつ", + "けんお", + "けんか", + "げんき", + "けんげん", + "けんこう", + "けんさく", + "けんしゅう", + "けんすう", + "げんそう", + "けんちく", + "けんてい", + "けんとう", + "けんない", + "けんにん", + "げんぶつ", + "けんま", + "けんみん", + "けんめい", + "けんらん", + "けんり", + "こあくま", + "こいぬ", + "こいびと", + "ごうい", + "こうえん", + "こうおん", + "こうかん", + "ごうきゅう", + "ごうけい", + "こうこう", + "こうさい", + "こうじ", + "こうすい", + "ごうせい", + "こうそく", + "こうたい", + "こうちゃ", + "こうつう", + "こうてい", + "こうどう", + "こうない", + "こうはい", + "ごうほう", + "ごうまん", + "こうもく", + "こうりつ", + "こえる", + "こおり", + "ごかい", + "ごがつ", + "ごかん", + "こくご", + "こくさい", + "こくとう", + "こくない", + "こくはく", + "こぐま", + "こけい", + "こける", + "ここのか", + "こころ", + "こさめ", + "こしつ", + "こすう", + "こせい", + "こせき", + "こぜん", + "こそだて", + "こたい", + "こたえる", + "こたつ", + "こちょう", + "こっか", + "こつこつ", + "こつばん", + "こつぶ", + "こてい", + "こてん", + "ことがら", + "ことし", + "ことば", + "ことり", + "こなごな", + "こねこね", + "このまま", + "このみ", + "このよ", + "ごはん", + "こひつじ", + "こふう", + "こふん", + "こぼれる", + "ごまあぶら", + "こまかい", + "ごますり", + "こまつな", + "こまる", + "こむぎこ", + "こもじ", + "こもち", + "こもの", + "こもん", + "こやく", + "こやま", + "こゆう", + "こゆび", + "こよい", + "こよう", + "こりる", + "これくしょん", + "ころっけ", + "こわもて", + "こわれる", + "こんいん", + "こんかい", + "こんき", + "こんしゅう", + "こんすい", + "こんだて", + "こんとん", + "こんなん", + "こんびに", + "こんぽん", + "こんまけ", + "こんや", + "こんれい", + "こんわく", + "ざいえき", + "さいかい", + "さいきん", + "ざいげん", + "ざいこ", + "さいしょ", + "さいせい", + "ざいたく", + "ざいちゅう", + "さいてき", + "ざいりょう", + "さうな", + "さかいし", + "さがす", + "さかな", + "さかみち", + "さがる", + "さぎょう", + "さくし", + "さくひん", + "さくら", + "さこく", + "さこつ", + "さずかる", + "ざせき", + "さたん", + "さつえい", + "ざつおん", + "ざっか", + "ざつがく", + "さっきょく", + "ざっし", + "さつじん", + "ざっそう", + "さつたば", + "さつまいも", + "さてい", + "さといも", + "さとう", + "さとおや", + "さとし", + "さとる", + "さのう", + "さばく", + "さびしい", + "さべつ", + "さほう", + "さほど", + "さます", + "さみしい", + "さみだれ", + "さむけ", + "さめる", + "さやえんどう", + "さゆう", + "さよう", + "さよく", + "さらだ", + "ざるそば", + "さわやか", + "さわる", + "さんいん", + "さんか", + "さんきゃく", + "さんこう", + "さんさい", + "ざんしょ", + "さんすう", + "さんせい", + "さんそ", + "さんち", + "さんま", + "さんみ", + "さんらん", + "しあい", + "しあげ", + "しあさって", + "しあわせ", + "しいく", + "しいん", + "しうち", + "しえい", + "しおけ", + "しかい", + "しかく", + "じかん", + "しごと", + "しすう", + "じだい", + "したうけ", + "したぎ", + "したて", + "したみ", + "しちょう", + "しちりん", + "しっかり", + "しつじ", + "しつもん", + "してい", + "してき", + "してつ", + "じてん", + "じどう", + "しなぎれ", + "しなもの", + "しなん", + "しねま", + "しねん", + "しのぐ", + "しのぶ", + "しはい", + "しばかり", + "しはつ", + "しはらい", + "しはん", + "しひょう", + "しふく", + "じぶん", + "しへい", + "しほう", + "しほん", + "しまう", + "しまる", + "しみん", + "しむける", + "じむしょ", + "しめい", + "しめる", + "しもん", + "しゃいん", + "しゃうん", + "しゃおん", + "じゃがいも", + "しやくしょ", + "しゃくほう", + "しゃけん", + "しゃこ", + "しゃざい", + "しゃしん", + "しゃせん", + "しゃそう", + "しゃたい", + "しゃちょう", + "しゃっきん", + "じゃま", + "しゃりん", + "しゃれい", + "じゆう", + "じゅうしょ", + "しゅくはく", + "じゅしん", + "しゅっせき", + "しゅみ", + "しゅらば", + "じゅんばん", + "しょうかい", + "しょくたく", + "しょっけん", + "しょどう", + "しょもつ", + "しらせる", + "しらべる", + "しんか", + "しんこう", + "じんじゃ", + "しんせいじ", + "しんちく", + "しんりん", + "すあげ", + "すあし", + "すあな", + "ずあん", + "すいえい", + "すいか", + "すいとう", + "ずいぶん", + "すいようび", + "すうがく", + "すうじつ", + "すうせん", + "すおどり", + "すきま", + "すくう", + "すくない", + "すける", + "すごい", + "すこし", + "ずさん", + "すずしい", + "すすむ", + "すすめる", + "すっかり", + "ずっしり", + "ずっと", + "すてき", + "すてる", + "すねる", + "すのこ", + "すはだ", + "すばらしい", + "ずひょう", + "ずぶぬれ", + "すぶり", + "すふれ", + "すべて", + "すべる", + "ずほう", + "すぼん", + "すまい", + "すめし", + "すもう", + "すやき", + "すらすら", + "するめ", + "すれちがう", + "すろっと", + "すわる", + "すんぜん", + "すんぽう", + "せあぶら", + "せいかつ", + "せいげん", + "せいじ", + "せいよう", + "せおう", + "せかいかん", + "せきにん", + "せきむ", + "せきゆ", + "せきらんうん", + "せけん", + "せこう", + "せすじ", + "せたい", + "せたけ", + "せっかく", + "せっきゃく", + "ぜっく", + "せっけん", + "せっこつ", + "せっさたくま", + "せつぞく", + "せつだん", + "せつでん", + "せっぱん", + "せつび", + "せつぶん", + "せつめい", + "せつりつ", + "せなか", + "せのび", + "せはば", + "せびろ", + "せぼね", + "せまい", + "せまる", + "せめる", + "せもたれ", + "せりふ", + "ぜんあく", + "せんい", + "せんえい", + "せんか", + "せんきょ", + "せんく", + "せんげん", + "ぜんご", + "せんさい", + "せんしゅ", + "せんすい", + "せんせい", + "せんぞ", + "せんたく", + "せんちょう", + "せんてい", + "せんとう", + "せんぬき", + "せんねん", + "せんぱい", + "ぜんぶ", + "ぜんぽう", + "せんむ", + "せんめんじょ", + "せんもん", + "せんやく", + "せんゆう", + "せんよう", + "ぜんら", + "ぜんりゃく", + "せんれい", + "せんろ", + "そあく", + "そいとげる", + "そいね", + "そうがんきょう", + "そうき", + "そうご", + "そうしん", + "そうだん", + "そうなん", + "そうび", + "そうめん", + "そうり", + "そえもの", + "そえん", + "そがい", + "そげき", + "そこう", + "そこそこ", + "そざい", + "そしな", + "そせい", + "そせん", + "そそぐ", + "そだてる", + "そつう", + "そつえん", + "そっかん", + "そつぎょう", + "そっけつ", + "そっこう", + "そっせん", + "そっと", + "そとがわ", + "そとづら", + "そなえる", + "そなた", + "そふぼ", + "そぼく", + "そぼろ", + "そまつ", + "そまる", + "そむく", + "そむりえ", + "そめる", + "そもそも", + "そよかぜ", + "そらまめ", + "そろう", + "そんかい", + "そんけい", + "そんざい", + "そんしつ", + "そんぞく", + "そんちょう", + "ぞんび", + "ぞんぶん", + "そんみん", + "たあい", + "たいいん", + "たいうん", + "たいえき", + "たいおう", + "だいがく", + "たいき", + "たいぐう", + "たいけん", + "たいこ", + "たいざい", + "だいじょうぶ", + "だいすき", + "たいせつ", + "たいそう", + "だいたい", + "たいちょう", + "たいてい", + "だいどころ", + "たいない", + "たいねつ", + "たいのう", + "たいはん", + "だいひょう", + "たいふう", + "たいへん", + "たいほ", + "たいまつばな", + "たいみんぐ", + "たいむ", + "たいめん", + "たいやき", + "たいよう", + "たいら", + "たいりょく", + "たいる", + "たいわん", + "たうえ", + "たえる", + "たおす", + "たおる", + "たおれる", + "たかい", + "たかね", + "たきび", + "たくさん", + "たこく", + "たこやき", + "たさい", + "たしざん", + "だじゃれ", + "たすける", + "たずさわる", + "たそがれ", + "たたかう", + "たたく", + "ただしい", + "たたみ", + "たちばな", + "だっかい", + "だっきゃく", + "だっこ", + "だっしゅつ", + "だったい", + "たてる", + "たとえる", + "たなばた", + "たにん", + "たぬき", + "たのしみ", + "たはつ", + "たぶん", + "たべる", + "たぼう", + "たまご", + "たまる", + "だむる", + "ためいき", + "ためす", + "ためる", + "たもつ", + "たやすい", + "たよる", + "たらす", + "たりきほんがん", + "たりょう", + "たりる", + "たると", + "たれる", + "たれんと", + "たろっと", + "たわむれる", + "だんあつ", + "たんい", + "たんおん", + "たんか", + "たんき", + "たんけん", + "たんご", + "たんさん", + "たんじょうび", + "だんせい", + "たんそく", + "たんたい", + "だんち", + "たんてい", + "たんとう", + "だんな", + "たんにん", + "だんねつ", + "たんのう", + "たんぴん", + "だんぼう", + "たんまつ", + "たんめい", + "だんれつ", + "だんろ", + "だんわ", + "ちあい", + "ちあん", + "ちいき", + "ちいさい", + "ちえん", + "ちかい", + "ちから", + "ちきゅう", + "ちきん", + "ちけいず", + "ちけん", + "ちこく", + "ちさい", + "ちしき", + "ちしりょう", + "ちせい", + "ちそう", + "ちたい", + "ちたん", + "ちちおや", + "ちつじょ", + "ちてき", + "ちてん", + "ちぬき", + "ちぬり", + "ちのう", + "ちひょう", + "ちへいせん", + "ちほう", + "ちまた", + "ちみつ", + "ちみどろ", + "ちめいど", + "ちゃんこなべ", + "ちゅうい", + "ちゆりょく", + "ちょうし", + "ちょさくけん", + "ちらし", + "ちらみ", + "ちりがみ", + "ちりょう", + "ちるど", + "ちわわ", + "ちんたい", + "ちんもく", + "ついか", + "ついたち", + "つうか", + "つうじょう", + "つうはん", + "つうわ", + "つかう", + "つかれる", + "つくね", + "つくる", + "つけね", + "つける", + "つごう", + "つたえる", + "つづく", + "つつじ", + "つつむ", + "つとめる", + "つながる", + "つなみ", + "つねづね", + "つのる", + "つぶす", + "つまらない", + "つまる", + "つみき", + "つめたい", + "つもり", + "つもる", + "つよい", + "つるぼ", + "つるみく", + "つわもの", + "つわり", + "てあし", + "てあて", + "てあみ", + "ていおん", + "ていか", + "ていき", + "ていけい", + "ていこく", + "ていさつ", + "ていし", + "ていせい", + "ていたい", + "ていど", + "ていねい", + "ていひょう", + "ていへん", + "ていぼう", + "てうち", + "ておくれ", + "てきとう", + "てくび", + "でこぼこ", + "てさぎょう", + "てさげ", + "てすり", + "てそう", + "てちがい", + "てちょう", + "てつがく", + "てつづき", + "でっぱ", + "てつぼう", + "てつや", + "でぬかえ", + "てぬき", + "てぬぐい", + "てのひら", + "てはい", + "てぶくろ", + "てふだ", + "てほどき", + "てほん", + "てまえ", + "てまきずし", + "てみじか", + "てみやげ", + "てらす", + "てれび", + "てわけ", + "てわたし", + "でんあつ", + "てんいん", + "てんかい", + "てんき", + "てんぐ", + "てんけん", + "てんごく", + "てんさい", + "てんし", + "てんすう", + "でんち", + "てんてき", + "てんとう", + "てんない", + "てんぷら", + "てんぼうだい", + "てんめつ", + "てんらんかい", + "でんりょく", + "でんわ", + "どあい", + "といれ", + "どうかん", + "とうきゅう", + "どうぐ", + "とうし", + "とうむぎ", + "とおい", + "とおか", + "とおく", + "とおす", + "とおる", + "とかい", + "とかす", + "ときおり", + "ときどき", + "とくい", + "とくしゅう", + "とくてん", + "とくに", + "とくべつ", + "とけい", + "とける", + "とこや", + "とさか", + "としょかん", + "とそう", + "とたん", + "とちゅう", + "とっきゅう", + "とっくん", + "とつぜん", + "とつにゅう", + "とどける", + "ととのえる", + "とない", + "となえる", + "となり", + "とのさま", + "とばす", + "どぶがわ", + "とほう", + "とまる", + "とめる", + "ともだち", + "ともる", + "どようび", + "とらえる", + "とんかつ", + "どんぶり", + "ないかく", + "ないこう", + "ないしょ", + "ないす", + "ないせん", + "ないそう", + "なおす", + "ながい", + "なくす", + "なげる", + "なこうど", + "なさけ", + "なたでここ", + "なっとう", + "なつやすみ", + "ななおし", + "なにごと", + "なにもの", + "なにわ", + "なのか", + "なふだ", + "なまいき", + "なまえ", + "なまみ", + "なみだ", + "なめらか", + "なめる", + "なやむ", + "ならう", + "ならび", + "ならぶ", + "なれる", + "なわとび", + "なわばり", + "にあう", + "にいがた", + "にうけ", + "におい", + "にかい", + "にがて", + "にきび", + "にくしみ", + "にくまん", + "にげる", + "にさんかたんそ", + "にしき", + "にせもの", + "にちじょう", + "にちようび", + "にっか", + "にっき", + "にっけい", + "にっこう", + "にっさん", + "にっしょく", + "にっすう", + "にっせき", + "にってい", + "になう", + "にほん", + "にまめ", + "にもつ", + "にやり", + "にゅういん", + "にりんしゃ", + "にわとり", + "にんい", + "にんか", + "にんき", + "にんげん", + "にんしき", + "にんずう", + "にんそう", + "にんたい", + "にんち", + "にんてい", + "にんにく", + "にんぷ", + "にんまり", + "にんむ", + "にんめい", + "にんよう", + "ぬいくぎ", + "ぬかす", + "ぬぐいとる", + "ぬぐう", + "ぬくもり", + "ぬすむ", + "ぬまえび", + "ぬめり", + "ぬらす", + "ぬんちゃく", + "ねあげ", + "ねいき", + "ねいる", + "ねいろ", + "ねぐせ", + "ねくたい", + "ねくら", + "ねこぜ", + "ねこむ", + "ねさげ", + "ねすごす", + "ねそべる", + "ねだん", + "ねつい", + "ねっしん", + "ねつぞう", + "ねったいぎょ", + "ねぶそく", + "ねふだ", + "ねぼう", + "ねほりはほり", + "ねまき", + "ねまわし", + "ねみみ", + "ねむい", + "ねむたい", + "ねもと", + "ねらう", + "ねわざ", + "ねんいり", + "ねんおし", + "ねんかん", + "ねんきん", + "ねんぐ", + "ねんざ", + "ねんし", + "ねんちゃく", + "ねんど", + "ねんぴ", + "ねんぶつ", + "ねんまつ", + "ねんりょう", + "ねんれい", + "のいず", + "のおづま", + "のがす", + "のきなみ", + "のこぎり", + "のこす", + "のこる", + "のせる", + "のぞく", + "のぞむ", + "のたまう", + "のちほど", + "のっく", + "のばす", + "のはら", + "のべる", + "のぼる", + "のみもの", + "のやま", + "のらいぬ", + "のらねこ", + "のりもの", + "のりゆき", + "のれん", + "のんき", + "ばあい", + "はあく", + "ばあさん", + "ばいか", + "ばいく", + "はいけん", + "はいご", + "はいしん", + "はいすい", + "はいせん", + "はいそう", + "はいち", + "ばいばい", + "はいれつ", + "はえる", + "はおる", + "はかい", + "ばかり", + "はかる", + "はくしゅ", + "はけん", + "はこぶ", + "はさみ", + "はさん", + "はしご", + "ばしょ", + "はしる", + "はせる", + "ぱそこん", + "はそん", + "はたん", + "はちみつ", + "はつおん", + "はっかく", + "はづき", + "はっきり", + "はっくつ", + "はっけん", + "はっこう", + "はっさん", + "はっしん", + "はったつ", + "はっちゅう", + "はってん", + "はっぴょう", + "はっぽう", + "はなす", + "はなび", + "はにかむ", + "はぶらし", + "はみがき", + "はむかう", + "はめつ", + "はやい", + "はやし", + "はらう", + "はろうぃん", + "はわい", + "はんい", + "はんえい", + "はんおん", + "はんかく", + "はんきょう", + "ばんぐみ", + "はんこ", + "はんしゃ", + "はんすう", + "はんだん", + "ぱんち", + "ぱんつ", + "はんてい", + "はんとし", + "はんのう", + "はんぱ", + "はんぶん", + "はんぺん", + "はんぼうき", + "はんめい", + "はんらん", + "はんろん", + "ひいき", + "ひうん", + "ひえる", + "ひかく", + "ひかり", + "ひかる", + "ひかん", + "ひくい", + "ひけつ", + "ひこうき", + "ひこく", + "ひさい", + "ひさしぶり", + "ひさん", + "びじゅつかん", + "ひしょ" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/portuguese.dart b/cw_haven/lib/mnemonics/portuguese.dart new file mode 100644 index 000000000..bdd63d3b2 --- /dev/null +++ b/cw_haven/lib/mnemonics/portuguese.dart @@ -0,0 +1,1630 @@ +class PortugueseMnemonics { + static const words = [ + "abaular", + "abdominal", + "abeto", + "abissinio", + "abjeto", + "ablucao", + "abnegar", + "abotoar", + "abrutalhar", + "absurdo", + "abutre", + "acautelar", + "accessorios", + "acetona", + "achocolatado", + "acirrar", + "acne", + "acovardar", + "acrostico", + "actinomicete", + "acustico", + "adaptavel", + "adeus", + "adivinho", + "adjunto", + "admoestar", + "adnominal", + "adotivo", + "adquirir", + "adriatico", + "adsorcao", + "adutora", + "advogar", + "aerossol", + "afazeres", + "afetuoso", + "afixo", + "afluir", + "afortunar", + "afrouxar", + "aftosa", + "afunilar", + "agentes", + "agito", + "aglutinar", + "aiatola", + "aimore", + "aino", + "aipo", + "airoso", + "ajeitar", + "ajoelhar", + "ajudante", + "ajuste", + "alazao", + "albumina", + "alcunha", + "alegria", + "alexandre", + "alforriar", + "alguns", + "alhures", + "alivio", + "almoxarife", + "alotropico", + "alpiste", + "alquimista", + "alsaciano", + "altura", + "aluviao", + "alvura", + "amazonico", + "ambulatorio", + "ametodico", + "amizades", + "amniotico", + "amovivel", + "amurada", + "anatomico", + "ancorar", + "anexo", + "anfora", + "aniversario", + "anjo", + "anotar", + "ansioso", + "anturio", + "anuviar", + "anverso", + "anzol", + "aonde", + "apaziguar", + "apito", + "aplicavel", + "apoteotico", + "aprimorar", + "aprumo", + "apto", + "apuros", + "aquoso", + "arauto", + "arbusto", + "arduo", + "aresta", + "arfar", + "arguto", + "aritmetico", + "arlequim", + "armisticio", + "aromatizar", + "arpoar", + "arquivo", + "arrumar", + "arsenio", + "arturiano", + "aruaque", + "arvores", + "asbesto", + "ascorbico", + "aspirina", + "asqueroso", + "assustar", + "astuto", + "atazanar", + "ativo", + "atletismo", + "atmosferico", + "atormentar", + "atroz", + "aturdir", + "audivel", + "auferir", + "augusto", + "aula", + "aumento", + "aurora", + "autuar", + "avatar", + "avexar", + "avizinhar", + "avolumar", + "avulso", + "axiomatico", + "azerbaijano", + "azimute", + "azoto", + "azulejo", + "bacteriologista", + "badulaque", + "baforada", + "baixote", + "bajular", + "balzaquiana", + "bambuzal", + "banzo", + "baoba", + "baqueta", + "barulho", + "bastonete", + "batuta", + "bauxita", + "bavaro", + "bazuca", + "bcrepuscular", + "beato", + "beduino", + "begonia", + "behaviorista", + "beisebol", + "belzebu", + "bemol", + "benzido", + "beocio", + "bequer", + "berro", + "besuntar", + "betume", + "bexiga", + "bezerro", + "biatlon", + "biboca", + "bicuspide", + "bidirecional", + "bienio", + "bifurcar", + "bigorna", + "bijuteria", + "bimotor", + "binormal", + "bioxido", + "bipolarizacao", + "biquini", + "birutice", + "bisturi", + "bituca", + "biunivoco", + "bivalve", + "bizarro", + "blasfemo", + "blenorreia", + "blindar", + "bloqueio", + "blusao", + "boazuda", + "bofete", + "bojudo", + "bolso", + "bombordo", + "bonzo", + "botina", + "boquiaberto", + "bostoniano", + "botulismo", + "bourbon", + "bovino", + "boximane", + "bravura", + "brevidade", + "britar", + "broxar", + "bruno", + "bruxuleio", + "bubonico", + "bucolico", + "buda", + "budista", + "bueiro", + "buffer", + "bugre", + "bujao", + "bumerangue", + "burundines", + "busto", + "butique", + "buzios", + "caatinga", + "cabuqui", + "cacunda", + "cafuzo", + "cajueiro", + "camurca", + "canudo", + "caquizeiro", + "carvoeiro", + "casulo", + "catuaba", + "cauterizar", + "cebolinha", + "cedula", + "ceifeiro", + "celulose", + "cerzir", + "cesto", + "cetro", + "ceus", + "cevar", + "chavena", + "cheroqui", + "chita", + "chovido", + "chuvoso", + "ciatico", + "cibernetico", + "cicuta", + "cidreira", + "cientistas", + "cifrar", + "cigarro", + "cilio", + "cimo", + "cinzento", + "cioso", + "cipriota", + "cirurgico", + "cisto", + "citrico", + "ciumento", + "civismo", + "clavicula", + "clero", + "clitoris", + "cluster", + "coaxial", + "cobrir", + "cocota", + "codorniz", + "coexistir", + "cogumelo", + "coito", + "colusao", + "compaixao", + "comutativo", + "contentamento", + "convulsivo", + "coordenativa", + "coquetel", + "correto", + "corvo", + "costureiro", + "cotovia", + "covil", + "cozinheiro", + "cretino", + "cristo", + "crivo", + "crotalo", + "cruzes", + "cubo", + "cucuia", + "cueiro", + "cuidar", + "cujo", + "cultural", + "cunilingua", + "cupula", + "curvo", + "custoso", + "cutucar", + "czarismo", + "dablio", + "dacota", + "dados", + "daguerreotipo", + "daiquiri", + "daltonismo", + "damista", + "dantesco", + "daquilo", + "darwinista", + "dasein", + "dativo", + "deao", + "debutantes", + "decurso", + "deduzir", + "defunto", + "degustar", + "dejeto", + "deltoide", + "demover", + "denunciar", + "deputado", + "deque", + "dervixe", + "desvirtuar", + "deturpar", + "deuteronomio", + "devoto", + "dextrose", + "dezoito", + "diatribe", + "dicotomico", + "didatico", + "dietista", + "difuso", + "digressao", + "diluvio", + "diminuto", + "dinheiro", + "dinossauro", + "dioxido", + "diplomatico", + "dique", + "dirimivel", + "disturbio", + "diurno", + "divulgar", + "dizivel", + "doar", + "dobro", + "docura", + "dodoi", + "doer", + "dogue", + "doloso", + "domo", + "donzela", + "doping", + "dorsal", + "dossie", + "dote", + "doutro", + "doze", + "dravidico", + "dreno", + "driver", + "dropes", + "druso", + "dubnio", + "ducto", + "dueto", + "dulija", + "dundum", + "duodeno", + "duquesa", + "durou", + "duvidoso", + "duzia", + "ebano", + "ebrio", + "eburneo", + "echarpe", + "eclusa", + "ecossistema", + "ectoplasma", + "ecumenismo", + "eczema", + "eden", + "editorial", + "edredom", + "edulcorar", + "efetuar", + "efigie", + "efluvio", + "egiptologo", + "egresso", + "egua", + "einsteiniano", + "eira", + "eivar", + "eixos", + "ejetar", + "elastomero", + "eldorado", + "elixir", + "elmo", + "eloquente", + "elucidativo", + "emaranhar", + "embutir", + "emerito", + "emfa", + "emitir", + "emotivo", + "empuxo", + "emulsao", + "enamorar", + "encurvar", + "enduro", + "enevoar", + "enfurnar", + "enguico", + "enho", + "enigmista", + "enlutar", + "enormidade", + "enpreendimento", + "enquanto", + "enriquecer", + "enrugar", + "entusiastico", + "enunciar", + "envolvimento", + "enxuto", + "enzimatico", + "eolico", + "epiteto", + "epoxi", + "epura", + "equivoco", + "erario", + "erbio", + "ereto", + "erguido", + "erisipela", + "ermo", + "erotizar", + "erros", + "erupcao", + "ervilha", + "esburacar", + "escutar", + "esfuziante", + "esguio", + "esloveno", + "esmurrar", + "esoterismo", + "esperanca", + "espirito", + "espurio", + "essencialmente", + "esturricar", + "esvoacar", + "etario", + "eterno", + "etiquetar", + "etnologo", + "etos", + "etrusco", + "euclidiano", + "euforico", + "eugenico", + "eunuco", + "europio", + "eustaquio", + "eutanasia", + "evasivo", + "eventualidade", + "evitavel", + "evoluir", + "exaustor", + "excursionista", + "exercito", + "exfoliado", + "exito", + "exotico", + "expurgo", + "exsudar", + "extrusora", + "exumar", + "fabuloso", + "facultativo", + "fado", + "fagulha", + "faixas", + "fajuto", + "faltoso", + "famoso", + "fanzine", + "fapesp", + "faquir", + "fartura", + "fastio", + "faturista", + "fausto", + "favorito", + "faxineira", + "fazer", + "fealdade", + "febril", + "fecundo", + "fedorento", + "feerico", + "feixe", + "felicidade", + "felpudo", + "feltro", + "femur", + "fenotipo", + "fervura", + "festivo", + "feto", + "feudo", + "fevereiro", + "fezinha", + "fiasco", + "fibra", + "ficticio", + "fiduciario", + "fiesp", + "fifa", + "figurino", + "fijiano", + "filtro", + "finura", + "fiorde", + "fiquei", + "firula", + "fissurar", + "fitoteca", + "fivela", + "fixo", + "flavio", + "flexor", + "flibusteiro", + "flotilha", + "fluxograma", + "fobos", + "foco", + "fofura", + "foguista", + "foie", + "foliculo", + "fominha", + "fonte", + "forum", + "fosso", + "fotossintese", + "foxtrote", + "fraudulento", + "frevo", + "frivolo", + "frouxo", + "frutose", + "fuba", + "fucsia", + "fugitivo", + "fuinha", + "fujao", + "fulustreco", + "fumo", + "funileiro", + "furunculo", + "fustigar", + "futurologo", + "fuxico", + "fuzue", + "gabriel", + "gado", + "gaelico", + "gafieira", + "gaguejo", + "gaivota", + "gajo", + "galvanoplastico", + "gamo", + "ganso", + "garrucha", + "gastronomo", + "gatuno", + "gaussiano", + "gaviao", + "gaxeta", + "gazeteiro", + "gear", + "geiser", + "geminiano", + "generoso", + "genuino", + "geossinclinal", + "gerundio", + "gestual", + "getulista", + "gibi", + "gigolo", + "gilete", + "ginseng", + "giroscopio", + "glaucio", + "glacial", + "gleba", + "glifo", + "glote", + "glutonia", + "gnostico", + "goela", + "gogo", + "goitaca", + "golpista", + "gomo", + "gonzo", + "gorro", + "gostou", + "goticula", + "gourmet", + "governo", + "gozo", + "graxo", + "grevista", + "grito", + "grotesco", + "gruta", + "guaxinim", + "gude", + "gueto", + "guizo", + "guloso", + "gume", + "guru", + "gustativo", + "grelhado", + "gutural", + "habitue", + "haitiano", + "halterofilista", + "hamburguer", + "hanseniase", + "happening", + "harpista", + "hastear", + "haveres", + "hebreu", + "hectometro", + "hedonista", + "hegira", + "helena", + "helminto", + "hemorroidas", + "henrique", + "heptassilabo", + "hertziano", + "hesitar", + "heterossexual", + "heuristico", + "hexagono", + "hiato", + "hibrido", + "hidrostatico", + "hieroglifo", + "hifenizar", + "higienizar", + "hilario", + "himen", + "hino", + "hippie", + "hirsuto", + "historiografia", + "hitlerista", + "hodometro", + "hoje", + "holograma", + "homus", + "honroso", + "hoquei", + "horto", + "hostilizar", + "hotentote", + "huguenote", + "humilde", + "huno", + "hurra", + "hutu", + "iaia", + "ialorixa", + "iambico", + "iansa", + "iaque", + "iara", + "iatista", + "iberico", + "ibis", + "icar", + "iceberg", + "icosagono", + "idade", + "ideologo", + "idiotice", + "idoso", + "iemenita", + "iene", + "igarape", + "iglu", + "ignorar", + "igreja", + "iguaria", + "iidiche", + "ilativo", + "iletrado", + "ilharga", + "ilimitado", + "ilogismo", + "ilustrissimo", + "imaturo", + "imbuzeiro", + "imerso", + "imitavel", + "imovel", + "imputar", + "imutavel", + "inaveriguavel", + "incutir", + "induzir", + "inextricavel", + "infusao", + "ingua", + "inhame", + "iniquo", + "injusto", + "inning", + "inoxidavel", + "inquisitorial", + "insustentavel", + "intumescimento", + "inutilizavel", + "invulneravel", + "inzoneiro", + "iodo", + "iogurte", + "ioio", + "ionosfera", + "ioruba", + "iota", + "ipsilon", + "irascivel", + "iris", + "irlandes", + "irmaos", + "iroques", + "irrupcao", + "isca", + "isento", + "islandes", + "isotopo", + "isqueiro", + "israelita", + "isso", + "isto", + "iterbio", + "itinerario", + "itrio", + "iuane", + "iugoslavo", + "jabuticabeira", + "jacutinga", + "jade", + "jagunco", + "jainista", + "jaleco", + "jambo", + "jantarada", + "japones", + "jaqueta", + "jarro", + "jasmim", + "jato", + "jaula", + "javel", + "jazz", + "jegue", + "jeitoso", + "jejum", + "jenipapo", + "jeova", + "jequitiba", + "jersei", + "jesus", + "jetom", + "jiboia", + "jihad", + "jilo", + "jingle", + "jipe", + "jocoso", + "joelho", + "joguete", + "joio", + "jojoba", + "jorro", + "jota", + "joule", + "joviano", + "jubiloso", + "judoca", + "jugular", + "juizo", + "jujuba", + "juliano", + "jumento", + "junto", + "jururu", + "justo", + "juta", + "juventude", + "labutar", + "laguna", + "laico", + "lajota", + "lanterninha", + "lapso", + "laquear", + "lastro", + "lauto", + "lavrar", + "laxativo", + "lazer", + "leasing", + "lebre", + "lecionar", + "ledo", + "leguminoso", + "leitura", + "lele", + "lemure", + "lento", + "leonardo", + "leopardo", + "lepton", + "leque", + "leste", + "letreiro", + "leucocito", + "levitico", + "lexicologo", + "lhama", + "lhufas", + "liame", + "licoroso", + "lidocaina", + "liliputiano", + "limusine", + "linotipo", + "lipoproteina", + "liquidos", + "lirismo", + "lisura", + "liturgico", + "livros", + "lixo", + "lobulo", + "locutor", + "lodo", + "logro", + "lojista", + "lombriga", + "lontra", + "loop", + "loquaz", + "lorota", + "losango", + "lotus", + "louvor", + "luar", + "lubrificavel", + "lucros", + "lugubre", + "luis", + "luminoso", + "luneta", + "lustroso", + "luto", + "luvas", + "luxuriante", + "luzeiro", + "maduro", + "maestro", + "mafioso", + "magro", + "maiuscula", + "majoritario", + "malvisto", + "mamute", + "manutencao", + "mapoteca", + "maquinista", + "marzipa", + "masturbar", + "matuto", + "mausoleu", + "mavioso", + "maxixe", + "mazurca", + "meandro", + "mecha", + "medusa", + "mefistofelico", + "megera", + "meirinho", + "melro", + "memorizar", + "menu", + "mequetrefe", + "mertiolate", + "mestria", + "metroviario", + "mexilhao", + "mezanino", + "miau", + "microssegundo", + "midia", + "migratorio", + "mimosa", + "minuto", + "miosotis", + "mirtilo", + "misturar", + "mitzvah", + "miudos", + "mixuruca", + "mnemonico", + "moagem", + "mobilizar", + "modulo", + "moer", + "mofo", + "mogno", + "moita", + "molusco", + "monumento", + "moqueca", + "morubixaba", + "mostruario", + "motriz", + "mouse", + "movivel", + "mozarela", + "muarra", + "muculmano", + "mudo", + "mugir", + "muitos", + "mumunha", + "munir", + "muon", + "muquira", + "murros", + "musselina", + "nacoes", + "nado", + "naftalina", + "nago", + "naipe", + "naja", + "nalgum", + "namoro", + "nanquim", + "napolitano", + "naquilo", + "nascimento", + "nautilo", + "navios", + "nazista", + "nebuloso", + "nectarina", + "nefrologo", + "negus", + "nelore", + "nenufar", + "nepotismo", + "nervura", + "neste", + "netuno", + "neutron", + "nevoeiro", + "newtoniano", + "nexo", + "nhenhenhem", + "nhoque", + "nigeriano", + "niilista", + "ninho", + "niobio", + "niponico", + "niquelar", + "nirvana", + "nisto", + "nitroglicerina", + "nivoso", + "nobreza", + "nocivo", + "noel", + "nogueira", + "noivo", + "nojo", + "nominativo", + "nonuplo", + "noruegues", + "nostalgico", + "noturno", + "nouveau", + "nuanca", + "nublar", + "nucleotideo", + "nudista", + "nulo", + "numismatico", + "nunquinha", + "nupcias", + "nutritivo", + "nuvens", + "oasis", + "obcecar", + "obeso", + "obituario", + "objetos", + "oblongo", + "obnoxio", + "obrigatorio", + "obstruir", + "obtuso", + "obus", + "obvio", + "ocaso", + "occipital", + "oceanografo", + "ocioso", + "oclusivo", + "ocorrer", + "ocre", + "octogono", + "odalisca", + "odisseia", + "odorifico", + "oersted", + "oeste", + "ofertar", + "ofidio", + "oftalmologo", + "ogiva", + "ogum", + "oigale", + "oitavo", + "oitocentos", + "ojeriza", + "olaria", + "oleoso", + "olfato", + "olhos", + "oliveira", + "olmo", + "olor", + "olvidavel", + "ombudsman", + "omeleteira", + "omitir", + "omoplata", + "onanismo", + "ondular", + "oneroso", + "onomatopeico", + "ontologico", + "onus", + "onze", + "opalescente", + "opcional", + "operistico", + "opio", + "oposto", + "oprobrio", + "optometrista", + "opusculo", + "oratorio", + "orbital", + "orcar", + "orfao", + "orixa", + "orla", + "ornitologo", + "orquidea", + "ortorrombico", + "orvalho", + "osculo", + "osmotico", + "ossudo", + "ostrogodo", + "otario", + "otite", + "ouro", + "ousar", + "outubro", + "ouvir", + "ovario", + "overnight", + "oviparo", + "ovni", + "ovoviviparo", + "ovulo", + "oxala", + "oxente", + "oxiuro", + "oxossi", + "ozonizar", + "paciente", + "pactuar", + "padronizar", + "paete", + "pagodeiro", + "paixao", + "pajem", + "paludismo", + "pampas", + "panturrilha", + "papudo", + "paquistanes", + "pastoso", + "patua", + "paulo", + "pauzinhos", + "pavoroso", + "paxa", + "pazes", + "peao", + "pecuniario", + "pedunculo", + "pegaso", + "peixinho", + "pejorativo", + "pelvis", + "penuria", + "pequno", + "petunia", + "pezada", + "piauiense", + "pictorico", + "pierro", + "pigmeu", + "pijama", + "pilulas", + "pimpolho", + "pintura", + "piorar", + "pipocar", + "piqueteiro", + "pirulito", + "pistoleiro", + "pituitaria", + "pivotar", + "pixote", + "pizzaria", + "plistoceno", + "plotar", + "pluviometrico", + "pneumonico", + "poco", + "podridao", + "poetisa", + "pogrom", + "pois", + "polvorosa", + "pomposo", + "ponderado", + "pontudo", + "populoso", + "poquer", + "porvir", + "posudo", + "potro", + "pouso", + "povoar", + "prazo", + "prezar", + "privilegios", + "proximo", + "prussiano", + "pseudopode", + "psoriase", + "pterossauros", + "ptialina", + "ptolemaico", + "pudor", + "pueril", + "pufe", + "pugilista", + "puir", + "pujante", + "pulverizar", + "pumba", + "punk", + "purulento", + "pustula", + "putsch", + "puxe", + "quatrocentos", + "quetzal", + "quixotesco", + "quotizavel", + "rabujice", + "racista", + "radonio", + "rafia", + "ragu", + "rajado", + "ralo", + "rampeiro", + "ranzinza", + "raptor", + "raquitismo", + "raro", + "rasurar", + "ratoeira", + "ravioli", + "razoavel", + "reavivar", + "rebuscar", + "recusavel", + "reduzivel", + "reexposicao", + "refutavel", + "regurgitar", + "reivindicavel", + "rejuvenescimento", + "relva", + "remuneravel", + "renunciar", + "reorientar", + "repuxo", + "requisito", + "resumo", + "returno", + "reutilizar", + "revolvido", + "rezonear", + "riacho", + "ribossomo", + "ricota", + "ridiculo", + "rifle", + "rigoroso", + "rijo", + "rimel", + "rins", + "rios", + "riqueza", + "respeito", + "rissole", + "ritualistico", + "rivalizar", + "rixa", + "robusto", + "rococo", + "rodoviario", + "roer", + "rogo", + "rojao", + "rolo", + "rompimento", + "ronronar", + "roqueiro", + "rorqual", + "rosto", + "rotundo", + "rouxinol", + "roxo", + "royal", + "ruas", + "rucula", + "rudimentos", + "ruela", + "rufo", + "rugoso", + "ruivo", + "rule", + "rumoroso", + "runico", + "ruptura", + "rural", + "rustico", + "rutilar", + "saariano", + "sabujo", + "sacudir", + "sadomasoquista", + "safra", + "sagui", + "sais", + "samurai", + "santuario", + "sapo", + "saquear", + "sartriano", + "saturno", + "saude", + "sauva", + "saveiro", + "saxofonista", + "sazonal", + "scherzo", + "script", + "seara", + "seborreia", + "secura", + "seduzir", + "sefardim", + "seguro", + "seja", + "selvas", + "sempre", + "senzala", + "sepultura", + "sequoia", + "sestercio", + "setuplo", + "seus", + "seviciar", + "sezonismo", + "shalom", + "siames", + "sibilante", + "sicrano", + "sidra", + "sifilitico", + "signos", + "silvo", + "simultaneo", + "sinusite", + "sionista", + "sirio", + "sisudo", + "situar", + "sivan", + "slide", + "slogan", + "soar", + "sobrio", + "socratico", + "sodomizar", + "soerguer", + "software", + "sogro", + "soja", + "solver", + "somente", + "sonso", + "sopro", + "soquete", + "sorveteiro", + "sossego", + "soturno", + "sousafone", + "sovinice", + "sozinho", + "suavizar", + "subverter", + "sucursal", + "sudoriparo", + "sufragio", + "sugestoes", + "suite", + "sujo", + "sultao", + "sumula", + "suntuoso", + "suor", + "supurar", + "suruba", + "susto", + "suturar", + "suvenir", + "tabuleta", + "taco", + "tadjique", + "tafeta", + "tagarelice", + "taitiano", + "talvez", + "tampouco", + "tanzaniano", + "taoista", + "tapume", + "taquion", + "tarugo", + "tascar", + "tatuar", + "tautologico", + "tavola", + "taxionomista", + "tchecoslovaco", + "teatrologo", + "tectonismo", + "tedioso", + "teflon", + "tegumento", + "teixo", + "telurio", + "temporas", + "tenue", + "teosofico", + "tepido", + "tequila", + "terrorista", + "testosterona", + "tetrico", + "teutonico", + "teve", + "texugo", + "tiara", + "tibia", + "tiete", + "tifoide", + "tigresa", + "tijolo", + "tilintar", + "timpano", + "tintureiro", + "tiquete", + "tiroteio", + "tisico", + "titulos", + "tive", + "toar", + "toboga", + "tofu", + "togoles", + "toicinho", + "tolueno", + "tomografo", + "tontura", + "toponimo", + "toquio", + "torvelinho", + "tostar", + "toto", + "touro", + "toxina", + "trazer", + "trezentos", + "trivialidade", + "trovoar", + "truta", + "tuaregue", + "tubular", + "tucano", + "tudo", + "tufo", + "tuiste", + "tulipa", + "tumultuoso", + "tunisino", + "tupiniquim", + "turvo", + "tutu", + "ucraniano", + "udenista", + "ufanista", + "ufologo", + "ugaritico", + "uiste", + "uivo", + "ulceroso", + "ulema", + "ultravioleta", + "umbilical", + "umero", + "umido", + "umlaut", + "unanimidade", + "unesco", + "ungulado", + "unheiro", + "univoco", + "untuoso", + "urano", + "urbano", + "urdir", + "uretra", + "urgente", + "urinol", + "urna", + "urologo", + "urro", + "ursulina", + "urtiga", + "urupe", + "usavel", + "usbeque", + "usei", + "usineiro", + "usurpar", + "utero", + "utilizar", + "utopico", + "uvular", + "uxoricidio", + "vacuo", + "vadio", + "vaguear", + "vaivem", + "valvula", + "vampiro", + "vantajoso", + "vaporoso", + "vaquinha", + "varziano", + "vasto", + "vaticinio", + "vaudeville", + "vazio", + "veado", + "vedico", + "veemente", + "vegetativo", + "veio", + "veja", + "veludo", + "venusiano", + "verdade", + "verve", + "vestuario", + "vetusto", + "vexatorio", + "vezes", + "viavel", + "vibratorio", + "victor", + "vicunha", + "vidros", + "vietnamita", + "vigoroso", + "vilipendiar", + "vime", + "vintem", + "violoncelo", + "viquingue", + "virus", + "visualizar", + "vituperio", + "viuvo", + "vivo", + "vizir", + "voar", + "vociferar", + "vodu", + "vogar", + "voile", + "volver", + "vomito", + "vontade", + "vortice", + "vosso", + "voto", + "vovozinha", + "voyeuse", + "vozes", + "vulva", + "vupt", + "western", + "xadrez", + "xale", + "xampu", + "xango", + "xarope", + "xaual", + "xavante", + "xaxim", + "xenonio", + "xepa", + "xerox", + "xicara", + "xifopago", + "xiita", + "xilogravura", + "xinxim", + "xistoso", + "xixi", + "xodo", + "xogum", + "xucro", + "zabumba", + "zagueiro", + "zambiano", + "zanzar", + "zarpar", + "zebu", + "zefiro", + "zeloso", + "zenite", + "zumbi" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/russian.dart b/cw_haven/lib/mnemonics/russian.dart new file mode 100644 index 000000000..f10af0ff6 --- /dev/null +++ b/cw_haven/lib/mnemonics/russian.dart @@ -0,0 +1,1630 @@ +class RussianMnemonics { + static const words = [ + "абажур", + "абзац", + "абонент", + "абрикос", + "абсурд", + "авангард", + "август", + "авиация", + "авоська", + "автор", + "агат", + "агент", + "агитатор", + "агнец", + "агония", + "агрегат", + "адвокат", + "адмирал", + "адрес", + "ажиотаж", + "азарт", + "азбука", + "азот", + "аист", + "айсберг", + "академия", + "аквариум", + "аккорд", + "акробат", + "аксиома", + "актер", + "акула", + "акция", + "алгоритм", + "алебарда", + "аллея", + "алмаз", + "алтарь", + "алфавит", + "алхимик", + "алый", + "альбом", + "алюминий", + "амбар", + "аметист", + "амнезия", + "ампула", + "амфора", + "анализ", + "ангел", + "анекдот", + "анимация", + "анкета", + "аномалия", + "ансамбль", + "антенна", + "апатия", + "апельсин", + "апофеоз", + "аппарат", + "апрель", + "аптека", + "арабский", + "арбуз", + "аргумент", + "арест", + "ария", + "арка", + "армия", + "аромат", + "арсенал", + "артист", + "архив", + "аршин", + "асбест", + "аскетизм", + "аспект", + "ассорти", + "астроном", + "асфальт", + "атака", + "ателье", + "атлас", + "атом", + "атрибут", + "аудитор", + "аукцион", + "аура", + "афера", + "афиша", + "ахинея", + "ацетон", + "аэропорт", + "бабушка", + "багаж", + "бадья", + "база", + "баклажан", + "балкон", + "бампер", + "банк", + "барон", + "бассейн", + "батарея", + "бахрома", + "башня", + "баян", + "бегство", + "бедро", + "бездна", + "бекон", + "белый", + "бензин", + "берег", + "беседа", + "бетонный", + "биатлон", + "библия", + "бивень", + "бигуди", + "бидон", + "бизнес", + "бикини", + "билет", + "бинокль", + "биология", + "биржа", + "бисер", + "битва", + "бицепс", + "благо", + "бледный", + "близкий", + "блок", + "блуждать", + "блюдо", + "бляха", + "бобер", + "богатый", + "бодрый", + "боевой", + "бокал", + "большой", + "борьба", + "босой", + "ботинок", + "боцман", + "бочка", + "боярин", + "брать", + "бревно", + "бригада", + "бросать", + "брызги", + "брюки", + "бублик", + "бугор", + "будущее", + "буква", + "бульвар", + "бумага", + "бунт", + "бурный", + "бусы", + "бутылка", + "буфет", + "бухта", + "бушлат", + "бывалый", + "быль", + "быстрый", + "быть", + "бюджет", + "бюро", + "бюст", + "вагон", + "важный", + "ваза", + "вакцина", + "валюта", + "вампир", + "ванная", + "вариант", + "вассал", + "вата", + "вафля", + "вахта", + "вдова", + "вдыхать", + "ведущий", + "веер", + "вежливый", + "везти", + "веко", + "великий", + "вена", + "верить", + "веселый", + "ветер", + "вечер", + "вешать", + "вещь", + "веяние", + "взаимный", + "взбучка", + "взвод", + "взгляд", + "вздыхать", + "взлетать", + "взмах", + "взнос", + "взор", + "взрыв", + "взывать", + "взятка", + "вибрация", + "визит", + "вилка", + "вино", + "вирус", + "висеть", + "витрина", + "вихрь", + "вишневый", + "включать", + "вкус", + "власть", + "влечь", + "влияние", + "влюблять", + "внешний", + "внимание", + "внук", + "внятный", + "вода", + "воевать", + "вождь", + "воздух", + "войти", + "вокзал", + "волос", + "вопрос", + "ворота", + "восток", + "впадать", + "впускать", + "врач", + "время", + "вручать", + "всадник", + "всеобщий", + "вспышка", + "встреча", + "вторник", + "вулкан", + "вурдалак", + "входить", + "въезд", + "выбор", + "вывод", + "выгодный", + "выделять", + "выезжать", + "выживать", + "вызывать", + "выигрыш", + "вылезать", + "выносить", + "выпивать", + "высокий", + "выходить", + "вычет", + "вышка", + "выяснять", + "вязать", + "вялый", + "гавань", + "гадать", + "газета", + "гаишник", + "галстук", + "гамма", + "гарантия", + "гастроли", + "гвардия", + "гвоздь", + "гектар", + "гель", + "генерал", + "геолог", + "герой", + "гешефт", + "гибель", + "гигант", + "гильза", + "гимн", + "гипотеза", + "гитара", + "глаз", + "глина", + "глоток", + "глубокий", + "глыба", + "глядеть", + "гнать", + "гнев", + "гнить", + "гном", + "гнуть", + "говорить", + "годовой", + "голова", + "гонка", + "город", + "гость", + "готовый", + "граница", + "грех", + "гриб", + "громкий", + "группа", + "грызть", + "грязный", + "губа", + "гудеть", + "гулять", + "гуманный", + "густой", + "гуща", + "давать", + "далекий", + "дама", + "данные", + "дарить", + "дать", + "дача", + "дверь", + "движение", + "двор", + "дебют", + "девушка", + "дедушка", + "дежурный", + "дезертир", + "действие", + "декабрь", + "дело", + "демократ", + "день", + "депутат", + "держать", + "десяток", + "детский", + "дефицит", + "дешевый", + "деятель", + "джаз", + "джинсы", + "джунгли", + "диалог", + "диван", + "диета", + "дизайн", + "дикий", + "динамика", + "диплом", + "директор", + "диск", + "дитя", + "дичь", + "длинный", + "дневник", + "добрый", + "доверие", + "договор", + "дождь", + "доза", + "документ", + "должен", + "домашний", + "допрос", + "дорога", + "доход", + "доцент", + "дочь", + "дощатый", + "драка", + "древний", + "дрожать", + "друг", + "дрянь", + "дубовый", + "дуга", + "дудка", + "дукат", + "дуло", + "думать", + "дупло", + "дурак", + "дуть", + "духи", + "душа", + "дуэт", + "дымить", + "дыня", + "дыра", + "дыханье", + "дышать", + "дьявол", + "дюжина", + "дюйм", + "дюна", + "дядя", + "дятел", + "егерь", + "единый", + "едкий", + "ежевика", + "ежик", + "езда", + "елка", + "емкость", + "ерунда", + "ехать", + "жадный", + "жажда", + "жалеть", + "жанр", + "жара", + "жать", + "жгучий", + "ждать", + "жевать", + "желание", + "жемчуг", + "женщина", + "жертва", + "жесткий", + "жечь", + "живой", + "жидкость", + "жизнь", + "жилье", + "жирный", + "житель", + "журнал", + "жюри", + "забывать", + "завод", + "загадка", + "задача", + "зажечь", + "зайти", + "закон", + "замечать", + "занимать", + "западный", + "зарплата", + "засыпать", + "затрата", + "захват", + "зацепка", + "зачет", + "защита", + "заявка", + "звать", + "звезда", + "звонить", + "звук", + "здание", + "здешний", + "здоровье", + "зебра", + "зевать", + "зеленый", + "земля", + "зенит", + "зеркало", + "зефир", + "зигзаг", + "зима", + "зиять", + "злак", + "злой", + "змея", + "знать", + "зной", + "зодчий", + "золотой", + "зомби", + "зона", + "зоопарк", + "зоркий", + "зрачок", + "зрение", + "зритель", + "зубной", + "зыбкий", + "зять", + "игла", + "иголка", + "играть", + "идея", + "идиот", + "идол", + "идти", + "иерархия", + "избрать", + "известие", + "изгонять", + "издание", + "излагать", + "изменять", + "износ", + "изоляция", + "изрядный", + "изучать", + "изымать", + "изящный", + "икона", + "икра", + "иллюзия", + "имбирь", + "иметь", + "имидж", + "иммунный", + "империя", + "инвестор", + "индивид", + "инерция", + "инженер", + "иномарка", + "институт", + "интерес", + "инфекция", + "инцидент", + "ипподром", + "ирис", + "ирония", + "искать", + "история", + "исходить", + "исчезать", + "итог", + "июль", + "июнь", + "кабинет", + "кавалер", + "кадр", + "казарма", + "кайф", + "кактус", + "калитка", + "камень", + "канал", + "капитан", + "картина", + "касса", + "катер", + "кафе", + "качество", + "каша", + "каюта", + "квартира", + "квинтет", + "квота", + "кедр", + "кекс", + "кенгуру", + "кепка", + "керосин", + "кетчуп", + "кефир", + "кибитка", + "кивнуть", + "кидать", + "километр", + "кино", + "киоск", + "кипеть", + "кирпич", + "кисть", + "китаец", + "класс", + "клетка", + "клиент", + "клоун", + "клуб", + "клык", + "ключ", + "клятва", + "книга", + "кнопка", + "кнут", + "князь", + "кобура", + "ковер", + "коготь", + "кодекс", + "кожа", + "козел", + "койка", + "коктейль", + "колено", + "компания", + "конец", + "копейка", + "короткий", + "костюм", + "котел", + "кофе", + "кошка", + "красный", + "кресло", + "кричать", + "кровь", + "крупный", + "крыша", + "крючок", + "кубок", + "кувшин", + "кудрявый", + "кузов", + "кукла", + "культура", + "кумир", + "купить", + "курс", + "кусок", + "кухня", + "куча", + "кушать", + "кювет", + "лабиринт", + "лавка", + "лагерь", + "ладонь", + "лазерный", + "лайнер", + "лакей", + "лампа", + "ландшафт", + "лапа", + "ларек", + "ласковый", + "лауреат", + "лачуга", + "лаять", + "лгать", + "лебедь", + "левый", + "легкий", + "ледяной", + "лежать", + "лекция", + "лента", + "лепесток", + "лесной", + "лето", + "лечь", + "леший", + "лживый", + "либерал", + "ливень", + "лига", + "лидер", + "ликовать", + "лиловый", + "лимон", + "линия", + "липа", + "лирика", + "лист", + "литр", + "лифт", + "лихой", + "лицо", + "личный", + "лишний", + "лобовой", + "ловить", + "логика", + "лодка", + "ложка", + "лозунг", + "локоть", + "ломать", + "лоно", + "лопата", + "лорд", + "лось", + "лоток", + "лохматый", + "лошадь", + "лужа", + "лукавый", + "луна", + "лупить", + "лучший", + "лыжный", + "лысый", + "львиный", + "льгота", + "льдина", + "любить", + "людской", + "люстра", + "лютый", + "лягушка", + "магазин", + "мадам", + "мазать", + "майор", + "максимум", + "мальчик", + "манера", + "март", + "масса", + "мать", + "мафия", + "махать", + "мачта", + "машина", + "маэстро", + "маяк", + "мгла", + "мебель", + "медведь", + "мелкий", + "мемуары", + "менять", + "мера", + "место", + "метод", + "механизм", + "мечтать", + "мешать", + "миграция", + "мизинец", + "микрофон", + "миллион", + "минута", + "мировой", + "миссия", + "митинг", + "мишень", + "младший", + "мнение", + "мнимый", + "могила", + "модель", + "мозг", + "мойка", + "мокрый", + "молодой", + "момент", + "монах", + "море", + "мост", + "мотор", + "мохнатый", + "мочь", + "мошенник", + "мощный", + "мрачный", + "мстить", + "мудрый", + "мужчина", + "музыка", + "мука", + "мумия", + "мундир", + "муравей", + "мусор", + "мутный", + "муфта", + "муха", + "мучить", + "мушкетер", + "мыло", + "мысль", + "мыть", + "мычать", + "мышь", + "мэтр", + "мюзикл", + "мягкий", + "мякиш", + "мясо", + "мятый", + "мячик", + "набор", + "навык", + "нагрузка", + "надежда", + "наемный", + "нажать", + "называть", + "наивный", + "накрыть", + "налог", + "намерен", + "наносить", + "написать", + "народ", + "натура", + "наука", + "нация", + "начать", + "небо", + "невеста", + "негодяй", + "неделя", + "нежный", + "незнание", + "нелепый", + "немалый", + "неправда", + "нервный", + "нести", + "нефть", + "нехватка", + "нечистый", + "неясный", + "нива", + "нижний", + "низкий", + "никель", + "нирвана", + "нить", + "ничья", + "ниша", + "нищий", + "новый", + "нога", + "ножницы", + "ноздря", + "ноль", + "номер", + "норма", + "нота", + "ночь", + "ноша", + "ноябрь", + "нрав", + "нужный", + "нутро", + "нынешний", + "нырнуть", + "ныть", + "нюанс", + "нюхать", + "няня", + "оазис", + "обаяние", + "обвинять", + "обгонять", + "обещать", + "обжигать", + "обзор", + "обида", + "область", + "обмен", + "обнимать", + "оборона", + "образ", + "обучение", + "обходить", + "обширный", + "общий", + "объект", + "обычный", + "обязать", + "овальный", + "овес", + "овощи", + "овраг", + "овца", + "овчарка", + "огненный", + "огонь", + "огромный", + "огурец", + "одежда", + "одинокий", + "одобрить", + "ожидать", + "ожог", + "озарение", + "озеро", + "означать", + "оказать", + "океан", + "оклад", + "окно", + "округ", + "октябрь", + "окурок", + "олень", + "опасный", + "операция", + "описать", + "оплата", + "опора", + "оппонент", + "опрос", + "оптимизм", + "опускать", + "опыт", + "орать", + "орбита", + "орган", + "орден", + "орел", + "оригинал", + "оркестр", + "орнамент", + "оружие", + "осадок", + "освещать", + "осень", + "осина", + "осколок", + "осмотр", + "основной", + "особый", + "осуждать", + "отбор", + "отвечать", + "отдать", + "отец", + "отзыв", + "открытие", + "отмечать", + "относить", + "отпуск", + "отрасль", + "отставка", + "оттенок", + "отходить", + "отчет", + "отъезд", + "офицер", + "охапка", + "охота", + "охрана", + "оценка", + "очаг", + "очередь", + "очищать", + "очки", + "ошейник", + "ошибка", + "ощущение", + "павильон", + "падать", + "паек", + "пакет", + "палец", + "память", + "панель", + "папка", + "партия", + "паспорт", + "патрон", + "пауза", + "пафос", + "пахнуть", + "пациент", + "пачка", + "пашня", + "певец", + "педагог", + "пейзаж", + "пельмень", + "пенсия", + "пепел", + "период", + "песня", + "петля", + "пехота", + "печать", + "пешеход", + "пещера", + "пианист", + "пиво", + "пиджак", + "пиковый", + "пилот", + "пионер", + "пирог", + "писать", + "пить", + "пицца", + "пишущий", + "пища", + "план", + "плечо", + "плита", + "плохой", + "плыть", + "плюс", + "пляж", + "победа", + "повод", + "погода", + "подумать", + "поехать", + "пожимать", + "позиция", + "поиск", + "покой", + "получать", + "помнить", + "пони", + "поощрять", + "попадать", + "порядок", + "пост", + "поток", + "похожий", + "поцелуй", + "почва", + "пощечина", + "поэт", + "пояснить", + "право", + "предмет", + "проблема", + "пруд", + "прыгать", + "прямой", + "психолог", + "птица", + "публика", + "пугать", + "пудра", + "пузырь", + "пуля", + "пункт", + "пурга", + "пустой", + "путь", + "пухлый", + "пучок", + "пушистый", + "пчела", + "пшеница", + "пыль", + "пытка", + "пыхтеть", + "пышный", + "пьеса", + "пьяный", + "пятно", + "работа", + "равный", + "радость", + "развитие", + "район", + "ракета", + "рамка", + "ранний", + "рапорт", + "рассказ", + "раунд", + "рация", + "рвать", + "реальный", + "ребенок", + "реветь", + "регион", + "редакция", + "реестр", + "режим", + "резкий", + "рейтинг", + "река", + "религия", + "ремонт", + "рента", + "реплика", + "ресурс", + "реформа", + "рецепт", + "речь", + "решение", + "ржавый", + "рисунок", + "ритм", + "рифма", + "робкий", + "ровный", + "рогатый", + "родитель", + "рождение", + "розовый", + "роковой", + "роль", + "роман", + "ронять", + "рост", + "рота", + "роща", + "рояль", + "рубль", + "ругать", + "руда", + "ружье", + "руины", + "рука", + "руль", + "румяный", + "русский", + "ручка", + "рыба", + "рывок", + "рыдать", + "рыжий", + "рынок", + "рысь", + "рыть", + "рыхлый", + "рыцарь", + "рычаг", + "рюкзак", + "рюмка", + "рябой", + "рядовой", + "сабля", + "садовый", + "сажать", + "салон", + "самолет", + "сани", + "сапог", + "сарай", + "сатира", + "сауна", + "сахар", + "сбегать", + "сбивать", + "сбор", + "сбыт", + "свадьба", + "свет", + "свидание", + "свобода", + "связь", + "сгорать", + "сдвигать", + "сеанс", + "северный", + "сегмент", + "седой", + "сезон", + "сейф", + "секунда", + "сельский", + "семья", + "сентябрь", + "сердце", + "сеть", + "сечение", + "сеять", + "сигнал", + "сидеть", + "сизый", + "сила", + "символ", + "синий", + "сирота", + "система", + "ситуация", + "сиять", + "сказать", + "скважина", + "скелет", + "скидка", + "склад", + "скорый", + "скрывать", + "скучный", + "слава", + "слеза", + "слияние", + "слово", + "случай", + "слышать", + "слюна", + "смех", + "смирение", + "смотреть", + "смутный", + "смысл", + "смятение", + "снаряд", + "снег", + "снижение", + "сносить", + "снять", + "событие", + "совет", + "согласие", + "сожалеть", + "сойти", + "сокол", + "солнце", + "сомнение", + "сонный", + "сообщать", + "соперник", + "сорт", + "состав", + "сотня", + "соус", + "социолог", + "сочинять", + "союз", + "спать", + "спешить", + "спина", + "сплошной", + "способ", + "спутник", + "средство", + "срок", + "срывать", + "стать", + "ствол", + "стена", + "стихи", + "сторона", + "страна", + "студент", + "стыд", + "субъект", + "сувенир", + "сугроб", + "судьба", + "суета", + "суждение", + "сукно", + "сулить", + "сумма", + "сунуть", + "супруг", + "суровый", + "сустав", + "суть", + "сухой", + "суша", + "существо", + "сфера", + "схема", + "сцена", + "счастье", + "счет", + "считать", + "сшивать", + "съезд", + "сынок", + "сыпать", + "сырье", + "сытый", + "сыщик", + "сюжет", + "сюрприз", + "таблица", + "таежный", + "таинство", + "тайна", + "такси", + "талант", + "таможня", + "танец", + "тарелка", + "таскать", + "тахта", + "тачка", + "таять", + "тварь", + "твердый", + "творить", + "театр", + "тезис", + "текст", + "тело", + "тема", + "тень", + "теория", + "теплый", + "терять", + "тесный", + "тетя", + "техника", + "течение", + "тигр", + "типичный", + "тираж", + "титул", + "тихий", + "тишина", + "ткань", + "товарищ", + "толпа", + "тонкий", + "топливо", + "торговля", + "тоска", + "точка", + "тощий", + "традиция", + "тревога", + "трибуна", + "трогать", + "труд", + "трюк", + "тряпка", + "туалет", + "тугой", + "туловище", + "туман", + "тундра", + "тупой", + "турнир", + "тусклый", + "туфля", + "туча", + "туша", + "тыкать", + "тысяча", + "тьма", + "тюльпан", + "тюрьма", + "тяга", + "тяжелый", + "тянуть", + "убеждать", + "убирать", + "убогий", + "убыток", + "уважение", + "уверять", + "увлекать", + "угнать", + "угол", + "угроза", + "удар", + "удивлять", + "удобный", + "уезд", + "ужас", + "ужин", + "узел", + "узкий", + "узнавать", + "узор", + "уйма", + "уклон", + "укол", + "уксус", + "улетать", + "улица", + "улучшать", + "улыбка", + "уметь", + "умиление", + "умный", + "умолять", + "умысел", + "унижать", + "уносить", + "уныние", + "упасть", + "уплата", + "упор", + "упрекать", + "упускать", + "уран", + "урна", + "уровень", + "усадьба", + "усердие", + "усилие", + "ускорять", + "условие", + "усмешка", + "уснуть", + "успеть", + "усыпать", + "утешать", + "утка", + "уточнять", + "утро", + "утюг", + "уходить", + "уцелеть", + "участие", + "ученый", + "учитель", + "ушко", + "ущерб", + "уютный", + "уяснять", + "фабрика", + "фаворит", + "фаза", + "файл", + "факт", + "фамилия", + "фантазия", + "фара", + "фасад", + "февраль", + "фельдшер", + "феномен", + "ферма", + "фигура", + "физика", + "фильм", + "финал", + "фирма", + "фишка", + "флаг", + "флейта", + "флот", + "фокус", + "фольклор", + "фонд", + "форма", + "фото", + "фраза", + "фреска", + "фронт", + "фрукт", + "функция", + "фуражка", + "футбол", + "фыркать", + "халат", + "хамство", + "хаос", + "характер", + "хата", + "хватать", + "хвост", + "хижина", + "хилый", + "химия", + "хирург", + "хитрый", + "хищник", + "хлам", + "хлеб", + "хлопать", + "хмурый", + "ходить", + "хозяин", + "хоккей", + "холодный", + "хороший", + "хотеть", + "хохотать", + "храм", + "хрен", + "хриплый", + "хроника", + "хрупкий", + "художник", + "хулиган", + "хутор", + "царь", + "цвет", + "цель", + "цемент", + "центр", + "цепь", + "церковь", + "цикл", + "цилиндр", + "циничный", + "цирк", + "цистерна", + "цитата", + "цифра", + "цыпленок", + "чадо", + "чайник", + "часть", + "чашка", + "человек", + "чемодан", + "чепуха", + "черный", + "честь", + "четкий", + "чехол", + "чиновник", + "число", + "читать", + "членство", + "чреватый", + "чтение", + "чувство", + "чугунный", + "чудо", + "чужой", + "чукча", + "чулок", + "чума", + "чуткий", + "чучело", + "чушь", + "шаблон", + "шагать", + "шайка", + "шакал", + "шалаш", + "шампунь", + "шанс", + "шапка", + "шарик", + "шасси", + "шатер", + "шахта", + "шашлык", + "швейный", + "швырять", + "шевелить", + "шедевр", + "шейка", + "шелковый", + "шептать", + "шерсть", + "шестерка", + "шикарный", + "шинель", + "шипеть", + "широкий", + "шить", + "шишка", + "шкаф", + "школа", + "шкура", + "шланг", + "шлем", + "шлюпка", + "шляпа", + "шнур", + "шоколад", + "шорох", + "шоссе", + "шофер", + "шпага", + "шпион", + "шприц", + "шрам", + "шрифт", + "штаб", + "штора", + "штраф", + "штука", + "штык", + "шуба", + "шуметь", + "шуршать", + "шутка", + "щадить", + "щедрый", + "щека", + "щель", + "щенок", + "щепка", + "щетка", + "щука", + "эволюция", + "эгоизм", + "экзамен", + "экипаж", + "экономия", + "экран", + "эксперт", + "элемент", + "элита", + "эмблема", + "эмигрант", + "эмоция", + "энергия", + "эпизод", + "эпоха", + "эскиз", + "эссе", + "эстрада", + "этап", + "этика", + "этюд", + "эфир", + "эффект", + "эшелон", + "юбилей", + "юбка", + "южный", + "юмор", + "юноша", + "юрист", + "яблоко", + "явление", + "ягода", + "ядерный", + "ядовитый", + "ядро", + "язва", + "язык", + "яйцо", + "якорь", + "январь", + "японец", + "яркий", + "ярмарка", + "ярость", + "ярус", + "ясный", + "яхта", + "ячейка", + "ящик" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/mnemonics/spanish.dart b/cw_haven/lib/mnemonics/spanish.dart new file mode 100644 index 000000000..531eafd35 --- /dev/null +++ b/cw_haven/lib/mnemonics/spanish.dart @@ -0,0 +1,1630 @@ +class SpanishMnemonics { + static const words = [ + "ábaco", + "abdomen", + "abeja", + "abierto", + "abogado", + "abono", + "aborto", + "abrazo", + "abrir", + "abuelo", + "abuso", + "acabar", + "academia", + "acceso", + "acción", + "aceite", + "acelga", + "acento", + "aceptar", + "ácido", + "aclarar", + "acné", + "acoger", + "acoso", + "activo", + "acto", + "actriz", + "actuar", + "acudir", + "acuerdo", + "acusar", + "adicto", + "admitir", + "adoptar", + "adorno", + "aduana", + "adulto", + "aéreo", + "afectar", + "afición", + "afinar", + "afirmar", + "ágil", + "agitar", + "agonía", + "agosto", + "agotar", + "agregar", + "agrio", + "agua", + "agudo", + "águila", + "aguja", + "ahogo", + "ahorro", + "aire", + "aislar", + "ajedrez", + "ajeno", + "ajuste", + "alacrán", + "alambre", + "alarma", + "alba", + "álbum", + "alcalde", + "aldea", + "alegre", + "alejar", + "alerta", + "aleta", + "alfiler", + "alga", + "algodón", + "aliado", + "aliento", + "alivio", + "alma", + "almeja", + "almíbar", + "altar", + "alteza", + "altivo", + "alto", + "altura", + "alumno", + "alzar", + "amable", + "amante", + "amapola", + "amargo", + "amasar", + "ámbar", + "ámbito", + "ameno", + "amigo", + "amistad", + "amor", + "amparo", + "amplio", + "ancho", + "anciano", + "ancla", + "andar", + "andén", + "anemia", + "ángulo", + "anillo", + "ánimo", + "anís", + "anotar", + "antena", + "antiguo", + "antojo", + "anual", + "anular", + "anuncio", + "añadir", + "añejo", + "año", + "apagar", + "aparato", + "apetito", + "apio", + "aplicar", + "apodo", + "aporte", + "apoyo", + "aprender", + "aprobar", + "apuesta", + "apuro", + "arado", + "araña", + "arar", + "árbitro", + "árbol", + "arbusto", + "archivo", + "arco", + "arder", + "ardilla", + "arduo", + "área", + "árido", + "aries", + "armonía", + "arnés", + "aroma", + "arpa", + "arpón", + "arreglo", + "arroz", + "arruga", + "arte", + "artista", + "asa", + "asado", + "asalto", + "ascenso", + "asegurar", + "aseo", + "asesor", + "asiento", + "asilo", + "asistir", + "asno", + "asombro", + "áspero", + "astilla", + "astro", + "astuto", + "asumir", + "asunto", + "atajo", + "ataque", + "atar", + "atento", + "ateo", + "ático", + "atleta", + "átomo", + "atraer", + "atroz", + "atún", + "audaz", + "audio", + "auge", + "aula", + "aumento", + "ausente", + "autor", + "aval", + "avance", + "avaro", + "ave", + "avellana", + "avena", + "avestruz", + "avión", + "aviso", + "ayer", + "ayuda", + "ayuno", + "azafrán", + "azar", + "azote", + "azúcar", + "azufre", + "azul", + "baba", + "babor", + "bache", + "bahía", + "baile", + "bajar", + "balanza", + "balcón", + "balde", + "bambú", + "banco", + "banda", + "baño", + "barba", + "barco", + "barniz", + "barro", + "báscula", + "bastón", + "basura", + "batalla", + "batería", + "batir", + "batuta", + "baúl", + "bazar", + "bebé", + "bebida", + "bello", + "besar", + "beso", + "bestia", + "bicho", + "bien", + "bingo", + "blanco", + "bloque", + "blusa", + "boa", + "bobina", + "bobo", + "boca", + "bocina", + "boda", + "bodega", + "boina", + "bola", + "bolero", + "bolsa", + "bomba", + "bondad", + "bonito", + "bono", + "bonsái", + "borde", + "borrar", + "bosque", + "bote", + "botín", + "bóveda", + "bozal", + "bravo", + "brazo", + "brecha", + "breve", + "brillo", + "brinco", + "brisa", + "broca", + "broma", + "bronce", + "brote", + "bruja", + "brusco", + "bruto", + "buceo", + "bucle", + "bueno", + "buey", + "bufanda", + "bufón", + "búho", + "buitre", + "bulto", + "burbuja", + "burla", + "burro", + "buscar", + "butaca", + "buzón", + "caballo", + "cabeza", + "cabina", + "cabra", + "cacao", + "cadáver", + "cadena", + "caer", + "café", + "caída", + "caimán", + "caja", + "cajón", + "cal", + "calamar", + "calcio", + "caldo", + "calidad", + "calle", + "calma", + "calor", + "calvo", + "cama", + "cambio", + "camello", + "camino", + "campo", + "cáncer", + "candil", + "canela", + "canguro", + "canica", + "canto", + "caña", + "cañón", + "caoba", + "caos", + "capaz", + "capitán", + "capote", + "captar", + "capucha", + "cara", + "carbón", + "cárcel", + "careta", + "carga", + "cariño", + "carne", + "carpeta", + "carro", + "carta", + "casa", + "casco", + "casero", + "caspa", + "castor", + "catorce", + "catre", + "caudal", + "causa", + "cazo", + "cebolla", + "ceder", + "cedro", + "celda", + "célebre", + "celoso", + "célula", + "cemento", + "ceniza", + "centro", + "cerca", + "cerdo", + "cereza", + "cero", + "cerrar", + "certeza", + "césped", + "cetro", + "chacal", + "chaleco", + "champú", + "chancla", + "chapa", + "charla", + "chico", + "chiste", + "chivo", + "choque", + "choza", + "chuleta", + "chupar", + "ciclón", + "ciego", + "cielo", + "cien", + "cierto", + "cifra", + "cigarro", + "cima", + "cinco", + "cine", + "cinta", + "ciprés", + "circo", + "ciruela", + "cisne", + "cita", + "ciudad", + "clamor", + "clan", + "claro", + "clase", + "clave", + "cliente", + "clima", + "clínica", + "cobre", + "cocción", + "cochino", + "cocina", + "coco", + "código", + "codo", + "cofre", + "coger", + "cohete", + "cojín", + "cojo", + "cola", + "colcha", + "colegio", + "colgar", + "colina", + "collar", + "colmo", + "columna", + "combate", + "comer", + "comida", + "cómodo", + "compra", + "conde", + "conejo", + "conga", + "conocer", + "consejo", + "contar", + "copa", + "copia", + "corazón", + "corbata", + "corcho", + "cordón", + "corona", + "correr", + "coser", + "cosmos", + "costa", + "cráneo", + "cráter", + "crear", + "crecer", + "creído", + "crema", + "cría", + "crimen", + "cripta", + "crisis", + "cromo", + "crónica", + "croqueta", + "crudo", + "cruz", + "cuadro", + "cuarto", + "cuatro", + "cubo", + "cubrir", + "cuchara", + "cuello", + "cuento", + "cuerda", + "cuesta", + "cueva", + "cuidar", + "culebra", + "culpa", + "culto", + "cumbre", + "cumplir", + "cuna", + "cuneta", + "cuota", + "cupón", + "cúpula", + "curar", + "curioso", + "curso", + "curva", + "cutis", + "dama", + "danza", + "dar", + "dardo", + "dátil", + "deber", + "débil", + "década", + "decir", + "dedo", + "defensa", + "definir", + "dejar", + "delfín", + "delgado", + "delito", + "demora", + "denso", + "dental", + "deporte", + "derecho", + "derrota", + "desayuno", + "deseo", + "desfile", + "desnudo", + "destino", + "desvío", + "detalle", + "detener", + "deuda", + "día", + "diablo", + "diadema", + "diamante", + "diana", + "diario", + "dibujo", + "dictar", + "diente", + "dieta", + "diez", + "difícil", + "digno", + "dilema", + "diluir", + "dinero", + "directo", + "dirigir", + "disco", + "diseño", + "disfraz", + "diva", + "divino", + "doble", + "doce", + "dolor", + "domingo", + "don", + "donar", + "dorado", + "dormir", + "dorso", + "dos", + "dosis", + "dragón", + "droga", + "ducha", + "duda", + "duelo", + "dueño", + "dulce", + "dúo", + "duque", + "durar", + "dureza", + "duro", + "ébano", + "ebrio", + "echar", + "eco", + "ecuador", + "edad", + "edición", + "edificio", + "editor", + "educar", + "efecto", + "eficaz", + "eje", + "ejemplo", + "elefante", + "elegir", + "elemento", + "elevar", + "elipse", + "élite", + "elixir", + "elogio", + "eludir", + "embudo", + "emitir", + "emoción", + "empate", + "empeño", + "empleo", + "empresa", + "enano", + "encargo", + "enchufe", + "encía", + "enemigo", + "enero", + "enfado", + "enfermo", + "engaño", + "enigma", + "enlace", + "enorme", + "enredo", + "ensayo", + "enseñar", + "entero", + "entrar", + "envase", + "envío", + "época", + "equipo", + "erizo", + "escala", + "escena", + "escolar", + "escribir", + "escudo", + "esencia", + "esfera", + "esfuerzo", + "espada", + "espejo", + "espía", + "esposa", + "espuma", + "esquí", + "estar", + "este", + "estilo", + "estufa", + "etapa", + "eterno", + "ética", + "etnia", + "evadir", + "evaluar", + "evento", + "evitar", + "exacto", + "examen", + "exceso", + "excusa", + "exento", + "exigir", + "exilio", + "existir", + "éxito", + "experto", + "explicar", + "exponer", + "extremo", + "fábrica", + "fábula", + "fachada", + "fácil", + "factor", + "faena", + "faja", + "falda", + "fallo", + "falso", + "faltar", + "fama", + "familia", + "famoso", + "faraón", + "farmacia", + "farol", + "farsa", + "fase", + "fatiga", + "fauna", + "favor", + "fax", + "febrero", + "fecha", + "feliz", + "feo", + "feria", + "feroz", + "fértil", + "fervor", + "festín", + "fiable", + "fianza", + "fiar", + "fibra", + "ficción", + "ficha", + "fideo", + "fiebre", + "fiel", + "fiera", + "fiesta", + "figura", + "fijar", + "fijo", + "fila", + "filete", + "filial", + "filtro", + "fin", + "finca", + "fingir", + "finito", + "firma", + "flaco", + "flauta", + "flecha", + "flor", + "flota", + "fluir", + "flujo", + "flúor", + "fobia", + "foca", + "fogata", + "fogón", + "folio", + "folleto", + "fondo", + "forma", + "forro", + "fortuna", + "forzar", + "fosa", + "foto", + "fracaso", + "frágil", + "franja", + "frase", + "fraude", + "freír", + "freno", + "fresa", + "frío", + "frito", + "fruta", + "fuego", + "fuente", + "fuerza", + "fuga", + "fumar", + "función", + "funda", + "furgón", + "furia", + "fusil", + "fútbol", + "futuro", + "gacela", + "gafas", + "gaita", + "gajo", + "gala", + "galería", + "gallo", + "gamba", + "ganar", + "gancho", + "ganga", + "ganso", + "garaje", + "garza", + "gasolina", + "gastar", + "gato", + "gavilán", + "gemelo", + "gemir", + "gen", + "género", + "genio", + "gente", + "geranio", + "gerente", + "germen", + "gesto", + "gigante", + "gimnasio", + "girar", + "giro", + "glaciar", + "globo", + "gloria", + "gol", + "golfo", + "goloso", + "golpe", + "goma", + "gordo", + "gorila", + "gorra", + "gota", + "goteo", + "gozar", + "grada", + "gráfico", + "grano", + "grasa", + "gratis", + "grave", + "grieta", + "grillo", + "gripe", + "gris", + "grito", + "grosor", + "grúa", + "grueso", + "grumo", + "grupo", + "guante", + "guapo", + "guardia", + "guerra", + "guía", + "guiño", + "guion", + "guiso", + "guitarra", + "gusano", + "gustar", + "haber", + "hábil", + "hablar", + "hacer", + "hacha", + "hada", + "hallar", + "hamaca", + "harina", + "haz", + "hazaña", + "hebilla", + "hebra", + "hecho", + "helado", + "helio", + "hembra", + "herir", + "hermano", + "héroe", + "hervir", + "hielo", + "hierro", + "hígado", + "higiene", + "hijo", + "himno", + "historia", + "hocico", + "hogar", + "hoguera", + "hoja", + "hombre", + "hongo", + "honor", + "honra", + "hora", + "hormiga", + "horno", + "hostil", + "hoyo", + "hueco", + "huelga", + "huerta", + "hueso", + "huevo", + "huida", + "huir", + "humano", + "húmedo", + "humilde", + "humo", + "hundir", + "huracán", + "hurto", + "icono", + "ideal", + "idioma", + "ídolo", + "iglesia", + "iglú", + "igual", + "ilegal", + "ilusión", + "imagen", + "imán", + "imitar", + "impar", + "imperio", + "imponer", + "impulso", + "incapaz", + "índice", + "inerte", + "infiel", + "informe", + "ingenio", + "inicio", + "inmenso", + "inmune", + "innato", + "insecto", + "instante", + "interés", + "íntimo", + "intuir", + "inútil", + "invierno", + "ira", + "iris", + "ironía", + "isla", + "islote", + "jabalí", + "jabón", + "jamón", + "jarabe", + "jardín", + "jarra", + "jaula", + "jazmín", + "jefe", + "jeringa", + "jinete", + "jornada", + "joroba", + "joven", + "joya", + "juerga", + "jueves", + "juez", + "jugador", + "jugo", + "juguete", + "juicio", + "junco", + "jungla", + "junio", + "juntar", + "júpiter", + "jurar", + "justo", + "juvenil", + "juzgar", + "kilo", + "koala", + "labio", + "lacio", + "lacra", + "lado", + "ladrón", + "lagarto", + "lágrima", + "laguna", + "laico", + "lamer", + "lámina", + "lámpara", + "lana", + "lancha", + "langosta", + "lanza", + "lápiz", + "largo", + "larva", + "lástima", + "lata", + "látex", + "latir", + "laurel", + "lavar", + "lazo", + "leal", + "lección", + "leche", + "lector", + "leer", + "legión", + "legumbre", + "lejano", + "lengua", + "lento", + "leña", + "león", + "leopardo", + "lesión", + "letal", + "letra", + "leve", + "leyenda", + "libertad", + "libro", + "licor", + "líder", + "lidiar", + "lienzo", + "liga", + "ligero", + "lima", + "límite", + "limón", + "limpio", + "lince", + "lindo", + "línea", + "lingote", + "lino", + "linterna", + "líquido", + "liso", + "lista", + "litera", + "litio", + "litro", + "llaga", + "llama", + "llanto", + "llave", + "llegar", + "llenar", + "llevar", + "llorar", + "llover", + "lluvia", + "lobo", + "loción", + "loco", + "locura", + "lógica", + "logro", + "lombriz", + "lomo", + "lonja", + "lote", + "lucha", + "lucir", + "lugar", + "lujo", + "luna", + "lunes", + "lupa", + "lustro", + "luto", + "luz", + "maceta", + "macho", + "madera", + "madre", + "maduro", + "maestro", + "mafia", + "magia", + "mago", + "maíz", + "maldad", + "maleta", + "malla", + "malo", + "mamá", + "mambo", + "mamut", + "manco", + "mando", + "manejar", + "manga", + "maniquí", + "manjar", + "mano", + "manso", + "manta", + "mañana", + "mapa", + "máquina", + "mar", + "marco", + "marea", + "marfil", + "margen", + "marido", + "mármol", + "marrón", + "martes", + "marzo", + "masa", + "máscara", + "masivo", + "matar", + "materia", + "matiz", + "matriz", + "máximo", + "mayor", + "mazorca", + "mecha", + "medalla", + "medio", + "médula", + "mejilla", + "mejor", + "melena", + "melón", + "memoria", + "menor", + "mensaje", + "mente", + "menú", + "mercado", + "merengue", + "mérito", + "mes", + "mesón", + "meta", + "meter", + "método", + "metro", + "mezcla", + "miedo", + "miel", + "miembro", + "miga", + "mil", + "milagro", + "militar", + "millón", + "mimo", + "mina", + "minero", + "mínimo", + "minuto", + "miope", + "mirar", + "misa", + "miseria", + "misil", + "mismo", + "mitad", + "mito", + "mochila", + "moción", + "moda", + "modelo", + "moho", + "mojar", + "molde", + "moler", + "molino", + "momento", + "momia", + "monarca", + "moneda", + "monja", + "monto", + "moño", + "morada", + "morder", + "moreno", + "morir", + "morro", + "morsa", + "mortal", + "mosca", + "mostrar", + "motivo", + "mover", + "móvil", + "mozo", + "mucho", + "mudar", + "mueble", + "muela", + "muerte", + "muestra", + "mugre", + "mujer", + "mula", + "muleta", + "multa", + "mundo", + "muñeca", + "mural", + "muro", + "músculo", + "museo", + "musgo", + "música", + "muslo", + "nácar", + "nación", + "nadar", + "naipe", + "naranja", + "nariz", + "narrar", + "nasal", + "natal", + "nativo", + "natural", + "náusea", + "naval", + "nave", + "navidad", + "necio", + "néctar", + "negar", + "negocio", + "negro", + "neón", + "nervio", + "neto", + "neutro", + "nevar", + "nevera", + "nicho", + "nido", + "niebla", + "nieto", + "niñez", + "niño", + "nítido", + "nivel", + "nobleza", + "noche", + "nómina", + "noria", + "norma", + "norte", + "nota", + "noticia", + "novato", + "novela", + "novio", + "nube", + "nuca", + "núcleo", + "nudillo", + "nudo", + "nuera", + "nueve", + "nuez", + "nulo", + "número", + "nutria", + "oasis", + "obeso", + "obispo", + "objeto", + "obra", + "obrero", + "observar", + "obtener", + "obvio", + "oca", + "ocaso", + "océano", + "ochenta", + "ocho", + "ocio", + "ocre", + "octavo", + "octubre", + "oculto", + "ocupar", + "ocurrir", + "odiar", + "odio", + "odisea", + "oeste", + "ofensa", + "oferta", + "oficio", + "ofrecer", + "ogro", + "oído", + "oír", + "ojo", + "ola", + "oleada", + "olfato", + "olivo", + "olla", + "olmo", + "olor", + "olvido", + "ombligo", + "onda", + "onza", + "opaco", + "opción", + "ópera", + "opinar", + "oponer", + "optar", + "óptica", + "opuesto", + "oración", + "orador", + "oral", + "órbita", + "orca", + "orden", + "oreja", + "órgano", + "orgía", + "orgullo", + "oriente", + "origen", + "orilla", + "oro", + "orquesta", + "oruga", + "osadía", + "oscuro", + "osezno", + "oso", + "ostra", + "otoño", + "otro", + "oveja", + "óvulo", + "óxido", + "oxígeno", + "oyente", + "ozono", + "pacto", + "padre", + "paella", + "página", + "pago", + "país", + "pájaro", + "palabra", + "palco", + "paleta", + "pálido", + "palma", + "paloma", + "palpar", + "pan", + "panal", + "pánico", + "pantera", + "pañuelo", + "papá", + "papel", + "papilla", + "paquete", + "parar", + "parcela", + "pared", + "parir", + "paro", + "párpado", + "parque", + "párrafo", + "parte", + "pasar", + "paseo", + "pasión", + "paso", + "pasta", + "pata", + "patio", + "patria", + "pausa", + "pauta", + "pavo", + "payaso", + "peatón", + "pecado", + "pecera", + "pecho", + "pedal", + "pedir", + "pegar", + "peine", + "pelar", + "peldaño", + "pelea", + "peligro", + "pellejo", + "pelo", + "peluca", + "pena", + "pensar", + "peñón", + "peón", + "peor", + "pepino", + "pequeño", + "pera", + "percha", + "perder", + "pereza", + "perfil", + "perico", + "perla", + "permiso", + "perro", + "persona", + "pesa", + "pesca", + "pésimo", + "pestaña", + "pétalo", + "petróleo", + "pez", + "pezuña", + "picar", + "pichón", + "pie", + "piedra", + "pierna", + "pieza", + "pijama", + "pilar", + "piloto", + "pimienta", + "pino", + "pintor", + "pinza", + "piña", + "piojo", + "pipa", + "pirata", + "pisar", + "piscina", + "piso", + "pista", + "pitón", + "pizca", + "placa", + "plan", + "plata", + "playa", + "plaza", + "pleito", + "pleno", + "plomo", + "pluma", + "plural", + "pobre", + "poco", + "poder", + "podio", + "poema", + "poesía", + "poeta", + "polen", + "policía", + "pollo", + "polvo", + "pomada", + "pomelo", + "pomo", + "pompa", + "poner", + "porción", + "portal", + "posada", + "poseer", + "posible", + "poste", + "potencia", + "potro", + "pozo", + "prado", + "precoz", + "pregunta", + "premio", + "prensa", + "preso", + "previo", + "primo", + "príncipe", + "prisión", + "privar", + "proa", + "probar", + "proceso", + "producto", + "proeza", + "profesor", + "programa", + "prole", + "promesa", + "pronto", + "propio", + "próximo", + "prueba", + "público", + "puchero", + "pudor", + "pueblo", + "puerta", + "puesto", + "pulga", + "pulir", + "pulmón", + "pulpo", + "pulso", + "puma", + "punto", + "puñal", + "puño", + "pupa", + "pupila", + "puré", + "quedar", + "queja", + "quemar", + "querer", + "queso", + "quieto", + "química", + "quince", + "quitar", + "rábano", + "rabia", + "rabo", + "ración", + "radical", + "raíz", + "rama", + "rampa", + "rancho", + "rango", + "rapaz", + "rápido", + "rapto", + "rasgo", + "raspa", + "rato", + "rayo", + "raza", + "razón", + "reacción", + "realidad", + "rebaño", + "rebote", + "recaer", + "receta", + "rechazo", + "recoger", + "recreo", + "recto", + "recurso", + "red", + "redondo", + "reducir", + "reflejo", + "reforma", + "refrán", + "refugio", + "regalo", + "regir", + "regla", + "regreso", + "rehén", + "reino", + "reír", + "reja", + "relato", + "relevo", + "relieve", + "relleno", + "reloj", + "remar", + "remedio", + "remo", + "rencor", + "rendir", + "renta", + "reparto", + "repetir", + "reposo", + "reptil", + "res", + "rescate", + "resina", + "respeto", + "resto", + "resumen", + "retiro", + "retorno", + "retrato", + "reunir", + "revés", + "revista", + "rey", + "rezar", + "rico", + "riego", + "rienda", + "riesgo", + "rifa", + "rígido", + "rigor", + "rincón", + "riñón", + "río", + "riqueza", + "risa", + "ritmo", + "rito" + ]; +} \ No newline at end of file diff --git a/cw_haven/lib/pending_haven_transaction.dart b/cw_haven/lib/pending_haven_transaction.dart new file mode 100644 index 000000000..7a8c6acc5 --- /dev/null +++ b/cw_haven/lib/pending_haven_transaction.dart @@ -0,0 +1,48 @@ +import 'package:cw_haven/api/structs/pending_transaction.dart'; +import 'package:cw_haven/api/transaction_history.dart' + as haven_transaction_history; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/core/amount_converter.dart'; +import 'package:cw_core/pending_transaction.dart'; + +class DoubleSpendException implements Exception { + DoubleSpendException(); + + @override + String toString() => + 'This transaction cannot be committed. This can be due to many reasons including the wallet not being synced, there is not enough XMR in your available balance, or previous transactions are not yet fully processed.'; +} + +class PendingHavenTransaction with PendingTransaction { + PendingHavenTransaction(this.pendingTransactionDescription, this.cryptoCurrency); + + final PendingTransactionDescription pendingTransactionDescription; + final CryptoCurrency cryptoCurrency; + + @override + String get id => pendingTransactionDescription.hash; + + @override + String get amountFormatted => AmountConverter.amountIntToString( + cryptoCurrency, pendingTransactionDescription.amount); + + @override + String get feeFormatted => AmountConverter.amountIntToString( + cryptoCurrency, pendingTransactionDescription.fee); + + @override + Future commit() async { + try { + haven_transaction_history.commitTransactionFromPointerAddress( + address: pendingTransactionDescription.pointerAddress); + } catch (e) { + final message = e.toString(); + + if (message.contains('Reason: double spend')) { + throw DoubleSpendException(); + } + + rethrow; + } + } +} diff --git a/cw_haven/lib/update_haven_rate.dart b/cw_haven/lib/update_haven_rate.dart new file mode 100644 index 000000000..967247af5 --- /dev/null +++ b/cw_haven/lib/update_haven_rate.dart @@ -0,0 +1,15 @@ +//import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_haven/balance_list.dart'; + +//Future updateHavenRate(FiatConversionStore fiatConversionStore) async { +// final rate = getRate(); +// final base = rate.firstWhere((row) => row.getAssetType() == 'XUSD', orElse: () => null); +// rate.forEach((row) { +// final cur = CryptoCurrency.fromString(row.getAssetType()); +// final baseRate = moneroAmountToDouble(amount: base.getRate()); +// final rowRate = moneroAmountToDouble(amount: row.getRate()); +// fiatConversionStore.prices[cur] = baseRate * rowRate; +// }); +//} \ No newline at end of file diff --git a/cw_haven/pubspec.lock b/cw_haven/pubspec.lock new file mode 100644 index 000000000..37e6b72b7 --- /dev/null +++ b/cw_haven/pubspec.lock @@ -0,0 +1,609 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "14.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.41.2" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.6" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.10" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.5" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.10" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.4" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.7.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + cw_monero: + dependency: "direct main" + description: + path: "../cw_monero" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.12" + dartx: + dependency: transitive + description: + name: dartx + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + encrypt: + dependency: transitive + description: + name: encrypt + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + ffi: + dependency: "direct main" + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0+2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + hive: + dependency: transitive + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4+1" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.2" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + mobx: + dependency: "direct main" + description: + name: mobx + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1+4" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.28" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+8" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.8" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4+1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.10+3" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.19" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1+3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.4+1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml new file mode 100644 index 000000000..a8d8417be --- /dev/null +++ b/cw_haven/pubspec.yaml @@ -0,0 +1,78 @@ +name: cw_haven +description: A new flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + ffi: ^0.1.3 + path_provider: ^1.4.0 + http: ^0.12.0+2 + mobx: ^1.2.1+2 + flutter_mobx: ^1.1.0+2 + intl: ^0.17.0 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^1.10.3 + build_resolvers: ^1.3.10 + mobx_codegen: ^1.1.0+1 + hive_generator: ^0.8.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.cw_haven + pluginClass: CwHavenPlugin + ios: + pluginClass: CwHavenPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/android/CMakeLists.txt b/cw_monero/android/CMakeLists.txt index 599a87f13..49acfcdad 100644 --- a/cw_monero/android/CMakeLists.txt +++ b/cw_monero/android/CMakeLists.txt @@ -7,8 +7,6 @@ add_library( cw_monero find_library( log-lib log ) -set(CMAKE_BUILD_TYPE Debug) - set(EXTERNAL_LIBS_DIR ${CMAKE_SOURCE_DIR}/../ios/External/android) ############ diff --git a/cw_monero/android/build.gradle b/cw_monero/android/build.gradle index 4d5a6dc5c..1d7ae93d8 100644 --- a/cw_monero/android/build.gradle +++ b/cw_monero/android/build.gradle @@ -38,12 +38,10 @@ android { disable 'InvalidPackage' } externalNativeBuild { - // Encapsulates your CMake build configurations. - cmake { - // Provides a relative path to your CMake build script. - path "CMakeLists.txt" + cmake { + path "CMakeLists.txt" + } } - } } dependencies { diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec index 7224073b3..8262427d8 100644 --- a/cw_monero/ios/cw_monero.podspec +++ b/cw_monero/ios/cw_monero.podspec @@ -12,43 +12,44 @@ Pod::Spec.new do |s| s.author = { 'CakeWallet' => 'support@cakewallet.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/ios/libs/monero/include/src/**/*.h, External/ios/libs/monero/include/contrib/**/*.h, External/ios/libs/monero/include/External/ios/**/*.h' + s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/ios/libs/monero/include/External/ios/**/*.h' s.dependency 'Flutter' + s.dependency 'cw_shared_external' s.platform = :ios, '10.0' s.swift_version = '4.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } s.subspec 'OpenSSL' do |openssl| - openssl.preserve_paths = 'External/ios/include/*.h' - openssl.vendored_libraries = 'External/ios/lib/libcrypto.a', 'External/ios/lib/libssl.a' + openssl.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' + openssl.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libcrypto.a', '../../../../../cw_shared_external/ios/External/ios/lib/libssl.a' openssl.libraries = 'ssl', 'crypto' openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } end - s.subspec 'Monero' do |monero| - monero.preserve_paths = 'External/ios/include/**/*.h' - monero.vendored_libraries = 'External/ios/lib/libeasylogging.a', 'External/ios/lib/libepee.a', 'External/ios/lib/liblmdb.a', 'External/ios/lib/librandomx.a', 'External/ios/lib/libunbound.a', 'External/ios/lib/libwallet_merged.a', 'libcryptonote_basic.a', 'libcryptonote_format_utils_basic.a' - monero.libraries = 'easylogging', 'epee', 'unbound', 'wallet_merged', 'lmdb', 'randomx', 'cryptonote_basic', 'cryptonote_format_utils_basic' - monero.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include" } - end - - s.subspec 'Boost' do |boost| - boost.preserve_paths = 'External/ios/include/**/*.h', 'External/ios/include/**/*.h' - boost.vendored_libraries = 'External/ios/lib/libboost_chrono.a', 'External/ios/lib/libboost_date_time.a', 'External/ios/lib/libboost_filesystem.a', 'External/ios/lib/libboost_graph.a', 'External/ios/lib/libboost_locale.a', 'External/ios/lib/libboost_program_options.a', 'External/ios/lib/libboost_random.a', 'External/ios/lib/libboost_regex.a', 'External/ios/lib/libboost_serialization.a', 'External/ios/lib/libboost_system.a', 'External/ios/lib/libboost_thread.a', 'External/ios/lib/libboost_wserialization.a' - boost.libraries = 'boost_wserialization', 'boost_thread', 'boost_system', 'boost_serialization', 'boost_regex', 'boost_random', 'boost_program_options', 'boost_locale', 'boost_graph', 'boost_filesystem', 'boost_date_time', 'boost_chrono' - boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } - end - s.subspec 'Sodium' do |sodium| - sodium.preserve_paths = 'External/ios/include/**/*.h' - sodium.vendored_libraries = 'External/ios/lib/libsodium.a' + sodium.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h' + sodium.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libsodium.a' sodium.libraries = 'sodium' sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } end - s.subspec 'lmdb' do |lmdb| - lmdb.vendored_libraries = 'External/ios/lib/liblmdb.a' - lmdb.libraries = 'lmdb' + s.subspec 'Boost' do |boost| + boost.preserve_paths = '../../../../../cw_shared_external/ios/External/ios/include/**/*.h', + boost.vendored_libraries = '../../../../../cw_shared_external/ios/External/ios/lib/libboost.a', + boost.libraries = 'boost' + boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } end + + s.subspec 'Monero' do |monero| + monero.preserve_paths = 'External/ios/include/**/*.h' + monero.vendored_libraries = 'External/ios/lib/libmonero.a' + monero.libraries = 'monero' + monero.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include" } + end + + # s.subspec 'lmdb' do |lmdb| + # lmdb.vendored_libraries = 'External/ios/lib/liblmdb.a' + # lmdb.libraries = 'lmdb' + # end end diff --git a/cw_monero/lib/monero_account_list.dart b/cw_monero/lib/monero_account_list.dart index 0d50c4f20..12cda5680 100644 --- a/cw_monero/lib/monero_account_list.dart +++ b/cw_monero/lib/monero_account_list.dart @@ -1,5 +1,5 @@ import 'package:mobx/mobx.dart'; -import 'package:cw_monero/account.dart'; +import 'package:cw_core/account.dart'; import 'package:cw_monero/api/account_list.dart' as account_list; part 'monero_account_list.g.dart'; @@ -44,7 +44,9 @@ abstract class MoneroAccountListBase with Store { List getAll() => account_list .getAllAccount() - .map((accountRow) => Account.fromRow(accountRow)) + .map((accountRow) => Account( + id: accountRow.getId(), + label: accountRow.getLabel())) .toList(); Future addAccount({String label}) async { diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index f5796bece..e59052207 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -2,7 +2,7 @@ import 'package:cw_monero/api/structs/subaddress_row.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; -import 'package:cw_monero/subaddress.dart'; +import 'package:cw_core/subaddress.dart'; part 'monero_subaddress_list.g.dart'; @@ -49,7 +49,13 @@ abstract class MoneroSubaddressListBase with Store { } return subaddresses - .map((subaddressRow) => Subaddress.fromRow(subaddressRow)) + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getId() == 0 && + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : subaddressRow.getLabel())) .toList(); } diff --git a/cw_monero/lib/monero_transaction_creation_credentials.dart b/cw_monero/lib/monero_transaction_creation_credentials.dart index 3c91026c5..3f0051046 100644 --- a/cw_monero/lib/monero_transaction_creation_credentials.dart +++ b/cw_monero/lib/monero_transaction_creation_credentials.dart @@ -1,6 +1,4 @@ -//import 'package:cake_wallet/entities/transaction_creation_credentials.dart'; -import 'package:cw_monero/monero_transaction_priority.dart'; -//import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/output_info.dart'; class MoneroTransactionCreationCredentials { diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 9677c1341..db393497a 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -1,5 +1,5 @@ import 'package:cw_core/transaction_info.dart'; -import 'package:cw_monero/monero_amount_format.dart'; +import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_monero/api/structs/transaction_info_row.dart'; import 'package:cw_core/parseBoolFromString.dart'; import 'package:cw_core/transaction_direction.dart'; diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 79808c11b..1d675cd8c 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_monero/monero_amount_format.dart'; +import 'package:cw_core/monero_amount_format.dart'; import 'package:cw_monero/monero_transaction_creation_exception.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:cw_monero/monero_wallet_utils.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'; @@ -16,16 +16,17 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history; import 'package:cw_monero/api/monero_output.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; -import 'package:cw_monero/monero_wallet_keys.dart'; -import 'package:cw_monero/monero_balance.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_balance.dart'; import 'package:cw_monero/monero_transaction_history.dart'; -import 'package:cw_monero/account.dart'; +import 'package:cw_core/account.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_monero/monero_transaction_priority.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/crypto_currency.dart'; part 'monero_wallet.g.dart'; @@ -38,18 +39,23 @@ abstract class MoneroWalletBase extends WalletBase.of({ + CryptoCurrency.xmr: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: 0), + unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) + }); _isTransactionUpdating = false; _hasSyncAfterStartup = false; walletAddresses = MoneroWalletAddresses(walletInfo); _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account account) { - balance = MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)); + balance = ObservableMap.of( + { + currency: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), + unlockedBalance: + monero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); walletAddresses.updateSubaddressList(accountIndex: account.id); }); } @@ -65,7 +71,7 @@ abstract class MoneroWalletBase extends WalletBase balance; @override String get seed => monero_wallet.getSeed(); @@ -85,10 +91,12 @@ abstract class MoneroWalletBase extends WalletBase init() async { await walletAddresses.init(); - balance = MoneroBalance( - fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id)); + balance = ObservableMap.of( + { + currency: MoneroBalance( + fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account.id), + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id)) + }); _setListeners(); await updateTransactions(); @@ -355,9 +363,9 @@ abstract class MoneroWalletBase extends WalletBase + diff --git a/cw_shared_external/android/src/main/kotlin/com/cakewallet/cw_shared_external/CwSharedExternalPlugin.kt b/cw_shared_external/android/src/main/kotlin/com/cakewallet/cw_shared_external/CwSharedExternalPlugin.kt new file mode 100644 index 000000000..6066999ff --- /dev/null +++ b/cw_shared_external/android/src/main/kotlin/com/cakewallet/cw_shared_external/CwSharedExternalPlugin.kt @@ -0,0 +1,36 @@ +package com.cakewallet.cw_shared_external + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry.Registrar + +/** CwSharedExternalPlugin */ +class CwSharedExternalPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_shared_external") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_shared_external/ios/.gitignore b/cw_shared_external/ios/.gitignore new file mode 100644 index 000000000..aa479fd3c --- /dev/null +++ b/cw_shared_external/ios/.gitignore @@ -0,0 +1,37 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_shared_external/ios/Assets/.gitkeep b/cw_shared_external/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_shared_external/ios/Classes/CwSharedExternalPlugin.h b/cw_shared_external/ios/Classes/CwSharedExternalPlugin.h new file mode 100644 index 000000000..96f5abe6b --- /dev/null +++ b/cw_shared_external/ios/Classes/CwSharedExternalPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface CwSharedExternalPlugin : NSObject +@end diff --git a/cw_shared_external/ios/Classes/CwSharedExternalPlugin.m b/cw_shared_external/ios/Classes/CwSharedExternalPlugin.m new file mode 100644 index 000000000..80f4bfe2f --- /dev/null +++ b/cw_shared_external/ios/Classes/CwSharedExternalPlugin.m @@ -0,0 +1,15 @@ +#import "CwSharedExternalPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "cw_shared_external-Swift.h" +#endif + +@implementation CwSharedExternalPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftCwSharedExternalPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/cw_shared_external/ios/Classes/SwiftCwSharedExternalPlugin.swift b/cw_shared_external/ios/Classes/SwiftCwSharedExternalPlugin.swift new file mode 100644 index 000000000..213fed6e4 --- /dev/null +++ b/cw_shared_external/ios/Classes/SwiftCwSharedExternalPlugin.swift @@ -0,0 +1,14 @@ +import Flutter +import UIKit + +public class SwiftCwSharedExternalPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_shared_external", binaryMessenger: registrar.messenger()) + let instance = SwiftCwSharedExternalPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result("iOS " + UIDevice.current.systemVersion) + } +} diff --git a/cw_shared_external/ios/cw_shared_external.podspec b/cw_shared_external/ios/cw_shared_external.podspec new file mode 100644 index 000000000..b147dc7df --- /dev/null +++ b/cw_shared_external/ios/cw_shared_external.podspec @@ -0,0 +1,41 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_shared_external.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_shared_external' + s.version = '0.0.1' + s.summary = 'Shared libraries for monero and haven.' + s.description = 'Shared libraries for monero and haven.' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'm@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '10.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } + s.swift_version = '5.0' + + s.subspec 'OpenSSL' do |openssl| + openssl.preserve_paths = 'External/ios/include/*.h' + openssl.vendored_libraries = 'External/ios/lib/libcrypto.a', 'External/ios/lib/libssl.a' + openssl.libraries = 'ssl', 'crypto' + openssl.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + + s.subspec 'Boost' do |boost| + boost.preserve_paths = 'External/ios/include/**/*.h' + boost.vendored_libraries = 'External/ios/lib/libboost.a', + boost.libraries = 'boost' + boost.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end + + s.subspec 'Sodium' do |sodium| + sodium.preserve_paths = 'External/ios/include/**/*.h' + sodium.vendored_libraries = 'External/ios/lib/libsodium.a' + sodium.libraries = 'sodium' + sodium.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/External/ios/include/**" } + end +end diff --git a/cw_shared_external/lib/cw_shared_external.dart b/cw_shared_external/lib/cw_shared_external.dart new file mode 100644 index 000000000..37d0350dd --- /dev/null +++ b/cw_shared_external/lib/cw_shared_external.dart @@ -0,0 +1,14 @@ + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +class CwSharedExternal { + static const MethodChannel _channel = + const MethodChannel('cw_shared_external'); + + static Future get platformVersion async { + final String version = await _channel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/cw_shared_external/pubspec.lock b/cw_shared_external/pubspec.lock new file mode 100644 index 000000000..ef01c9f9a --- /dev/null +++ b/cw_shared_external/pubspec.lock @@ -0,0 +1,147 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.10" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.19" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" +sdks: + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/cw_shared_external/pubspec.yaml b/cw_shared_external/pubspec.yaml new file mode 100644 index 000000000..b9a8ca1e0 --- /dev/null +++ b/cw_shared_external/pubspec.yaml @@ -0,0 +1,26 @@ +name: cw_shared_external +description: Shared external libraries for monero and haven +version: 0.0.1 +author: Cake Walelt +homepage: https://cakewallet.com + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: com.cakewallet.cw_shared_external + pluginClass: CwSharedExternalPlugin + ios: + pluginClass: CwSharedExternalPlugin \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fef5810ff..d90ef8ca3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,22 +8,54 @@ PODS: - Flutter - Reachability - CryptoSwift (1.3.2) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter - cw_monero (0.0.2): - cw_monero/Boost (= 0.0.2) - - cw_monero/lmdb (= 0.0.2) - cw_monero/Monero (= 0.0.2) - cw_monero/OpenSSL (= 0.0.2) - cw_monero/Sodium (= 0.0.2) + - cw_shared_external - Flutter - cw_monero/Boost (0.0.2): - - Flutter - - cw_monero/lmdb (0.0.2): + - cw_shared_external - Flutter - cw_monero/Monero (0.0.2): + - cw_shared_external - Flutter - cw_monero/OpenSSL (0.0.2): + - cw_shared_external - Flutter - cw_monero/Sodium (0.0.2): + - cw_shared_external + - Flutter + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): - Flutter - devicelocale (0.0.1): - Flutter @@ -99,7 +131,9 @@ DEPENDENCIES: - barcode_scan (from `.symlinks/plugins/barcode_scan/ios`) - connectivity (from `.symlinks/plugins/connectivity/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_monero (from `.symlinks/plugins/cw_monero/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - esys_flutter_share (from `.symlinks/plugins/esys_flutter_share/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -134,8 +168,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/barcode_scan/ios" connectivity: :path: ".symlinks/plugins/connectivity/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_monero: :path: ".symlinks/plugins/cw_monero/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" esys_flutter_share: @@ -170,7 +208,9 @@ SPEC CHECKSUMS: BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 - cw_monero: c79d5530b828b8013c1db421f1be8bab687f7b7e + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a + cw_monero: 88c5e7aa596c6848330750f5f8bcf05fb9c66375 + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index cadcd78bb..a0808e686 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -366,7 +366,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; @@ -510,7 +510,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; @@ -546,7 +546,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 903def2af..0c67376eb 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -1,8 +1,5 @@ - - aps-environment - development - + diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 0401c2921..8e87c9930 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -46,6 +46,22 @@ class AddressValidator extends TextValidator { return '[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; + case CryptoCurrency.xhv: + case CryptoCurrency.xhv: + case CryptoCurrency.xag: + case CryptoCurrency.xau: + case CryptoCurrency.xaud: + case CryptoCurrency.xbtc: + case CryptoCurrency.xcad: + case CryptoCurrency.xchf: + case CryptoCurrency.xcny: + case CryptoCurrency.xeur: + case CryptoCurrency.xgbp: + case CryptoCurrency.xjpy: + case CryptoCurrency.xnok: + case CryptoCurrency.xnzd: + case CryptoCurrency.xusd: + return '[0-9a-zA-Z]'; default: return '[0-9a-zA-Z]'; } @@ -85,6 +101,22 @@ class AddressValidator extends TextValidator { return [56]; case CryptoCurrency.xrp: return null; + case CryptoCurrency.xhv: + case CryptoCurrency.xhv: + case CryptoCurrency.xag: + case CryptoCurrency.xau: + case CryptoCurrency.xaud: + case CryptoCurrency.xbtc: + case CryptoCurrency.xcad: + case CryptoCurrency.xchf: + case CryptoCurrency.xcny: + case CryptoCurrency.xeur: + case CryptoCurrency.xgbp: + case CryptoCurrency.xjpy: + case CryptoCurrency.xnok: + case CryptoCurrency.xnzd: + case CryptoCurrency.xusd: + return [98, 99, 106]; default: return []; } diff --git a/lib/core/amount_converter.dart b/lib/core/amount_converter.dart index c939007f8..b3c976e25 100644 --- a/lib/core/amount_converter.dart +++ b/lib/core/amount_converter.dart @@ -31,6 +31,21 @@ class AmountConverter { return _ethereumAmountToDouble(amount); case CryptoCurrency.ltc: return _litecoinAmountToDouble(amount); + case CryptoCurrency.xhv: + case CryptoCurrency.xag: + case CryptoCurrency.xau: + case CryptoCurrency.xaud: + case CryptoCurrency.xbtc: + case CryptoCurrency.xcad: + case CryptoCurrency.xchf: + case CryptoCurrency.xcny: + case CryptoCurrency.xeur: + case CryptoCurrency.xgbp: + case CryptoCurrency.xjpy: + case CryptoCurrency.xnok: + case CryptoCurrency.xnzd: + case CryptoCurrency.xusd: + return _moneroAmountToDouble(amount); default: return null; } @@ -40,6 +55,21 @@ class AmountConverter { switch (cryptoCurrency) { case CryptoCurrency.xmr: return _moneroParseAmount(amount); + case CryptoCurrency.xhv: + case CryptoCurrency.xag: + case CryptoCurrency.xau: + case CryptoCurrency.xaud: + case CryptoCurrency.xbtc: + case CryptoCurrency.xcad: + case CryptoCurrency.xchf: + case CryptoCurrency.xcny: + case CryptoCurrency.xeur: + case CryptoCurrency.xgbp: + case CryptoCurrency.xjpy: + case CryptoCurrency.xnok: + case CryptoCurrency.xnzd: + case CryptoCurrency.xusd: + return _moneroParseAmount(amount); default: return null; } @@ -51,6 +81,21 @@ class AmountConverter { return _moneroAmountToString(amount); case CryptoCurrency.btc: return _bitcoinAmountToString(amount); + case CryptoCurrency.xhv: + case CryptoCurrency.xag: + case CryptoCurrency.xau: + case CryptoCurrency.xaud: + case CryptoCurrency.xbtc: + case CryptoCurrency.xcad: + case CryptoCurrency.xchf: + case CryptoCurrency.xcny: + case CryptoCurrency.xeur: + case CryptoCurrency.xgbp: + case CryptoCurrency.xjpy: + case CryptoCurrency.xnok: + case CryptoCurrency.xnzd: + case CryptoCurrency.xusd: + return _moneroAmountToString(amount); default: return null; } diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index c45490f95..e1e5920fd 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/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cw_core/wallet_type.dart'; @@ -21,6 +22,8 @@ class SeedValidator extends Validator { return getBitcoinWordList(language); case WalletType.monero: return monero.getMoneroWordList(language); + case WalletType.haven: + return haven.getMoneroWordList(language); default: return []; } diff --git a/lib/di.dart b/lib/di.dart index 4252ec3a5..3cc4dacae 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -2,6 +2,8 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/balance_page.dart'; import 'package:cw_core/unspent_coins_info.dart'; @@ -122,6 +124,7 @@ import 'package:cake_wallet/store/templates/exchange_template_store.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; final getIt = GetIt.instance; @@ -312,6 +315,9 @@ Future setup( getIt.registerFactory(() => DashboardPage( balancePage: getIt.get(), walletViewModel: getIt.get(), addressListViewModel: getIt.get())); getIt.registerFactory(() => ReceivePage( addressListViewModel: getIt.get())); + getIt.registerFactory(() => AddressPage( + addressListViewModel: getIt.get(), + walletViewModel: getIt.get())); getIt.registerFactoryParam( (dynamic item, _) => WalletAddressEditOrCreateViewModel( @@ -344,8 +350,7 @@ Future setup( getIt.registerFactory(() => WalletListViewModel( _walletInfoSource, getIt.get(), - getIt.get(), - getIt.get(param1: WalletType.monero))); + getIt.get())); getIt.registerFactory(() => WalletListPage(walletListViewModel: getIt.get())); @@ -353,7 +358,7 @@ Future setup( getIt.registerFactory(() { final wallet = getIt.get().wallet; - if (wallet.type == WalletType.monero) { + if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { return MoneroAccountListViewModel(wallet); } @@ -383,6 +388,7 @@ Future setup( AccountListItem, void>( (AccountListItem account, _) => MoneroAccountEditOrCreateViewModel( monero.getAccountList(getIt.get().wallet), + haven.getAccountList(getIt.get().wallet), wallet: getIt.get().wallet, accountListItem: account)); @@ -469,6 +475,8 @@ Future setup( getIt.registerFactoryParam( (WalletType param1, __) { switch (param1) { + case WalletType.haven: + return haven.createHavenWalletService(_walletInfoSource); case WalletType.monero: return monero.createMoneroWalletService(_walletInfoSource); case WalletType.bitcoin: diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index 407030d7f..69ab56749 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -11,5 +11,16 @@ String calculateFiatAmount({double price, String cryptoAmount}) { return '0.00'; } - return result > 0.01 ? result.toStringAsFixed(2) : '< 0.01'; + var formatted = ''; + final parts = result.toString().split('.'); + + if (parts.length >= 2) { + if (parts[1].length > 2) { + formatted = parts[0] + '.' + parts[1].substring(0, 2); + } else { + formatted = parts[0] + '.' + parts[1]; + } + } + + return result > 0.01 ? formatted : '< 0.01'; } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 889769055..c0e541058 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -22,6 +22,7 @@ import 'package:encrypt/encrypt.dart' as encrypt; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; +const havenDefaultNodeUri = 'vault.havenprotocol.org:443'; Future defaultSettingsMigration( {@required int version, @@ -120,6 +121,13 @@ Future defaultSettingsMigration( await checkCurrentNodes(nodes, sharedPreferences); break; + case 16: + await addHavenNodeList(nodes: nodes); + await changeHavenCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await checkCurrentNodes(nodes, sharedPreferences); + break; + default: break; } @@ -182,6 +190,14 @@ Node getLitecoinDefaultElectrumServer({@required Box nodes}) { orElse: () => null); } +Node getHavenDefaultNode({@required Box nodes}) { + return nodes.values.firstWhere( + (Node node) => node.uriRaw == havenDefaultNodeUri, + orElse: () => null) ?? + nodes.values.firstWhere((node) => node.type == WalletType.haven, + orElse: () => null); +} + Node getMoneroDefaultNode({@required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -217,6 +233,15 @@ Future changeLitecoinCurrentElectrumServerToDefault( await sharedPreferences.setInt('current_node_id_ltc', serverId); } +Future changeHavenCurrentNodeToDefault( + {@required SharedPreferences sharedPreferences, + @required Box nodes}) async { + final node = getHavenDefaultNode(nodes: nodes); + final nodeId = node?.key as int ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, nodeId); +} + Future replaceDefaultNode( {@required SharedPreferences sharedPreferences, @required Box nodes}) async { @@ -258,6 +283,11 @@ Future addLitecoinElectrumServerList({@required Box nodes}) async { await nodes.addAll(serverList); } +Future addHavenNodeList({@required Box nodes}) async { + final nodeList = await loadDefaultHavenNodes(); + await nodes.addAll(nodeList); +} + Future addAddressesForMoneroWallets( Box walletInfoSource) async { final moneroWalletsInfo = @@ -347,6 +377,8 @@ Future checkCurrentNodes( sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); final currentLitecoinElectrumSeverId = sharedPreferences .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final currentHavenNodeId = sharedPreferences + .getInt(PreferencesKey.currentHavenNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhere( (node) => node.key == currentMoneroNodeId, orElse: () => null); @@ -356,6 +388,9 @@ Future checkCurrentNodes( final currentLitecoinElectrumServer = nodeSource.values.firstWhere( (node) => node.key == currentLitecoinElectrumSeverId, orElse: () => null); + final currentHavenNodeServer = nodeSource.values.firstWhere( + (node) => node.key == currentHavenNodeId, + orElse: () => null); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -382,6 +417,14 @@ Future checkCurrentNodes( PreferencesKey.currentLitecoinElectrumSererIdKey, cakeWalletElectrum.key as int); } + + if (currentHavenNodeServer == null) { + final nodes = await loadDefaultHavenNodes(); + final node = nodes.first; + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentHavenNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 167d9483c..66b41c20b 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -54,12 +54,32 @@ Future> loadLitecoinElectrumServerList() async { }).toList(); } +Future> loadDefaultHavenNodes() async { + final nodesRaw = await rootBundle.loadString('assets/haven_node_list.yml'); + final nodes = loadYaml(nodesRaw) as YamlList; + + return nodes.map((dynamic raw) { + if (raw is Map) { + final node = Node.fromMap(raw); + node?.type = WalletType.haven; + + return node; + } + + return null; + }).toList(); +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); + final havenNodes = await loadDefaultHavenNodes(); final nodes = - moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList; + moneroNodes + + bitcoinElectrumServerList + + litecoinElectrumServerList + + havenNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 3c94e8322..82e100c42 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -4,6 +4,7 @@ class PreferencesKey { static const currentNodeIdKey = 'current_node_id'; static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; + static const currentHavenNodeIdKey = 'current_node_id_xhv'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; diff --git a/lib/entities/update_haven_rate.dart b/lib/entities/update_haven_rate.dart new file mode 100644 index 000000000..1bb1a88e1 --- /dev/null +++ b/lib/entities/update_haven_rate.dart @@ -0,0 +1,21 @@ +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_haven/api/balance_list.dart'; + +Future updateHavenRate(FiatConversionStore fiatConversionStore) async { + final rate = getRate(); + final base = rate.firstWhere((row) => row.getAssetType() == 'XUSD', orElse: () => null); + rate.forEach((row) { + final cur = CryptoCurrency.fromString(row.getAssetType()); + final baseRate = moneroAmountToDouble(amount: base.getRate()); + final rowRate = moneroAmountToDouble(amount: row.getRate()); + + if (cur == CryptoCurrency.xusd) { + fiatConversionStore.prices[cur] = 1.0; + return; + } + + fiatConversionStore.prices[cur] = baseRate / rowRate; + }); +} \ No newline at end of file diff --git a/lib/haven/cw_haven.dart b/lib/haven/cw_haven.dart new file mode 100644 index 000000000..6261e1fc9 --- /dev/null +++ b/lib/haven/cw_haven.dart @@ -0,0 +1,298 @@ +part of 'haven.dart'; + +class CWHavenAccountList extends HavenAccountList { + CWHavenAccountList(this._wallet); + Object _wallet; + + @override + @computed + ObservableList get accounts { + final havenWallet = _wallet as HavenWallet; + final accounts = havenWallet.walletAddresses.accountList + .accounts + .map((acc) => Account(id: acc.id, label: acc.label)) + .toList(); + return ObservableList.of(accounts); + } + + @override + void update(Object wallet) { + final havenWallet = wallet as HavenWallet; + havenWallet.walletAddresses.accountList.update(); + } + + @override + void refresh(Object wallet) { + final havenWallet = wallet as HavenWallet; + havenWallet.walletAddresses.accountList.refresh(); + } + + @override + List getAll(Object wallet) { + final havenWallet = wallet as HavenWallet; + return havenWallet.walletAddresses.accountList + .getAll() + .map((acc) => Account(id: acc.id, label: acc.label)) + .toList(); + } + + @override + Future addAccount(Object wallet, {String label}) async { + final havenWallet = wallet as HavenWallet; + await havenWallet.walletAddresses.accountList.addAccount(label: label); + } + + @override + Future setLabelAccount(Object wallet, {int accountIndex, String label}) async { + final havenWallet = wallet as HavenWallet; + await havenWallet.walletAddresses.accountList + .setLabelAccount( + accountIndex: accountIndex, + label: label); + } +} + +class CWHavenSubaddressList extends MoneroSubaddressList { + CWHavenSubaddressList(this._wallet); + Object _wallet; + + @override + @computed + ObservableList get subaddresses { + final havenWallet = _wallet as HavenWallet; + final subAddresses = havenWallet.walletAddresses.subaddressList + .subaddresses + .map((sub) => Subaddress( + id: sub.id, + address: sub.address, + label: sub.label)) + .toList(); + return ObservableList.of(subAddresses); + } + + @override + void update(Object wallet, {int accountIndex}) { + final havenWallet = wallet as HavenWallet; + havenWallet.walletAddresses.subaddressList.update(accountIndex: accountIndex); + } + + @override + void refresh(Object wallet, {int accountIndex}) { + final havenWallet = wallet as HavenWallet; + havenWallet.walletAddresses.subaddressList.refresh(accountIndex: accountIndex); + } + + @override + List getAll(Object wallet) { + final havenWallet = wallet as HavenWallet; + return havenWallet.walletAddresses + .subaddressList + .getAll() + .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) + .toList(); + } + + @override + Future addSubaddress(Object wallet, {int accountIndex, String label}) async { + final havenWallet = wallet as HavenWallet; + await havenWallet.walletAddresses.subaddressList + .addSubaddress( + accountIndex: accountIndex, + label: label); + } + + @override + Future setLabelSubaddress(Object wallet, + {int accountIndex, int addressIndex, String label}) async { + final havenWallet = wallet as HavenWallet; + await havenWallet.walletAddresses.subaddressList + .setLabelSubaddress( + accountIndex: accountIndex, + addressIndex: addressIndex, + label: label); + } +} + +class CWHavenWalletDetails extends HavenWalletDetails { + CWHavenWalletDetails(this._wallet); + Object _wallet; + + @computed + Account get account { + final havenWallet = _wallet as HavenWallet; + final acc = havenWallet.walletAddresses.account as monero_account.Account; + return Account(id: acc.id, label: acc.label); + } + + @computed + HavenBalance get balance { + final havenWallet = _wallet as HavenWallet; + final balance = havenWallet.balance; + return null; + //return HavenBalance( + // fullBalance: balance.fullBalance, + // unlockedBalance: balance.unlockedBalance); + } +} + +class CWHaven extends Haven { + HavenAccountList getAccountList(Object wallet) { + return CWHavenAccountList(wallet); + } + + MoneroSubaddressList getSubaddressList(Object wallet) { + return CWHavenSubaddressList(wallet); + } + + TransactionHistoryBase getTransactionHistory(Object wallet) { + final havenWallet = wallet as HavenWallet; + return havenWallet.transactionHistory; + } + + HavenWalletDetails getMoneroWalletDetails(Object wallet) { + return CWHavenWalletDetails(wallet); + } + + int getHeigthByDate({DateTime date}) { + return getMoneroHeigthByDate(date: date); + } + + TransactionPriority getDefaultTransactionPriority() { + return MoneroTransactionPriority.slow; + } + + TransactionPriority deserializeMoneroTransactionPriority({int raw}) { + return MoneroTransactionPriority.deserialize(raw: raw); + } + + List getTransactionPriorities() { + return MoneroTransactionPriority.all; + } + + List getMoneroWordList(String language) { + switch (language.toLowerCase()) { + case 'english': + return EnglishMnemonics.words; + case 'chinese (simplified)': + return ChineseSimplifiedMnemonics.words; + case 'dutch': + return DutchMnemonics.words; + case 'german': + return GermanMnemonics.words; + case 'japanese': + return JapaneseMnemonics.words; + case 'portuguese': + return PortugueseMnemonics.words; + case 'russian': + return RussianMnemonics.words; + case 'spanish': + return SpanishMnemonics.words; + case 'french': + return FrenchMnemonics.words; + case 'italian': + return ItalianMnemonics.words; + default: + return EnglishMnemonics.words; + } + } + + WalletCredentials createHavenRestoreWalletFromKeysCredentials({ + String name, + String spendKey, + String viewKey, + String address, + String password, + String language, + int height}) { + return HavenRestoreWalletFromKeysCredentials( + name: name, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: language, + height: height); + } + + WalletCredentials createHavenRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}) { + return HavenRestoreWalletFromSeedCredentials( + name: name, + password: password, + height: height, + mnemonic: mnemonic); + } + + WalletCredentials createHavenNewWalletCredentials({String name, String password, String language}) { + return HavenNewWalletCredentials( + name: name, + password: password, + language: language); + } + + Map getKeys(Object wallet) { + final havenWallet = wallet as HavenWallet; + final keys = havenWallet.keys; + return { + 'privateSpendKey': keys.privateSpendKey, + 'privateViewKey': keys.privateViewKey, + 'publicSpendKey': keys.publicSpendKey, + 'publicViewKey': keys.publicViewKey}; + } + + Object createHavenTransactionCreationCredentials({List outputs, TransactionPriority priority, String assetType}) { + return HavenTransactionCreationCredentials( + outputs: outputs.map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as MoneroTransactionPriority, + assetType: assetType); + } + + String formatterMoneroAmountToString({int amount}) { + return moneroAmountToString(amount: amount); + } + + double formatterMoneroAmountToDouble({int amount}) { + return moneroAmountToDouble(amount: amount); + } + + int formatterMoneroParseAmount({String amount}) { + return moneroParseAmount(amount: amount); + } + + Account getCurrentAccount(Object wallet) { + final havenWallet = wallet as HavenWallet; + final acc = havenWallet.walletAddresses.account as monero_account.Account; + return Account(id: acc.id, label: acc.label); + } + + void setCurrentAccount(Object wallet, int id, String label) { + final havenWallet = wallet as HavenWallet; + havenWallet.walletAddresses.account = monero_account.Account(id: id, label: label); + } + + void onStartup() { + monero_wallet_api.onStartup(); + } + + int getTransactionInfoAccountId(TransactionInfo tx) { + final havenTransactionInfo = tx as HavenTransactionInfo; + return havenTransactionInfo.accountIndex; + } + + WalletService createHavenWalletService(Box walletInfoSource) { + return HavenWalletService(walletInfoSource); + } + + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex) { + final havenWallet = wallet as HavenWallet; + return havenWallet.getTransactionAddress(accountIndex, addressIndex); + } +} diff --git a/lib/haven/haven.dart b/lib/haven/haven.dart new file mode 100644 index 000000000..2c9ecae8e --- /dev/null +++ b/lib/haven/haven.dart @@ -0,0 +1,144 @@ +import 'package:mobx/mobx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_haven/haven_wallet_service.dart'; +import 'package:cw_haven/haven_wallet.dart'; +import 'package:cw_haven/haven_transaction_info.dart'; +import 'package:cw_haven/haven_transaction_history.dart'; +import 'package:cw_core/account.dart' as monero_account; +import 'package:cw_haven/api/wallet.dart' as monero_wallet_api; +import 'package:cw_haven/mnemonics/english.dart'; +import 'package:cw_haven/mnemonics/chinese_simplified.dart'; +import 'package:cw_haven/mnemonics/dutch.dart'; +import 'package:cw_haven/mnemonics/german.dart'; +import 'package:cw_haven/mnemonics/japanese.dart'; +import 'package:cw_haven/mnemonics/russian.dart'; +import 'package:cw_haven/mnemonics/spanish.dart'; +import 'package:cw_haven/mnemonics/portuguese.dart'; +import 'package:cw_haven/mnemonics/french.dart'; +import 'package:cw_haven/mnemonics/italian.dart'; +import 'package:cw_haven/haven_transaction_creation_credentials.dart'; + +part 'cw_haven.dart'; + +Haven haven = CWHaven(); + +class Account { + Account({this.id, this.label}); + final int id; + final String label; +} + +class Subaddress { + Subaddress({this.id, this.accountId, this.label, this.address}); + final int id; + final int accountId; + final String label; + final String address; +} + +class HavenBalance extends Balance { + HavenBalance({@required this.fullBalance, @required this.unlockedBalance}) + : formattedFullBalance = haven.formatterMoneroAmountToString(amount: fullBalance), + formattedUnlockedBalance = + haven.formatterMoneroAmountToString(amount: unlockedBalance), + super(unlockedBalance, fullBalance); + + HavenBalance.fromString( + {@required this.formattedFullBalance, + @required this.formattedUnlockedBalance}) + : fullBalance = haven.formatterMoneroParseAmount(amount: formattedFullBalance), + unlockedBalance = haven.formatterMoneroParseAmount(amount: formattedUnlockedBalance), + super(haven.formatterMoneroParseAmount(amount: formattedUnlockedBalance), + haven.formatterMoneroParseAmount(amount: formattedFullBalance)); + + final int fullBalance; + final int unlockedBalance; + final String formattedFullBalance; + final String formattedUnlockedBalance; + + @override + String get formattedAvailableBalance => formattedUnlockedBalance; + + @override + String get formattedAdditionalBalance => formattedFullBalance; +} + +abstract class HavenWalletDetails { + @observable + Account account; + + @observable + HavenBalance balance; +} + +abstract class Haven { + HavenAccountList getAccountList(Object wallet); + + MoneroSubaddressList getSubaddressList(Object wallet); + + TransactionHistoryBase getTransactionHistory(Object wallet); + + HavenWalletDetails getMoneroWalletDetails(Object wallet); + + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); + + int getHeigthByDate({DateTime date}); + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority deserializeMoneroTransactionPriority({int raw}); + List getTransactionPriorities(); + List getMoneroWordList(String language); + + WalletCredentials createHavenRestoreWalletFromKeysCredentials({ + String name, + String spendKey, + String viewKey, + String address, + String password, + String language, + int height}); + WalletCredentials createHavenRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}); + WalletCredentials createHavenNewWalletCredentials({String name, String password, String language}); + Map getKeys(Object wallet); + Object createHavenTransactionCreationCredentials({List outputs, TransactionPriority priority, String assetType}); + String formatterMoneroAmountToString({int amount}); + double formatterMoneroAmountToDouble({int amount}); + int formatterMoneroParseAmount({String amount}); + Account getCurrentAccount(Object wallet); + void setCurrentAccount(Object wallet, int id, String label); + void onStartup(); + int getTransactionInfoAccountId(TransactionInfo tx); + WalletService createHavenWalletService(Box walletInfoSource); +} + +abstract class MoneroSubaddressList { + ObservableList get subaddresses; + void update(Object wallet, {int accountIndex}); + void refresh(Object wallet, {int accountIndex}); + List getAll(Object wallet); + Future addSubaddress(Object wallet, {int accountIndex, String label}); + Future setLabelSubaddress(Object wallet, + {int accountIndex, int addressIndex, String label}); +} + +abstract class HavenAccountList { + ObservableList get accounts; + void update(Object wallet); + void refresh(Object wallet); + List getAll(Object wallet); + Future addAccount(Object wallet, {String label}); + Future setLabelAccount(Object wallet, {int accountIndex, String label}); +} + \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 749f02b66..4291df9fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -112,6 +112,17 @@ Future main() async { if (!isMoneroOnly) { unspentCoinsInfoSource = await Hive.openBox(UnspentCoinsInfo.boxName); } + + FlutterError.onError = (FlutterErrorDetails details) { + runApp(MaterialApp( + debugShowCheckedModeBanner: true, + home: Scaffold( + body: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Text( + 'Error:\n${details.stack.toString()}', + style: TextStyle(fontSize: 22)))))); + }; await initialSetup( sharedPreferences: await SharedPreferences.getInstance(), @@ -126,7 +137,7 @@ Future main() async { exchangeTemplates: exchangeTemplates, transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, - initialMigrationVersion: 15); + initialMigrationVersion: 16); runApp(App()); } catch (e) { runApp(MaterialApp( diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index f17e4b6ed..878fdda99 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -128,9 +128,10 @@ class CWMoneroWalletDetails extends MoneroWalletDetails { MoneroBalance get balance { final moneroWallet = _wallet as MoneroWallet; final balance = moneroWallet.balance; - return MoneroBalance( - fullBalance: balance.fullBalance, - unlockedBalance: balance.unlockedBalance); + return MoneroBalance(); + //return MoneroBalance( + // fullBalance: balance.fullBalance, + // unlockedBalance: balance.unlockedBalance); } } @@ -271,9 +272,9 @@ class CWMonero extends Monero { return Account(id: acc.id, label: acc.label); } - void setCurrentAccount(Object wallet, Account account) { + void setCurrentAccount(Object wallet, int id, String label) { final moneroWallet = wallet as MoneroWallet; - moneroWallet.walletAddresses.account = monero_account.Account(id: account.id, label: account.label); + moneroWallet.walletAddresses.account = monero_account.Account(id: id, label: label); } void onStartup() { diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 365423ef4..04134b555 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; +import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_type.dart'; Timer _timer; @@ -18,7 +20,16 @@ Future startFiatRateUpdate(AppStore appStore, SettingsStore settingsStore, _timer = Timer.periodic( Duration(seconds: 30), - (_) async => fiatConversionStore.prices[appStore.wallet.currency] = - await FiatConversionService.fetchPrice( - appStore.wallet.currency, settingsStore.fiatCurrency)); + (_) async { + try { + if (appStore.wallet.type == WalletType.haven) { + await updateHavenRate(fiatConversionStore); + } else { + fiatConversionStore.prices[appStore.wallet.currency] = await FiatConversionService.fetchPrice( + appStore.wallet.currency, settingsStore.fiatCurrency); + } + } catch(e) { + print(e); + } + }); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 842bc761f..ab1838d58 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -51,7 +53,7 @@ void startCurrentWalletChangeReaction(AppStore appStore, wallet) async { try { final node = settingsStore.getCurrentNode(wallet.type); - startWalletSyncStatusChangeReaction(wallet); + startWalletSyncStatusChangeReaction(wallet, fiatConversionStore); startCheckConnectionReaction(wallet, settingsStore); await getIt .get() @@ -60,6 +62,11 @@ void startCurrentWalletChangeReaction(AppStore appStore, PreferencesKey.currentWalletType, serializeToInt(wallet.type)); await wallet.connectToNode(node: node); + if (wallet.type == WalletType.haven) { + settingsStore.fiatCurrency = FiatCurrency.usd; + await updateHavenRate(fiatConversionStore); + } + if (wallet.walletInfo.address?.isEmpty ?? true) { wallet.walletInfo.address = wallet.walletAddresses.address; diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index bb245898c..4ee5821e3 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,5 +1,8 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; +import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; @@ -12,14 +15,18 @@ ReactionDisposer _onWalletSyncStatusChangeReaction; void startWalletSyncStatusChangeReaction( WalletBase, - TransactionInfo> - wallet) { + TransactionInfo> wallet, + FiatConversionStore fiatConversionStore) { final _wakeLock = getIt.get(); _onWalletSyncStatusChangeReaction?.reaction?.dispose(); _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { if (status is ConnectedSyncStatus) { await wallet.startSync(); + + if (wallet.type == WalletType.haven) { + await updateHavenRate(fiatConversionStore); + } } if (status is SyncingSyncStatus) { await _wakeLock.enableWake(); diff --git a/lib/router.dart b/lib/router.dart index 11a7e5ae3..acfb18684 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -68,6 +68,8 @@ import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart' import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cake_wallet/wallet_types.g.dart'; +import 'package:cake_wallet/src/screens/dashboard/widgets/address_page.dart'; RouteSettings currentRouteSettings; @@ -82,11 +84,11 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( builder: (_) => getIt.get( param1: (PinCodeState context, dynamic _) { - if (isMoneroOnly) { - Navigator.of(context.context).pushNamed(Routes.newWallet, arguments: WalletType.monero); - } else { - Navigator.of(context.context).pushNamed(Routes.newWalletType); - } + if (availableWalletTypes.length == 1) { + Navigator.of(context.context).pushNamed(Routes.newWallet, arguments: availableWalletTypes.first); + } else { + Navigator.of(context.context).pushNamed(Routes.newWalletType); + } }), fullscreenDialog: true); @@ -216,6 +218,10 @@ Route createRoute(RouteSettings settings) { return CupertinoPageRoute( fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.addressPage: + return CupertinoPageRoute( + fullscreenDialog: true, builder: (_) => getIt.get()); + case Routes.transactionDetails: return CupertinoPageRoute( fullscreenDialog: true, diff --git a/lib/routes.dart b/lib/routes.dart index afb64f9ad..23e236023 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -59,4 +59,5 @@ class Routes { static const unspentCoinsDetails = '/unspent_coins_details'; static const moneroRestoreWalletFromWelcome = '/monero_restore_wallet'; static const moneroNewWalletFromWelcome = '/monero_new_wallet'; + static const addressPage = '/address_page'; } \ No newline at end of file diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index e29b78a1e..dca2afdf8 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -224,6 +224,9 @@ class ContactListPage extends BasePage { case CryptoCurrency.xrp: image = Image.asset('assets/images/xrp.png', height: 24, width: 24); break; + case CryptoCurrency.xhv: + image = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); + break; default: image = null; } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index d626bf735..eb8c9ab17 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -85,7 +85,7 @@ class DashboardPage extends BasePage { final DashboardViewModel walletViewModel; final WalletAddressListViewModel addressListViewModel; - final controller = PageController(initialPage: 1); + final controller = PageController(initialPage: 0); var pages = []; bool _isEffectsInstalled = false; @@ -101,21 +101,10 @@ class DashboardPage extends BasePage { height: 24, width: 24, color: Theme.of(context).accentTextTheme.display3.backgroundColor); - final exchangeImage = Image.asset('assets/images/transfer.png', - height: 24, - width: 24, - color: Theme.of(context).accentTextTheme.display3.backgroundColor); - final buyImage = Image.asset('assets/images/buy.png', - height: 24, - width: 24, - color: Theme.of(context).accentTextTheme.display3.backgroundColor); - final sellImage = Image.asset('assets/images/sell.png', - height: 24, - width: 24, - color: Theme.of(context).accentTextTheme.display3.backgroundColor); _setEffects(context); return SafeArea( + minimum: EdgeInsets.only(bottom: 24), child: Column( mainAxisSize: MainAxisSize.max, children: [ @@ -125,7 +114,7 @@ class DashboardPage extends BasePage { itemCount: pages.length, itemBuilder: (context, index) => pages[index])), Padding( - padding: EdgeInsets.only(bottom: 24), + padding: EdgeInsets.only(bottom: 24, top: 10), child: SmoothPageIndicator( controller: controller, count: pages.length, @@ -140,48 +129,89 @@ class DashboardPage extends BasePage { .display1 .backgroundColor), )), - - ClipRect( - child:Container( - margin: const EdgeInsets.only(left: 16, right: 16, bottom: 38), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.0), - border: Border.all(color: currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), - color:Theme.of(context).textTheme.title.backgroundColor - ), - child: Container( - padding: EdgeInsets.only(left: 32, right: 32, bottom: 14, top: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (!isMoneroOnly) + Observer(builder: (_) { + return ClipRect( + child:Container( + margin: const EdgeInsets.only(left: 16, right: 16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + border: Border.all(color: currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), + color:Theme.of(context).textTheme.title.backgroundColor), + child: Container( + padding: EdgeInsets.only(left: 32, right: 32), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (walletViewModel.hasBuyAction) + ActionButton( + image: Image.asset('assets/images/buy.png', + height: 24, + width: 24, + color: !walletViewModel.isEnabledBuyAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : Theme.of(context).accentTextTheme.display3.backgroundColor), + title: S.of(context).buy, + onClick: () async => await _onClickBuyButton(context), + textColor: !walletViewModel.isEnabledBuyAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : null), ActionButton( - image: buyImage, - title: S.of(context).buy, - onClick: () async => await _onClickBuyButton(context), - ), - ActionButton( - image: receiveImage, - title: S.of(context).receive, - route: Routes.receive), - ActionButton( - image: exchangeImage, - title: S.of(context).exchange, - route: Routes.exchange), - ActionButton( - image: sendImage, - title: S.of(context).send, - route: Routes.send), - if (!isMoneroOnly) + image: receiveImage, + title: S.of(context).receive, + route: Routes.addressPage), + if (walletViewModel.hasExchangeAction) + ActionButton( + image: Image.asset('assets/images/transfer.png', + height: 24, + width: 24, + color: !walletViewModel.isEnabledExchangeAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : Theme.of(context).accentTextTheme.display3.backgroundColor), + title: S.of(context).exchange, + onClick: () async => _onClickExchangeButton(context), + textColor: !walletViewModel.isEnabledExchangeAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : null), ActionButton( - image: sellImage, - title: S.of(context).sell, - onClick: () async => await _onClickSellButton(context), - ), - ], - ),), - ),),), + image: sendImage, + title: S.of(context).send, + route: Routes.send), + if (walletViewModel.hasSellAction) + ActionButton( + image: Image.asset('assets/images/sell.png', + height: 24, + width: 24, + color: !walletViewModel.isEnabledSellAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : Theme.of(context).accentTextTheme.display3.backgroundColor), + title: S.of(context).sell, + onClick: () async => await _onClickSellButton(context), + textColor: !walletViewModel.isEnabledSellAction + ? Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor + : null), + ], + ),), + ),),); + }), ], )); @@ -192,9 +222,6 @@ class DashboardPage extends BasePage { return; } - pages.add(AddressPage( - addressListViewModel: addressListViewModel, - walletViewModel: walletViewModel)); pages.add(balancePage); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); _isEffectsInstalled = true; @@ -289,4 +316,24 @@ class DashboardPage extends BasePage { }); } } -} \ No newline at end of file + + Future _onClickExchangeButton(BuildContext context) async { + final walletType = walletViewModel.type; + + switch (walletType) { + case WalletType.haven: + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: 'Exchange', + alertContent: 'Exchange for this asset is not supported yet.', + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + break; + default: + await Navigator.of(context).pushNamed(Routes.exchange); + } + } +} diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 8ca117ed4..66d16c5eb 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -6,17 +6,25 @@ class ActionButton extends StatelessWidget { @required this.title, this.route, this.onClick, - this.alignment = Alignment.center}); + this.alignment = Alignment.center, + this.textColor}); final Image image; final String title; final String route; final Alignment alignment; final void Function() onClick; + final Color textColor; @override Widget build(BuildContext context) { + var _textColor = textColor ?? Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor; + return Container( + padding: EdgeInsets.only(top: 14, bottom: 16, left: 10, right: 10), alignment: alignment, child: Column( mainAxisSize: MainAxisSize.max, @@ -42,8 +50,7 @@ class ActionButton extends StatelessWidget { title, style: TextStyle( fontSize: 10, - color: Theme.of(context).accentTextTheme.display3 - .backgroundColor), + color: _textColor), ) ], ), diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index da9391131..d67e636f6 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -15,7 +16,7 @@ import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:mobx/mobx.dart'; -class AddressPage extends StatelessWidget { +class AddressPage extends BasePage { AddressPage({@required this.addressListViewModel, this.walletViewModel}) : _cryptoAmountFocus = FocusNode(); @@ -26,7 +27,64 @@ class AddressPage extends StatelessWidget { final FocusNode _cryptoAmountFocus; @override - Widget build(BuildContext context) { + String get title => S.current.receive; + + @override + Color get backgroundLightColor => currentTheme.type == ThemeType.bright + ? Colors.transparent : Colors.white; + + @override + Color get backgroundDarkColor => Colors.transparent; + + @override + bool get resizeToAvoidBottomInset => false; + + @override + Widget leading(BuildContext context) { + final _backButton = Icon(Icons.arrow_back_ios, + color: Theme.of(context).accentTextTheme.display3.backgroundColor, + size: 16,); + + return SizedBox( + height: 37, + width: 37, + child: ButtonTheme( + minWidth: double.minPositive, + child: FlatButton( + highlightColor: Colors.transparent, + splashColor: Colors.transparent, + padding: EdgeInsets.all(0), + onPressed: () => onClose(context), + child: _backButton), + ), + ); + } + + @override + Widget middle(BuildContext context) { + return Text( + title, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + fontFamily: 'Lato', + color: Theme.of(context).accentTextTheme.display3.backgroundColor), + ); + } + + @override + Widget Function(BuildContext, Widget) get rootWrapper => + (BuildContext context, Widget scaffold) => Container( + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + Theme.of(context).accentColor, + Theme.of(context).scaffoldBackgroundColor, + Theme.of(context).primaryColor, + ], begin: Alignment.topRight, end: Alignment.bottomLeft)), + child: scaffold); + + @override + Widget body(BuildContext context) { autorun((_) async { if (!walletViewModel.isOutdatedElectrumWallet || !walletViewModel.settingsStore.shouldShowReceiveWarning) { @@ -66,7 +124,6 @@ class AddressPage extends StatelessWidget { ) ]), child: Container( - height: 1, padding: EdgeInsets.fromLTRB(24, 24, 24, 32), child: Column( children: [ diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index e4186ad9b..b3e68e691 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:flutter/material.dart'; @@ -15,23 +16,21 @@ class BalancePage extends StatelessWidget{ Color get backgroundLightColor => settingsStore.currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; + @override Widget build(BuildContext context) { return GestureDetector( - onLongPress: () => - dashboardViewModel.balanceViewModel.isReversing = - !dashboardViewModel.balanceViewModel.isReversing, - onLongPressUp: () => - dashboardViewModel.balanceViewModel.isReversing = - !dashboardViewModel.balanceViewModel.isReversing, + onLongPress: () => dashboardViewModel.balanceViewModel.isReversing = !dashboardViewModel.balanceViewModel.isReversing, + onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = !dashboardViewModel.balanceViewModel.isReversing, + child: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 56), Container( - alignment: Alignment.topLeft, margin: const EdgeInsets.only(left: 24, bottom: 16), child: Observer(builder: (_) { - return AutoSizeText( + return Text( dashboardViewModel.balanceViewModel.asset, style: TextStyle( fontSize: 24, @@ -44,162 +43,140 @@ class BalancePage extends StatelessWidget{ height: 1), maxLines: 1, textAlign: TextAlign.center); - })), - - Container( - margin: const EdgeInsets.only(left: 16, right: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all(color: settingsStore.currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), - color:Theme.of(context).textTheme.title.backgroundColor - ), - child: Container( - margin: const EdgeInsets.only(top: 24, left: 24, right: 24, bottom: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 8,), - Observer(builder: (_) { - return Column( - children: [ - Text( - '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme - .display2 - .backgroundColor, - height: 1), - ) - ], - ); - }), - SizedBox(height: 8,), - Observer(builder: (_) { - return AutoSizeText( - dashboardViewModel.balanceViewModel.availableBalance, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - - fontWeight: FontWeight.w900, - color: Theme.of(context) - .accentTextTheme - .display3 - .backgroundColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.center); - }), - SizedBox(height: 4,), - Observer(builder: (_) { - return Column( - children: [ - Text( - '${dashboardViewModel.balanceViewModel.availableFiatBalance.toString()}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme - .display3 - .backgroundColor, - height: 1), - ) - ], - ); - }), - SizedBox(height: 26), - Observer(builder: (_) { - return Column( - children: [ - Text( - '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme - .display2 - .backgroundColor, - height: 1), - ) - ], - ); - }), - SizedBox(height: 8), - Observer(builder: (_) { - return AutoSizeText( - dashboardViewModel.balanceViewModel.additionalBalance - .toString(), - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w900, - color: Theme.of(context) - .accentTextTheme - .display3 - .backgroundColor, - height: 1), - maxLines: 1, - textAlign: TextAlign.center); - }), - SizedBox(height: 4,), - Observer(builder: (_) { - return Column( - children: [ - Text( - '${dashboardViewModel.balanceViewModel.additionalFiatBalance.toString()}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme - .display3 - .backgroundColor, - height: 1), - ) - ], - ); - }), - ], - ), - Observer(builder: (_) { - return Text( - dashboardViewModel.balanceViewModel.currency.toString(), - style: TextStyle( - fontSize: 28, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context) - .accentTextTheme - .display3 - .backgroundColor, - height: 1), - ); - }), - ], - ), - ), - ), - - ], + })), + Observer(builder: (_) { + return ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), + itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, + itemBuilder: (__, index) { + final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); + return buildBalanceRow(context, + availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', + availableBalance: balance.availableBalance, + availableFiatBalance: balance.fiatAvailableBalance, + additionalBalanceLabel: '${dashboardViewModel.balanceViewModel.additionalBalanceLabel}', + additionalBalance: balance.additionalBalance, + additionalFiatBalance: balance.fiatAdditionalBalance, + currency: balance.formattedAssetTitle); + }); + }) + ]))); + } + + Widget buildBalanceRow(BuildContext context, + {String availableBalanceLabel, + String availableBalance, + String availableFiatBalance, + String additionalBalanceLabel, + String additionalBalance, + String additionalFiatBalance, + String currency}) { + return Container( + margin: const EdgeInsets.only(left: 16, right: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + border: Border.all(color: settingsStore.currentTheme.type == ThemeType.bright ? Color.fromRGBO(255, 255, 255, 0.2): Colors.transparent, width: 1, ), + color:Theme.of(context).textTheme.title.backgroundColor ), - + child: Container( + margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4,), + Text('${availableBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor, + height: 1)), + SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AutoSizeText( + availableBalance, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w900, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.center), + Text(currency, + style: TextStyle( + fontSize: 28, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1)), + ]), + SizedBox(height: 4,), + Text('${availableFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1)), + SizedBox(height: 26), + Text('${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .accentTextTheme + .display2 + .backgroundColor, + height: 1)), + SizedBox(height: 8), + AutoSizeText( + additionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1), + maxLines: 1, + textAlign: TextAlign.center), + SizedBox(height: 4,), + Text('${additionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context) + .accentTextTheme + .display3 + .backgroundColor, + height: 1), + ) + ])), ); } } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 480dddd32..d4e2673e6 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -22,6 +22,7 @@ class MenuWidgetState extends State { Image moneroIcon; Image bitcoinIcon; Image litecoinIcon; + Image havenIcon; final largeScreen = 731; double menuWidth; @@ -78,6 +79,7 @@ class MenuWidgetState extends State { bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', color: Theme.of(context).accentTextTheme.overline.decorationColor); litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); + havenIcon = Image.asset('assets/images/haven_menu.png'); return Row( mainAxisSize: MainAxisSize.max, @@ -242,6 +244,8 @@ class MenuWidgetState extends State { return bitcoinIcon; case WalletType.litecoin: return litecoinIcon; + case WalletType.haven: + return havenIcon; default: return null; } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index d1838c550..c4220cefd 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -65,6 +65,8 @@ class WalletTypeFormState extends State { final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); + final havenIcon = + Image.asset('assets/images/haven_logo.png', height: 24, width: 24); WalletType selected; List types; @@ -129,6 +131,8 @@ class WalletTypeFormState extends State { return bitcoinIcon; case WalletType.litecoin: return litecoinIcon; + case WalletType.haven: + return havenIcon; default: return null; } diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index cf5d20426..9e86f2b1c 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -109,7 +109,7 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { - return addressListViewModel.type == WalletType.monero + return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) ? KeyboardActions( config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index b85979f5b..1bfbd6230 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -133,54 +133,7 @@ class QRWidget extends StatelessWidget { ], ), ))), - ), - Observer(builder: (_) { - return addressListViewModel.emoji.isNotEmpty - ? Padding( - padding: EdgeInsets.only(bottom: 10), - child: Builder( - builder: (context) => GestureDetector( - onTap: () { - Clipboard.setData(ClipboardData( - text: addressListViewModel.emoji)); - showBar( - context, S.of(context).copied_to_clipboard); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - S.of(context).yat, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.normal, - color: Theme.of(context).accentTextTheme. - display3.backgroundColor), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child:Text( - addressListViewModel.emoji, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 26))), - Padding( - padding: EdgeInsets.only(left: 12), - child: copyImage, - )] - ), - ) - ] - ) - )), - ) - : Container(); - }) + ) ], ); } diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index a0d314ec2..a54d48ee7 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -130,7 +130,8 @@ class WalletRestoreFromSeedFormState extends State { BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, key: blockchainHeightKey, - onHeightOrDateEntered: widget.onHeightOrDateEntered) + onHeightOrDateEntered: widget.onHeightOrDateEntered, + hasDatePicker: widget.type == WalletType.monero) ])); } diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 1dbb1b8b7..65b6c03ec 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -215,7 +215,7 @@ class WalletRestorePage extends BasePage { .text .split(' '); - if (walletRestoreViewModel.type == WalletType.monero && + if ((walletRestoreViewModel.type == WalletType.monero || walletRestoreViewModel.type == WalletType.haven) && seedWords.length != WalletRestoreViewModelBase.moneroSeedMnemonicLength) { return false; } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 8340641a4..4a423a263 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/view_model/send/output.dart'; import 'package:flutter/cupertino.dart'; @@ -22,6 +23,7 @@ import 'package:dotted_border/dotted_border.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:cw_core/crypto_currency.dart'; class SendPage extends BasePage { SendPage({@required this.sendViewModel}) : _formKey = GlobalKey(); @@ -140,6 +142,7 @@ class SendPage extends BasePage { ), ), ), + if (sendViewModel.hasMultiRecipient) Container( height: 40, width: double.infinity, @@ -256,27 +259,43 @@ class SendPage extends BasePage { EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ - Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () { - sendViewModel.addOutput(); - Future.delayed(const Duration(milliseconds: 250), () { - controller.jumpToPage(sendViewModel.outputs.length - 1); - }); - }, - text: S.of(context).add_receiver, - color: Colors.transparent, - textColor: Theme.of(context) - .accentTextTheme - .display2 - .decorationColor, - isDottedBorder: true, - borderColor: Theme.of(context) - .primaryTextTheme - .display2 - .decorationColor, - )), + if (sendViewModel.hasCurrecyChanger) + Observer(builder: (_) => + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context) + .accentTextTheme + .display2 + .decorationColor, + ) + ) + ), + if (sendViewModel.hasMultiRecipient) + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () { + sendViewModel.addOutput(); + Future.delayed(const Duration(milliseconds: 250), () { + controller.jumpToPage(sendViewModel.outputs.length - 1); + }); + }, + text: S.of(context).add_receiver, + color: Colors.transparent, + textColor: Theme.of(context) + .accentTextTheme + .display2 + .decorationColor, + isDottedBorder: true, + borderColor: Theme.of(context) + .primaryTextTheme + .display2 + .decorationColor, + )), Observer( builder: (_) { return LoadingPrimaryButton( @@ -298,7 +317,7 @@ class SendPage extends BasePage { showErrorValidationAlert(context); return; } - + await sendViewModel.createTransaction(); }, @@ -377,7 +396,7 @@ class SendPage extends BasePage { return AlertWithOneAction( alertTitle: '', alertContent: S.of(context).send_success( - sendViewModel.currency.toString()), + sendViewModel.selectedCryptoCurrency.toString()), buttonText: S.of(context).ok, buttonAction: () => Navigator.of(context).pop()); @@ -418,4 +437,17 @@ class SendPage extends BasePage { buttonAction: () => Navigator.of(context).pop()); }); } + + void presentCurrencyPicker(BuildContext context) async { + await showPopUp( + builder: (_) => Picker( + items: sendViewModel.currencies, + displayItem: (Object item) => item.toString(), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (CryptoCurrency cur) => sendViewModel.selectedCryptoCurrency = cur, + ), + context: context); + } } diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0e145d9b5..7782524f2 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -196,7 +196,7 @@ class SendCardState extends State prefixIcon: Padding( padding: EdgeInsets.only(top: 9), child: Text( - sendViewModel.currency.title + + sendViewModel.selectedCryptoCurrency.title + ':', style: TextStyle( fontSize: 16, @@ -391,7 +391,7 @@ class SendCardState extends State .toString() + ' ' + sendViewModel - .currency.title, + .selectedCryptoCurrency.toString(), style: TextStyle( fontSize: 12, fontWeight: diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 7194a4135..d583f4a71 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -45,6 +45,8 @@ class WalletListBodyState extends State { Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); + final havenIcon = + Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar _progressBar; @@ -176,12 +178,12 @@ class WalletListBodyState extends State { bottomSection: Column(children: [ PrimaryImageButton( onPressed: () { - if (isMoneroOnly) { - Navigator.of(context).pushNamed(Routes.newWallet, arguments: WalletType.monero); - } else { - Navigator.of(context).pushNamed(Routes.newWalletType); - } - }, + if (isSingleCoin) { + Navigator.of(context).pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } + }, image: newWalletImage, text: S.of(context).wallet_list_create_new_wallet, color: Theme.of(context).accentTextTheme.body2.color, @@ -190,7 +192,7 @@ class WalletListBodyState extends State { SizedBox(height: 10.0), PrimaryImageButton( onPressed: () { - if (isMoneroOnly) { + if (isSingleCoin) { Navigator .of(context) .pushNamed( @@ -216,6 +218,8 @@ class WalletListBodyState extends State { return moneroIcon; case WalletType.litecoin: return litecoinIcon; + case WalletType.haven: + return havenIcon; default: return nonWalletTypeIcon; } @@ -264,18 +268,6 @@ class WalletListBodyState extends State { }); } - Future _generateNewWallet() async { - try { - changeProcessText(S.of(context).creating_new_wallet); - await widget.walletListViewModel.walletNewVM - .create(options: 'English'); // FIXME: Unnamed constant - hideProgressText(); - await Navigator.of(context).pushNamed(Routes.preSeed); - } catch (e) { - changeProcessText(S.of(context).creating_new_wallet_error(e.toString())); - } - } - void changeProcessText(String text) { _progressBar = createBar(text, duration: null)..show(context); } diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index a8a185e05..394b6fea8 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -11,6 +11,30 @@ class WelcomePage extends BasePage { final welcomeImageLight = Image.asset('assets/images/welcome_light.png'); final welcomeImageDark = Image.asset('assets/images/welcome.png'); + String appTitle(BuildContext context) { + if (isMoneroOnly) { + return S.of(context).monero_com; + } + + if (isHaven) { + return S.of(context).haven_app; + } + + return S.of(context).cake_wallet; + } + + String appDescription(BuildContext context) { + if (isMoneroOnly) { + return S.of(context).monero_com_wallet_text; + } + + if (isHaven) { + return S.of(context).haven_app_wallet_text; + } + + return S.of(context).first_wallet_text; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -83,9 +107,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 5), child: Text( - isMoneroOnly - ? S.of(context).monero_com - : S.of(context).cake_wallet, + appTitle(context), style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, @@ -101,9 +123,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 5), child: Text( - isMoneroOnly - ? S.of(context).monero_com_wallet_text - : S.of(context).first_wallet_text, + appDescription(context), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index b87e83bc5..dab3fdd26 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -7,12 +7,13 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; class BlockchainHeightWidget extends StatefulWidget { BlockchainHeightWidget({GlobalKey key, this.onHeightChange, this.focusNode, - this.onHeightOrDateEntered}) + this.onHeightOrDateEntered, this.hasDatePicker}) : super(key: key); final Function(int) onHeightChange; final Function(bool) onHeightOrDateEntered; final FocusNode focusNode; + final bool hasDatePicker; @override State createState() => BlockchainHeightState(); @@ -67,43 +68,45 @@ class BlockchainHeightState extends State { ))) ], ), - Padding( - padding: EdgeInsets.only(top: 15, bottom: 15), - child: Text( - S.of(context).widgets_or, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).primaryTextTheme.title.color), - ), - ), - Row( - children: [ - Flexible( - child: Container( - child: InkWell( - onTap: () => _selectDate(context), - child: IgnorePointer( - child: BaseTextFormField( - controller: dateController, - hintText: S.of(context).widgets_restore_from_date, - )), - ), - )) - ], - ), - Padding( - padding: EdgeInsets.only(left: 40, right: 40, top: 24), - child: Text( - S.of(context).restore_from_date_or_blockheight, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: Theme.of(context).hintColor + if (widget.hasDatePicker) ...[ + Padding( + padding: EdgeInsets.only(top: 15, bottom: 15), + child: Text( + S.of(context).widgets_or, + style: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.title.color), ), ), - ) + Row( + children: [ + Flexible( + child: Container( + child: InkWell( + onTap: () => _selectDate(context), + child: IgnorePointer( + child: BaseTextFormField( + controller: dateController, + hintText: S.of(context).widgets_restore_from_date, + )), + ), + )) + ], + ), + Padding( + padding: EdgeInsets.only(left: 40, right: 40, top: 24), + child: Text( + S.of(context).restore_from_date_or_blockheight, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: Theme.of(context).hintColor + ), + ), + ) + ] ], ); } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 40f700192..373ba0aca 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -227,9 +227,12 @@ abstract class SettingsStoreBase with Store { .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); final litecoinElectrumServerId = sharedPreferences .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final havenNodeId = sharedPreferences + .getInt(PreferencesKey.currentHavenNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); + final havenNode = nodeSource.get(havenNodeId); final packageInfo = await PackageInfo.fromPlatform(); final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -239,7 +242,8 @@ abstract class SettingsStoreBase with Store { nodes: { WalletType.monero: moneroNode, WalletType.bitcoin: bitcoinElectrumServer, - WalletType.litecoin: litecoinElectrumServer + WalletType.litecoin: litecoinElectrumServer, + WalletType.haven: havenNode }, appVersion: packageInfo.version, isBitcoinBuyEnabled: isBitcoinBuyEnabled, diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 77203bf17..7bd8a9284 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -15,6 +15,21 @@ import 'package:mobx/mobx.dart'; part 'balance_view_model.g.dart'; +class BalanceRecord { + const BalanceRecord({this.availableBalance, + this.additionalBalance, + this.fiatAvailableBalance, + this.fiatAdditionalBalance, + this.asset, + this.formattedAssetTitle}); + final String fiatAdditionalBalance; + final String fiatAvailableBalance; + final String additionalBalance; + final String availableBalance; + final CryptoCurrency asset; + final String formattedAssetTitle; +} + class BalanceViewModel = BalanceViewModelBase with _$BalanceViewModel; abstract class BalanceViewModelBase with Store { @@ -24,16 +39,7 @@ abstract class BalanceViewModelBase with Store { @required this.fiatConvertationStore}) { isReversing = false; wallet ??= appStore.wallet; - balance = wallet.balance; - reaction((_) => appStore.wallet, _onWalletChange); - - _onCurrentWalletChangeReaction = - reaction((_) => wallet.balance, (dynamic balance) { - if (balance is Balance) { - this.balance = balance; - } - }); } final AppStore appStore; @@ -45,9 +51,6 @@ abstract class BalanceViewModelBase with Store { @observable bool isReversing; - @observable - Balance balance; - @observable WalletBase, TransactionInfo> wallet; @@ -58,47 +61,56 @@ abstract class BalanceViewModelBase with Store { @computed BalanceDisplayMode get savedDisplayMode => settingsStore.balanceDisplayMode; - @computed + @computed String get asset { - - switch(appStore.wallet.currency){ - case CryptoCurrency.btc: - return 'Bitcoin Assets'; - case CryptoCurrency.xmr: - return 'Monero Assets'; - case CryptoCurrency.ltc: - return 'Litecoin Assets'; + final typeFormatted = walletTypeToString(appStore.wallet.type); + + switch(wallet.type) { + case WalletType.haven: + return '$typeFormatted Assets'; default: - return ''; + return typeFormatted; } - } @computed - BalanceDisplayMode get displayMode => isReversing - ? savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? BalanceDisplayMode.displayableBalance - : savedDisplayMode - : savedDisplayMode; + BalanceDisplayMode get displayMode { + if (isReversing) { + if (savedDisplayMode == BalanceDisplayMode.hiddenBalance) { + return BalanceDisplayMode.displayableBalance; + } else { + return BalanceDisplayMode.hiddenBalance; + } + } + + return savedDisplayMode; + } @computed String get availableBalanceLabel { - if (wallet.type == WalletType.monero) { - return S.current.xmr_available_balance; + switch(wallet.type) { + case WalletType.monero: + case WalletType.haven: + return S.current.xmr_available_balance; + default: + return S.current.confirmed; } - - return S.current.confirmed; } @computed String get additionalBalanceLabel { - if (wallet.type == WalletType.monero) { - return S.current.xmr_full_balance; + switch(wallet.type) { + case WalletType.monero: + case WalletType.haven: + return S.current.xmr_full_balance; + default: + return S.current.unconfirmed; } - - return S.current.unconfirmed; } + @computed + bool get hasMultiBalance => appStore.wallet.type == WalletType.haven; + @computed String get availableBalance { final walletBalance = _walletBalance; @@ -152,7 +164,73 @@ abstract class BalanceViewModelBase with Store { } @computed - Balance get _walletBalance => wallet.balance; + Map get balances { + return wallet.balance.map((key, value) { + if (displayMode == BalanceDisplayMode.hiddenBalance) { + return MapEntry(key, BalanceRecord( + availableBalance: '---', + additionalBalance: '---', + fiatAdditionalBalance: '---', + fiatAvailableBalance: '---', + asset: key, + formattedAssetTitle: _formatterAsset(key))); + } + final fiatCurrency = settingsStore.fiatCurrency; + final additionalFiatBalance = fiatCurrency.toString() + + ' ' + + _getFiatBalance( + price: fiatConvertationStore.prices[key], + cryptoAmount: value.formattedAdditionalBalance); + + final availableFiatBalance = fiatCurrency.toString() + + ' ' + + _getFiatBalance( + price: fiatConvertationStore.prices[key], + cryptoAmount: value.formattedAvailableBalance); + + return MapEntry(key, BalanceRecord( + availableBalance: value.formattedAvailableBalance, + additionalBalance: value.formattedAdditionalBalance, + fiatAdditionalBalance: additionalFiatBalance, + fiatAvailableBalance: availableFiatBalance, + asset: key, + formattedAssetTitle: _formatterAsset(key))); + }); + } + + @computed + List get formattedBalances { + final balance = balances.values.toList(); + + balance.sort((BalanceRecord a, BalanceRecord b) { + if (b.asset == CryptoCurrency.xhv) { + return 1; + } + + if (b.asset == CryptoCurrency.xusd) { + if (a.asset == CryptoCurrency.xhv) { + return -1; + } + + return 1; + } + + if (b.asset == CryptoCurrency.xbtc) { + return 1; + } + + if (b.asset == CryptoCurrency.xeur) { + return 1; + } + + return 0; + }); + + return balance; + } + + @computed + Balance get _walletBalance => wallet.balance[wallet.currency]; @computed CryptoCurrency get currency => appStore.wallet.currency; @@ -164,11 +242,8 @@ abstract class BalanceViewModelBase with Store { WalletBase, TransactionInfo> wallet) { - this.wallet = wallet; - balance = wallet.balance; + this.wallet = wallet; _onCurrentWalletChangeReaction?.reaction?.dispose(); - _onCurrentWalletChangeReaction = reaction( - (_) => wallet.balance, (Balance balance) => this.balance = balance); } String _getFiatBalance({double price, String cryptoAmount}) { @@ -178,5 +253,20 @@ abstract class BalanceViewModelBase with Store { return calculateFiatAmount(price: price, cryptoAmount: cryptoAmount); } + + String _formatterAsset(CryptoCurrency asset) { + switch (wallet.type) { + case WalletType.haven: + final assetStringified = asset.toString(); + + if (asset != CryptoCurrency.xhv && assetStringified[0].toUpperCase() == 'X') { + return assetStringified.replaceFirst('X', 'x'); + } + + return asset.toString(); + default: + return asset.toString(); + } + } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 8fc0e08a6..1a7a44406 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cake_wallet/buy/order.dart'; @@ -78,6 +79,8 @@ abstract class DashboardViewModelBase with Store { isShowFirstYatIntroduction = false; isShowSecondYatIntroduction = false; isShowThirdYatIntroduction = false; + updateActions(); + final _wallet = wallet; if (_wallet.type == WalletType.monero) { @@ -229,6 +232,24 @@ abstract class DashboardViewModelBase with Store { void furtherShowYatPopup(bool shouldShow) => settingsStore.shouldShowYatPopup = shouldShow; + @observable + bool isEnabledExchangeAction; + + @observable + bool hasExchangeAction; + + @observable + bool isEnabledBuyAction; + + @observable + bool hasBuyAction; + + @observable + bool isEnabledSellAction; + + @observable + bool hasSellAction; + ReactionDisposer _onMoneroAccountChangeReaction; ReactionDisposer _onMoneroBalanceChangeReaction; @@ -251,6 +272,7 @@ abstract class DashboardViewModelBase with Store { name = wallet.name; isOutdatedElectrumWallet = wallet.type == WalletType.bitcoin && wallet.seed.split(' ').length < 24; + updateActions(); if (wallet.type == WalletType.monero) { subname = monero.getCurrentAccount(wallet)?.label; @@ -313,4 +335,16 @@ abstract class DashboardViewModelBase with Store { balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } + + void updateActions() { + isEnabledExchangeAction = wallet.type != WalletType.haven; + hasExchangeAction = !isHaven; + isEnabledBuyAction = wallet.type != WalletType.haven + && wallet.type != WalletType.monero; + hasBuyAction = !isMoneroOnly && !isHaven; + isEnabledSellAction = wallet.type != WalletType.haven + && wallet.type != WalletType.monero + && wallet.type != WalletType.litecoin; + hasSellAction = !isMoneroOnly && !isHaven; + } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 989de3e3f..a7467ffb0 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,15 +1,18 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/keyable.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_haven/haven_transaction_info.dart'; class TransactionListItem extends ActionListItem with Keyable { TransactionListItem( @@ -35,21 +38,33 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedFiatAmount { - if (balanceViewModel.wallet.type == WalletType.monero) { - final amount = calculateFiatAmountRaw( + var amount = ''; + + switch(balanceViewModel.wallet.type) { + case WalletType.monero: + amount = calculateFiatAmountRaw( cryptoAmount: monero.formatterMoneroAmountToDouble(amount: transaction.amount), price: price); - transaction.changeFiatAmount(amount); - } - - if (balanceViewModel.wallet.type == WalletType.bitcoin - || balanceViewModel.wallet.type == WalletType.litecoin) { - final amount = calculateFiatAmountRaw( + break; + case WalletType.bitcoin: + case WalletType.litecoin: + amount = calculateFiatAmountRaw( cryptoAmount: bitcoin.formatterBitcoinAmountToDouble(amount: transaction.amount), price: price); - transaction.changeFiatAmount(amount); + break; + case WalletType.haven: + final tx = transaction as HavenTransactionInfo; + final asset = CryptoCurrency.fromString(tx.assetType); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: haven.formatterMoneroAmountToDouble(amount: transaction.amount), + price: price); + break; + default: + break; } + transaction.changeFiatAmount(amount); return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : fiatCurrency.title + ' ' + transaction.fiatAmount(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 9e4e8d861..030556fbc 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -338,7 +338,7 @@ abstract class ExchangeViewModelBase with Store { @action void calculateDepositAllAmount() { if (wallet.type == WalletType.bitcoin) { - final availableBalance = wallet.balance.available; + final availableBalance = wallet.balance[wallet.currency].available; final priority = _settingsStore.priority[wallet.type]; final fee = wallet.calculateEstimatedFee(priority, null); diff --git a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart index d9a73c325..63415cd39 100644 --- a/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart @@ -1,8 +1,10 @@ import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; part 'monero_account_edit_or_create_view_model.g.dart'; @@ -11,7 +13,7 @@ class MoneroAccountEditOrCreateViewModel = MoneroAccountEditOrCreateViewModelBas with _$MoneroAccountEditOrCreateViewModel; abstract class MoneroAccountEditOrCreateViewModelBase with Store { - MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, + MoneroAccountEditOrCreateViewModelBase(this._moneroAccountList, this._havenAccountList, {@required WalletBase wallet, AccountListItem accountListItem}) : state = InitialExecutionState(), isEdit = accountListItem != null, @@ -28,10 +30,21 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { String label; final MoneroAccountList _moneroAccountList; + final HavenAccountList _havenAccountList; final AccountListItem _accountListItem; final WalletBase _wallet; Future save() async { + if (_wallet.type == WalletType.monero) { + await saveMonero(); + } + + if (_wallet.type == WalletType.haven) { + await saveHaven(); + } + } + + Future saveMonero() async { try { state = IsExecutingState(); @@ -52,4 +65,30 @@ abstract class MoneroAccountEditOrCreateViewModelBase with Store { state = FailureState(e.toString()); } } + + Future saveHaven() async { + if (!(_wallet.type == WalletType.haven)) { + return; + } + + try { + state = IsExecutingState(); + + if (_accountListItem != null) { + await _havenAccountList.setLabelAccount( + _wallet, + accountIndex: _accountListItem.id, + label: label); + } else { + await _havenAccountList.addAccount( + _wallet, + label: label); + } + + await _wallet.save(); + state = ExecutedSuccessfullyState(); + } catch (e) { + state = FailureState(e.toString()); + } + } } diff --git a/lib/view_model/monero_account_list/monero_account_list_view_model.dart b/lib/view_model/monero_account_list/monero_account_list_view_model.dart index 679c3985b..b7735ff60 100644 --- a/lib/view_model/monero_account_list/monero_account_list_view_model.dart +++ b/lib/view_model/monero_account_list/monero_account_list_view_model.dart @@ -1,7 +1,9 @@ +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'monero_account_list_view_model.g.dart'; @@ -20,20 +22,43 @@ abstract class MoneroAccountListViewModelBase with Store { } @computed - List get accounts => monero - .getAccountList(_wallet) - .accounts.map((acc) => AccountListItem( - label: acc.label, - id: acc.id, - isSelected: acc.id == monero.getCurrentAccount(_wallet).id)) - .toList(); + List get accounts { + if (_wallet.type == WalletType.haven) { + return haven + .getAccountList(_wallet) + .accounts.map((acc) => AccountListItem( + label: acc.label, + id: acc.id, + isSelected: acc.id == haven.getCurrentAccount(_wallet).id)) + .toList(); + } + + if (_wallet.type == WalletType.monero) { + return monero + .getAccountList(_wallet) + .accounts.map((acc) => AccountListItem( + label: acc.label, + id: acc.id, + isSelected: acc.id == monero.getCurrentAccount(_wallet).id)) + .toList(); + } + } final WalletBase _wallet; - void select(AccountListItem item) => + void select(AccountListItem item) { + if (_wallet.type == WalletType.monero) { monero.setCurrentAccount( _wallet, - Account( - id: item.id, - label: item.label)); + item.id, + item.label); + } + + if (_wallet.type == WalletType.haven) { + haven.setCurrentAccount( + _wallet, + item.id, + item.label); + } + } } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index a097e59cf..ef7e03189 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -41,7 +41,8 @@ abstract class NodeCreateOrEditViewModelBase with Store { bool get isReady => (address?.isNotEmpty ?? false) && (port?.isNotEmpty ?? false); - bool get hasAuthCredentials => _wallet.type == WalletType.monero; + bool get hasAuthCredentials => _wallet.type == WalletType.monero || + _wallet.type == WalletType.haven; String get uri { var uri = address; diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index f8161a55f..39c2fa837 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -2,7 +2,9 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; @@ -22,7 +24,7 @@ const String cryptoNumberPattern = '0.0'; class Output = OutputBase with _$Output; abstract class OutputBase with Store { - OutputBase(this._wallet, this._settingsStore, this._fiatConversationStore) + OutputBase(this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler) : _cryptoNumberFormat = NumberFormat(cryptoNumberPattern) { reset(); _setCryptoNumMaximumFractionDigits(); @@ -77,6 +79,9 @@ abstract class OutputBase with Store { _amount = bitcoin.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.haven: + _amount = haven.formatterMoneroParseAmount(amount: _cryptoAmount); + break; default: break; } @@ -106,6 +111,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.monero) { return monero.formatterMoneroAmountToDouble(amount: fee); } + + if (_wallet.type == WalletType.haven) { + return haven.formatterMoneroAmountToDouble(amount: fee); + } } catch (e) { print(e.toString()); } @@ -117,7 +126,7 @@ abstract class OutputBase with Store { String get estimatedFeeFiatAmount { try { final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[_wallet.currency], + price: _fiatConversationStore.prices[cryptoCurrencyHandler()], cryptoAmount: estimatedFee); return fiat; } catch (_) { @@ -126,6 +135,7 @@ abstract class OutputBase with Store { } WalletType get walletType => _wallet.type; + final CryptoCurrency Function() cryptoCurrencyHandler; final WalletBase _wallet; final SettingsStore _settingsStore; final FiatConversionStore _fiatConversationStore; @@ -169,7 +179,7 @@ abstract class OutputBase with Store { void _updateFiatAmount() { try { final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[_wallet.currency], + price: _fiatConversationStore.prices[cryptoCurrencyHandler()], cryptoAmount: cryptoAmount.replaceAll(',', '.')); if (fiatAmount != fiat) { fiatAmount = fiat; @@ -183,7 +193,7 @@ abstract class OutputBase with Store { void _updateCryptoAmount() { try { final crypto = double.parse(fiatAmount.replaceAll(',', '.')) / - _fiatConversationStore.prices[_wallet.currency]; + _fiatConversationStore.prices[cryptoCurrencyHandler()]; final cryptoAmountTmp = _cryptoNumberFormat.format(crypto); if (cryptoAmount != cryptoAmountTmp) { @@ -207,6 +217,9 @@ abstract class OutputBase with Store { case WalletType.litecoin: maximumFractionDigits = 8; break; + case WalletType.haven: + maximumFractionDigits = 12; + break; default: break; } @@ -216,7 +229,7 @@ abstract class OutputBase with Store { Future fetchParsedAddress(BuildContext context) async { final domain = address; - final ticker = _wallet.currency.title.toLowerCase(); + final ticker = cryptoCurrencyHandler().title.toLowerCase(); parsedAddress = await getIt.get().resolve(domain, ticker); extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 9e1e49f06..e52022f8d 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -21,7 +21,7 @@ abstract class SendTemplateViewModelBase with Store { SendTemplateViewModelBase(this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore) { - output = Output(_wallet, _settingsStore, _fiatConversationStore); + output = Output(_wallet, _settingsStore, _fiatConversationStore, () => currency); } Output output; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index b4ef7eaad..5c4a15c27 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -24,6 +24,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'send_view_model.g.dart'; @@ -39,13 +40,15 @@ abstract class SendViewModelBase with Store { : state = InitialExecutionState() { final priority = _settingsStore.priority[_wallet.type]; final priorities = priorityForWalletType(_wallet.type); + selectedCryptoCurrency = _wallet.currency; + currencies = _wallet.balance.keys.toList(); if (!priorityForWalletType(_wallet.type).contains(priority)) { _settingsStore.priority[_wallet.type] = priorities.first; } outputs = ObservableList() - ..add(Output(_wallet, _settingsStore, _fiatConversationStore)); + ..add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @observable @@ -55,7 +58,7 @@ abstract class SendViewModelBase with Store { @action void addOutput() { - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore)); + outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @action @@ -79,7 +82,7 @@ abstract class SendViewModelBase with Store { try { if (pendingTransaction != null) { final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[_wallet.currency], + price: _fiatConversationStore.prices[selectedCryptoCurrency], cryptoAmount: pendingTransaction.amountFormatted); return fiat; } else { @@ -95,7 +98,7 @@ abstract class SendViewModelBase with Store { try { if (pendingTransaction != null) { final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[_wallet.currency], + price: _fiatConversationStore.prices[selectedCryptoCurrency], cryptoAmount: pendingTransaction.feeFormatted); return fiat; } else { @@ -117,7 +120,7 @@ abstract class SendViewModelBase with Store { Validator get allAmountValidator => AllAmountValidator(); - Validator get addressValidator => AddressValidator(type: _wallet.currency); + Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); Validator get textValidator => TextValidator(); @@ -125,12 +128,7 @@ abstract class SendViewModelBase with Store { PendingTransaction pendingTransaction; @computed - String get balance { - if(_settingsStore.balanceDisplayMode == BalanceDisplayMode.hiddenBalance){ - return '---'; - } - return _wallet.balance.formattedAvailableBalance ?? '0.0' ; - } + String get balance => _wallet.balance[selectedCryptoCurrency].formattedAvailableBalance ?? '0.0'; @computed bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus; @@ -144,11 +142,21 @@ abstract class SendViewModelBase with Store { bool get isElectrumWallet => _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin; + @observable + CryptoCurrency selectedCryptoCurrency; + + List currencies; + + bool get hasMultiRecipient => _wallet.type != WalletType.haven; + bool get hasYat => outputs.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord); WalletType get walletType => _wallet.type; + + bool get hasCurrecyChanger => walletType == WalletType.haven; + final WalletBase _wallet; final SettingsStore _settingsStore; final SendTemplateViewModel sendTemplateViewModel; @@ -221,6 +229,11 @@ abstract class SendViewModelBase with Store { return monero.createMoneroTransactionCreationCredentials( outputs: outputs, priority: priority); + case WalletType.haven: + final priority = _settingsStore.priority[_wallet.type]; + + return haven.createHavenTransactionCreationCredentials( + outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title); default: return null; } diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 6f5bf8649..555b0d2a0 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cw_core/node.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/view_model/settings/version_list_item.dart'; import 'package:cake_wallet/view_model/settings/picker_list_item.dart'; @@ -29,6 +30,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; part 'settings_view_model.g.dart'; @@ -42,6 +44,8 @@ List priorityForWalletType(WalletType type) { return bitcoin.getTransactionPriorities(); case WalletType.litecoin: return bitcoin.getLitecoinTransactionPriorities(); + case WalletType.haven: + return haven.getTransactionPriorities(); default: return []; } @@ -101,12 +105,13 @@ abstract class SettingsViewModelBase with Store { selectedItem: () => balanceDisplayMode, onItemSelected: (BalanceDisplayMode mode) => _settingsStore.balanceDisplayMode = mode), - PickerListItem( - title: S.current.settings_currency, - items: FiatCurrency.all, - selectedItem: () => fiatCurrency, - onItemSelected: (FiatCurrency currency) => - setFiatCurrency(currency)), + if (!isHaven) + PickerListItem( + title: S.current.settings_currency, + items: FiatCurrency.all, + selectedItem: () => fiatCurrency, + onItemSelected: (FiatCurrency currency) => + setFiatCurrency(currency)), PickerListItem( title: S.current.settings_fee_priority, items: priorityForWalletType(wallet.type), diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index cc5061561..5c9b18caa 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -14,6 +14,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'transaction_details_view_model.g.dart'; @@ -100,6 +101,23 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + if (wallet.type == WalletType.haven) { + items.addAll([ + StandartListItem( + title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date)), + StandartListItem( + title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted()), + StandartListItem( + title: S.current.transaction_details_fee, value: tx.feeFormatted()), + ]); + } + if (showRecipientAddress && !isRecipientAddressShown) { final recipientAddress = transactionDescriptionBox.values .firstWhere((val) => val.id == transactionInfo.id, orElse: () => null) @@ -154,6 +172,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://www.blockchain.com/btc/tx/${txId}'; case WalletType.litecoin: return 'https://blockchair.com/litecoin/transaction/${txId}'; + case WalletType.haven: + return 'https://explorer.havenprotocol.org/search?value=${txId}'; default: return ''; } @@ -167,6 +187,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'View Transaction on Blockchain.com'; case WalletType.litecoin: return 'View Transaction on Blockchair.com'; + case WalletType.haven: + return 'View Transaction on explorer.havenprotocol.org'; default: return ''; } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 4edb995ee..4e7572bce 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; import 'package:cw_core/wallet_type.dart'; part 'wallet_address_edit_or_create_view_model.g.dart'; @@ -78,6 +79,16 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { label: label); await wallet.save(); } + + if (wallet.type == WalletType.haven) { + await haven + .getSubaddressList(wallet) + .addSubaddress( + wallet, + accountIndex: haven.getCurrentAccount(wallet).id, + label: label); + await wallet.save(); + } } Future _update() async { @@ -98,5 +109,16 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { label: label); await wallet.save(); } + + if (wallet.type == WalletType.haven) { + await haven + .getSubaddressList(wallet) + .setLabelSubaddress( + wallet, + accountIndex: haven.getCurrentAccount(wallet).id, + addressIndex: _item.id as int, + label: label); + await wallet.save(); + } } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index d3730223f..30675926a 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -15,6 +15,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'dart:async'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'wallet_address_list_view_model.g.dart'; @@ -44,6 +45,22 @@ class MoneroURI extends PaymentURI { } } +class HavenURI extends PaymentURI { + HavenURI({String amount, String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'haven:' + address; + + if (amount?.isNotEmpty ?? false) { + base += '?tx_amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + class BitcoinURI extends PaymentURI { BitcoinURI({String amount, String address}) : super(amount: amount, address: address); @@ -83,30 +100,7 @@ abstract class WalletAddressListViewModelBase with Store { }) { _appStore = appStore; _wallet = _appStore.wallet; - emoji = ''; - hasAccounts = _wallet?.type == WalletType.monero; - reaction((_) => _wallet.walletAddresses.address, (String address) { - if (address == _wallet.walletInfo.yatLastUsedAddress) { - emoji = yatStore.emoji; - } else { - emoji = ''; - } - }); - - //reaction((_) => yatStore.emoji, (String emojiId) => this.emoji = emojiId); - - //_onLastUsedYatAddressSubscription = - // _wallet.walletInfo.yatLastUsedAddressStream.listen((String yatAddress) { - // if (yatAddress == _wallet.walletAddresses.address) { - // emoji = yatStore.emoji; - // } else { - // emoji = ''; - // } - //}); - - if (_wallet.walletAddresses.address == _wallet.walletInfo.yatLastUsedAddress) { - emoji = yatStore.emoji; - } + hasAccounts = _wallet?.type == WalletType.monero || _wallet?.type == WalletType.haven; _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< Balance, TransactionHistoryBase, TransactionInfo> @@ -133,6 +127,10 @@ abstract class WalletAddressListViewModelBase with Store { return MoneroURI(amount: amount, address: address.address); } + if (_wallet.type == WalletType.haven) { + return HavenURI(amount: amount, address: address.address); + } + if (_wallet.type == WalletType.bitcoin) { return BitcoinURI(amount: amount, address: address.address); } @@ -170,6 +168,23 @@ abstract class WalletAddressListViewModelBase with Store { addressList.addAll(addressItems); } + if (wallet.type == WalletType.haven) { + final primaryAddress = haven.getSubaddressList(wallet).subaddresses.first; + final addressItems = haven + .getSubaddressList(wallet) + .subaddresses + .map((subaddress) { + final isPrimary = subaddress == primaryAddress; + + return WalletAddressListItem( + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address); + }); + addressList.addAll(addressItems); + } + if (wallet.type == WalletType.bitcoin) { final primaryAddress = bitcoin.getAddress(wallet); final bitcoinAddresses = bitcoin.getAddresses(wallet).map((addr) { @@ -195,14 +210,15 @@ abstract class WalletAddressListViewModelBase with Store { return monero.getCurrentAccount(wallet).label; } + if (wallet.type == WalletType.haven) { + return haven.getCurrentAccount(wallet).label; + } + return null; } @computed - bool get hasAddressList => _wallet.type == WalletType.monero; - - @observable - String emoji; + bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven; @observable WalletBase, TransactionInfo> @@ -216,9 +232,6 @@ abstract class WalletAddressListViewModelBase with Store { ReactionDisposer _onWalletChangeReaction; - StreamSubscription _onLastUsedYatAddressSubscription; - StreamSubscription _onEmojiIdChangeSubscription; - @action void setAddress(WalletAddressListItem address) => _wallet.walletAddresses.address = address.address; @@ -226,7 +239,7 @@ abstract class WalletAddressListViewModelBase with Store { void _init() { _baseItems = []; - if (_wallet.type == WalletType.monero) { + if (_wallet.type == WalletType.monero || _wallet.type == WalletType.haven) { _baseItems.add(WalletAccountListHeader()); } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index b2c4f80ae..34f4ea2b6 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -5,6 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'wallet_keys_view_model.g.dart'; @@ -29,6 +30,22 @@ abstract class WalletKeysViewModelBase with Store { ]); } + if (wallet.type == WalletType.haven) { + final keys = haven.getKeys(wallet); + + items.addAll([ + StandartListItem( + title: S.current.spend_key_public, value: keys['publicSpendKey']), + StandartListItem( + title: S.current.spend_key_private, value: keys['privateSpendKey']), + StandartListItem( + title: S.current.view_key_public, value: keys['publicViewKey']), + StandartListItem( + title: S.current.view_key_private, value: keys['privateViewKey']), + StandartListItem(title: S.current.wallet_seed, value: wallet.seed), + ]); + } + if (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) { final keys = bitcoin.getWalletKeys(wallet); diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 70bf1b30a..67a80db8f 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -16,7 +16,7 @@ class WalletListViewModel = WalletListViewModelBase with _$WalletListViewModel; abstract class WalletListViewModelBase with Store { WalletListViewModelBase(this._walletInfoSource, this._appStore, - this._keyService, this.walletNewVM) { + this._keyService) { wallets = ObservableList(); _updateList(); } @@ -27,7 +27,6 @@ abstract class WalletListViewModelBase with Store { final AppStore _appStore; final Box _walletInfoSource; final KeyService _keyService; - final WalletNewVM walletNewVM; WalletType get currentWalletType => _appStore.wallet.type; diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index b6c7540c0..b044ea369 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -10,6 +10,7 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'wallet_new_vm.g.dart'; @@ -25,7 +26,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { @observable String selectedMnemonicLanguage; - bool get hasLanguageSelector => type == WalletType.monero; + bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven; final WalletCreationService _walletCreationService; @@ -39,6 +40,9 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return bitcoin.createBitcoinNewWalletCredentials(name: name); case WalletType.litecoin: return bitcoin.createBitcoinNewWalletCredentials(name: name); + case WalletType.haven: + return haven.createHavenNewWalletCredentials( + name: name, language: options as String); default: return null; } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 818b72eb1..1943f76b9 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -12,6 +12,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/haven/haven.dart'; part 'wallet_restore_view_model.g.dart'; @@ -24,11 +25,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, this._walletCreationService, Box walletInfoSource, {@required WalletType type}) - : availableModes = type == WalletType.monero + : availableModes = (type == WalletType.monero || type == WalletType.haven) ? WalletRestoreMode.values : [WalletRestoreMode.seed], - hasSeedLanguageSelector = type == WalletType.monero, - hasBlockchainHeightLanguageSelector = type == WalletType.monero, + hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, + hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, super(appStore, walletInfoSource, type: type, isRecovery: true) { isButtonEnabled = !hasSeedLanguageSelector && !hasBlockchainHeightLanguageSelector; @@ -64,7 +65,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.monero: return monero.createMoneroRestoreWalletFromSeedCredentials( name: name, - height: height, + height: height ?? 0, mnemonic: seed, password: password); case WalletType.bitcoin: @@ -77,6 +78,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { name: name, mnemonic: seed, password: password); + case WalletType.haven: + return haven.createHavenRestoreWalletFromSeedCredentials( + name: name, + height: height ?? 0, + mnemonic: seed, + password: password); default: break; } @@ -87,14 +94,27 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { final spendKey = options['spendKey'] as String; final address = options['address'] as String; - return monero.createMoneroRestoreWalletFromKeysCredentials( - name: name, - height: height, - spendKey: spendKey, - viewKey: viewKey, - address: address, - password: password, - language: 'English'); + if (type == WalletType.monero) { + return monero.createMoneroRestoreWalletFromKeysCredentials( + name: name, + height: height, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: 'English'); + } + + if (type == WalletType.haven) { + return haven.createHavenRestoreWalletFromKeysCredentials( + name: name, + height: height, + spendKey: spendKey, + viewKey: viewKey, + address: address, + password: password, + language: 'English'); + } } return null; diff --git a/lib/wallet_type_utils.dart b/lib/wallet_type_utils.dart index 78666d67c..5ed78dc64 100644 --- a/lib/wallet_type_utils.dart +++ b/lib/wallet_type_utils.dart @@ -2,12 +2,28 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/wallet_types.g.dart'; bool get isMoneroOnly { - return availableWalletTypes.length == 1 - && availableWalletTypes.first == WalletType.monero; + return availableWalletTypes.length == 1 + && availableWalletTypes.first == WalletType.monero; +} + +bool get isHaven { + return availableWalletTypes.length == 1 + && availableWalletTypes.first == WalletType.haven; +} + + +bool get isSingleCoin { + return availableWalletTypes.length == 1; } String get approximatedAppName { - return isMoneroOnly - ? 'Monero.com' - : 'Cake Wallet'; + if (isMoneroOnly) { + return 'Monero.com'; + } + + if (isHaven) { + return 'Haven'; + } + + return 'Cake Wallet'; } \ No newline at end of file diff --git a/pubspec_base.yaml b/pubspec_base.yaml index fc7b60d11..62f129246 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -76,6 +76,7 @@ flutter: assets: - assets/images/ - assets/node_list.yml + - assets/haven_node_list.yml - assets/bitcoin_electrum_server_list.yml - assets/litecoin_electrum_server_list.yml - assets/text/ diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 7bc8ba8b4..c898d93b3 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Konten", "edit" : "Bearbeiten", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 23ff23bc7..5eccf598a 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -9,6 +9,8 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Accounts", "edit" : "Edit", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index e15234826..2b2a0263a 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -9,6 +9,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", + "accounts" : "Cuentas", "edit" : "Editar", "account" : "Cuenta", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 83386a68a..0e8542804 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "हिसाब किताब", "edit" : "संपादित करें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 4036b5733..70ae48051 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Računi", "edit" : "Uredi", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 510bd10c7..9dea0b0f5 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Accounts", "edit" : "Modifica", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 0b495e151..0a8a4696a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "アカウント", "edit" : "編集", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index ba736167e..bac95b1f7 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "계정", "edit" : "편집하다", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 5e974309d..0f554140f 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Accounts", "edit" : "Bewerk", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 7a10381e7..fa06d5642 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -8,6 +8,12 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Konta", "edit" : "Edytować", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index b830feddb..04494b2f6 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Contas", "edit" : "Editar", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 544300cdb..eeaed0998 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Аккаунты", "edit" : "Редактировать", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ab1a9b0a4..2f0968098 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "Акаунти", "edit" : "Редагувати", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index af52ca85e..745d7a7b1 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -8,6 +8,9 @@ "monero_com": "Monero.com by Cake Wallet", "monero_com_wallet_text": "Awesome wallet for Monero", + + "haven_app": "Haven by Cake Wallet", + "haven_app_wallet_text": "Awesome wallet for Haven", "accounts" : "账户", "edit" : "编辑", diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index ad2769eb4..0d0bfe0c8 100755 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -8,8 +8,9 @@ APP_ANDROID_PACKAGE="" MONERO_COM="monero.com" CAKEWALLET="cakewallet" +HAVEN="haven" -TYPES=($MONERO_COM $CAKEWALLET) +TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" @@ -19,11 +20,17 @@ MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.3.8" -CAKEWALLET_BUILD_NUMBER=89 +CAKEWALLET_VERSION="4.3.9" +CAKEWALLET_BUILD_NUMBER=94 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" +HAVEN_NAME="Haven" +HAVEN_VERSION="1.0.0" +HAVEN_BUILD_NUMBER=1 +HAVEN_BUNDLE_ID="com.cakewallet.haven" +HAVEN_PACKAGE="com.cakewallet.haven" + if ! [[ " ${TYPES[*]} " =~ " ${APP_ANDROID_TYPE} " ]]; then echo "Wrong app type." exit 1 @@ -44,6 +51,13 @@ case $APP_ANDROID_TYPE in APP_ANDROID_BUNDLE_ID=$CAKEWALLET_BUNDLE_ID APP_ANDROID_PACKAGE=$CAKEWALLET_PACKAGE ;; + $HAVEN) + APP_ANDROID_NAME=$HAVEN_NAME + APP_ANDROID_VERSION=$HAVEN_VERSION + APP_ANDROID_BUILD_NUMBER=$HAVEN_BUILD_NUMBER + APP_ANDROID_BUNDLE_ID=$HAVEN_BUNDLE_ID + APP_ANDROID_PACKAGE=$HAVEN_PACKAGE + ;; esac export APP_ANDROID_TYPE diff --git a/scripts/android/app_icon.sh b/scripts/android/app_icon.sh index 341542123..9db01f227 100755 --- a/scripts/android/app_icon.sh +++ b/scripts/android/app_icon.sh @@ -14,15 +14,20 @@ ANDROID_ICON_SET_DEST_PATH=`pwd`/../../android/app/src/main/res case $APP_ANDROID_TYPE in "monero.com") - APP_LOGO=$ASSETS_DIR/images/monero.com_logo.png - ANDROID_ICON=$MONERO_COM_PATH - ANDROID_ICON_SET=$MONEROCOM_ICON_SET_PATH + APP_LOGO=$ASSETS_DIR/images/monero.com_logo.png + ANDROID_ICON=$MONERO_COM_PATH + ANDROID_ICON_SET=$MONEROCOM_ICON_SET_PATH ;; "cakewallet") APP_LOGO=$ASSETS_DIR/images/cakewallet_logo.png ANDROID_ICON=$CAKEWALLET_PATH ANDROID_ICON_SET=$CAKEWALLET_ICON_SET_PATH ;; + "haven") + APP_LOGO=$ASSETS_DIR/images/cakewallet_logo.png + ANDROID_ICON=$CAKEWALLET_PATH + ANDROID_ICON_SET=$CAKEWALLET_ICON_SET_PATH + ;; esac rm $APP_LOGO_DEST_PATH diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index 208a4f9ef..271001dac 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -1,8 +1,14 @@ -# /bin/bash +#!/bin/sh -./build_iconv.sh -./build_boost.sh -./build_openssl.sh -./build_sodium.sh -./build_zmq.sh -./build_monero.sh +if [ -z "$APP_ANDROID_TYPE" ]; then + echo "Please set APP_ANDROID_TYPE" + exit 1 +fi + +DIR=$(dirname "$0") + +case $APP_ANDROID_TYPE in + "monero.com") $DIR/build_monero_all.sh ;; + "cakewallet") $DIR/build_monero_all.sh ;; + "haven") $DIR/build_haven_all.sh ;; +esac diff --git a/scripts/android/build_haven.sh b/scripts/android/build_haven.sh new file mode 100755 index 000000000..40dff300d --- /dev/null +++ b/scripts/android/build_haven.sh @@ -0,0 +1,70 @@ +#!/bin/sh + +. ./config.sh +HAVEN_VERSION=tags/v2.2.2 +HAVEN_SRC_DIR=${WORKDIR}/haven + +git clone https://github.com/haven-protocol-org/haven-main.git ${HAVEN_SRC_DIR} +git checkout ${HAVEN_VERSION} +cd $HAVEN_SRC_DIR +git submodule init +git submodule update + +for arch in "aarch" "aarch64" "i686" "x86_64" +do +FLAGS="" +PREFIX=${WORKDIR}/prefix_${arch} +DEST_LIB_DIR=${PREFIX}/lib/haven +DEST_INCLUDE_DIR=${PREFIX}/include +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" +ANDROID_STANDALONE_TOOLCHAIN_PATH="${TOOLCHAIN_BASE_DIR}_${arch}" +PATH="${ANDROID_STANDALONE_TOOLCHAIN_PATH}/bin:${ORIGINAL_PATH}" + +mkdir -p $DEST_LIB_DIR +mkdir -p $DEST_INCLUDE_DIR + +case $arch in + "aarch" ) + CLANG=arm-linux-androideabi-clang + CXXLANG=arm-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-armv7" + ARCH="armv7-a" + ARCH_ABI="armeabi-v7a" + FLAGS="-D CMAKE_ANDROID_ARM_MODE=ON -D NO_AES=true";; + "aarch64" ) + CLANG=aarch64-linux-androideabi-clang + CXXLANG=aarch64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-armv8" + ARCH="armv8-a" + ARCH_ABI="arm64-v8a";; + "i686" ) + CLANG=i686-linux-androideabi-clang + CXXLANG=i686-linux-androideabi-clang++ + BUILD_64=OFF + TAG="android-x86" + ARCH="i686" + ARCH_ABI="x86";; + "x86_64" ) + CLANG=x86_64-linux-androideabi-clang + CXXLANG=x86_64-linux-androideabi-clang++ + BUILD_64=ON + TAG="android-x86_64" + ARCH="x86-64" + ARCH_ABI="x86_64";; +esac + +cd $HAVEN_SRC_DIR +rm -rf ./build/release +mkdir -p ./build/release +cd ./build/release +CC=${CLANG} CXX=${CXXLANG} cmake -D USE_DEVICE_TREZOR=OFF -D BUILD_GUI_DEPS=1 -D BUILD_TESTS=OFF -D ARCH=${ARCH} -D STATIC=ON -D BUILD_64=${BUILD_64} -D CMAKE_BUILD_TYPE=release -D ANDROID=true -D INSTALL_VENDORED_LIBUNBOUND=ON -D BUILD_TAG=${TAG} -D CMAKE_SYSTEM_NAME="Android" -D CMAKE_ANDROID_STANDALONE_TOOLCHAIN="${ANDROID_STANDALONE_TOOLCHAIN_PATH}" -D CMAKE_ANDROID_ARCH_ABI=${ARCH_ABI} $FLAGS ../.. + +make wallet_api -j4 +find . -path ./lib -prune -o -name '*.a' -exec cp '{}' lib \; + +cp -r ./lib/* $DEST_LIB_DIR +cp ../../src/wallet/api/wallet2_api.h $DEST_INCLUDE_DIR +done diff --git a/scripts/android/build_haven_all.sh b/scripts/android/build_haven_all.sh new file mode 100755 index 000000000..d004c7ebf --- /dev/null +++ b/scripts/android/build_haven_all.sh @@ -0,0 +1,8 @@ +# /bin/bash + +./build_iconv.sh +./build_boost.sh +./build_openssl.sh +./build_sodium.sh +./build_zmq.sh +./build_haven.sh diff --git a/scripts/android/build_monero_all.sh b/scripts/android/build_monero_all.sh new file mode 100755 index 000000000..208a4f9ef --- /dev/null +++ b/scripts/android/build_monero_all.sh @@ -0,0 +1,8 @@ +# /bin/bash + +./build_iconv.sh +./build_boost.sh +./build_openssl.sh +./build_sodium.sh +./build_zmq.sh +./build_monero.sh diff --git a/scripts/android/copy_monero_deps.sh b/scripts/android/copy_monero_deps.sh index dc5e53fec..d518bbc88 100755 --- a/scripts/android/copy_monero_deps.sh +++ b/scripts/android/copy_monero_deps.sh @@ -2,7 +2,7 @@ WORKDIR=/opt/android CW_DIR=${WORKDIR}/cake_wallet -CW_EXRTERNAL_DIR=${CW_DIR}/cw_monero/ios/External/android +CW_EXRTERNAL_DIR=${CW_DIR}/cw_shared_external/ios/External/android for arch in "aarch" "aarch64" "i686" "x86_64" do diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 350aa664d..c69ac3906 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -2,6 +2,7 @@ MONERO_COM=monero.com CAKEWALLET=cakewallet +HAVEN=haven CONFIG_ARGS="" case $APP_ANDROID_TYPE in @@ -9,7 +10,10 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin" + CONFIG_ARGS="--monero --bitcoin --haven" + ;; + $HAVEN) + CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 4512d3028..fdc0072eb 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -2,6 +2,7 @@ MONERO_COM="monero.com" CAKEWALLET="cakewallet" +HAVEN="haven" DIR=`pwd` if [ -z "$APP_IOS_TYPE" ]; then @@ -22,7 +23,10 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin" + CONFIG_ARGS="--monero --bitcoin --haven" + ;; + $HAVEN) + CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index f5f33c5bb..ad6c9b933 100755 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -7,8 +7,9 @@ APP_IOS_BUNDLE_ID="" MONERO_COM="monero.com" CAKEWALLET="cakewallet" +HAVEN="haven" -TYPES=($MONERO_COM $CAKEWALLET) +TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" @@ -17,10 +18,15 @@ MONERO_COM_BUILD_NUMBER=14 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.3.8" -CAKEWALLET_BUILD_NUMBER=84 +CAKEWALLET_VERSION="4.3.9" +CAKEWALLET_BUILD_NUMBER=89 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" +HAVEN_NAME="Haven" +HAVEN_VERSION="1.0.0" +HAVEN_BUILD_NUMBER=3 +HAVEN_BUNDLE_ID="com.cakewallet.haven" + if ! [[ " ${TYPES[*]} " =~ " ${APP_IOS_TYPE} " ]]; then echo "Wrong app type." exit 1 @@ -39,6 +45,12 @@ case $APP_IOS_TYPE in APP_IOS_BUILD_NUMBER=$CAKEWALLET_BUILD_NUMBER APP_IOS_BUNDLE_ID=$CAKEWALLET_BUNDLE_ID ;; + $HAVEN) + APP_IOS_NAME=$HAVEN_NAME + APP_IOS_VERSION=$HAVEN_VERSION + APP_IOS_BUILD_NUMBER=$HAVEN_BUILD_NUMBER + APP_IOS_BUNDLE_ID=$HAVEN_BUNDLE_ID + ;; esac export APP_IOS_TYPE diff --git a/scripts/ios/app_icon.sh b/scripts/ios/app_icon.sh index e3eb36b3c..3914c3756 100755 --- a/scripts/ios/app_icon.sh +++ b/scripts/ios/app_icon.sh @@ -14,6 +14,10 @@ case $APP_IOS_TYPE in ICON_120_PATH=`pwd`/../../assets/images/cakewallet_icon_120.png ICON_180_PATH=`pwd`/../../assets/images/cakewallet_icon_180.png ICON_1024_PATH=`pwd`/../../assets/images/cakewallet_icon_1024.png;; + "haven") + ICON_120_PATH=`pwd`/../../assets/images/cakewallet_icon_120.png + ICON_180_PATH=`pwd`/../../assets/images/cakewallet_icon_180.png + ICON_1024_PATH=`pwd`/../../assets/images/cakewallet_icon_1024.png;; esac rm $DEST_DIR_PATH/app_icon_120.png diff --git a/scripts/ios/build_deps.sh b/scripts/ios/build_all.sh similarity index 84% rename from scripts/ios/build_deps.sh rename to scripts/ios/build_all.sh index 141a7d00f..85c1f63b9 100755 --- a/scripts/ios/build_deps.sh +++ b/scripts/ios/build_all.sh @@ -10,4 +10,5 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh ;; + "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_haven.sh b/scripts/ios/build_haven.sh new file mode 100755 index 000000000..5f12f39f4 --- /dev/null +++ b/scripts/ios/build_haven.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +. ./config.sh + +HAVEN_URL="https://github.com/haven-protocol-org/haven-main.git" +HAVEN_DIR_PATH="${EXTERNAL_IOS_SOURCE_DIR}/haven" +HAVEN_VERSION=tags/v2.2.2 +BUILD_TYPE=release +PREFIX=${EXTERNAL_IOS_DIR} + +echo "Cloning haven from - $HAVEN_URL to - $HAVEN_DIR_PATH" +git clone $HAVEN_URL $HAVEN_DIR_PATH +cd $HAVEN_DIR_PATH +git checkout $HAVEN_VERSION +git submodule update --init --force +mkdir -p build +cd .. + +ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +if [ -z $INSTALL_PREFIX ]; then + INSTALL_PREFIX=${ROOT_DIR}/haven +fi + +for arch in "arm64" #"armv7" "arm64" +do + +echo "Building IOS ${arch}" +export CMAKE_INCLUDE_PATH="${PREFIX}/include" +export CMAKE_LIBRARY_PATH="${PREFIX}/lib" + +case $arch in + "armv7" ) + DEST_LIB=../../lib-armv7;; + "arm64" ) + DEST_LIB=../../lib-armv8-a;; +esac + +rm -rf haven/build > /dev/null + +mkdir -p haven/build/${BUILD_TYPE} +pushd haven/build/${BUILD_TYPE} +cmake -D IOS=ON \ + -DARCH=${arch} \ + -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \ + -DSTATIC=ON \ + -DBUILD_GUI_DEPS=ON \ + -DINSTALL_VENDORED_LIBUNBOUND=ON \ + -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -DUSE_DEVICE_TREZOR=OFF \ + ../.. +make -j4 && make install +cp src/cryptonote_basic/libcryptonote_basic.a ${DEST_LIB} +cp src/offshore/liboffshore.a ${DEST_LIB} +popd + +done + +mkdir -p $EXTERNAL_IOS_LIB_DIR/haven +mkdir -p $EXTERNAL_IOS_INCLUDE_DIR/haven +#only for arm64 +cp ${HAVEN_DIR_PATH}/lib-armv8-a/* $EXTERNAL_IOS_LIB_DIR/haven +cp ${HAVEN_DIR_PATH}/include/wallet/api/* $EXTERNAL_IOS_INCLUDE_DIR/haven \ No newline at end of file diff --git a/scripts/ios/build_haven_all.sh b/scripts/ios/build_haven_all.sh new file mode 100755 index 000000000..116a30d25 --- /dev/null +++ b/scripts/ios/build_haven_all.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +. ./config.sh +./install_missing_headers.sh +./build_openssl.sh +./build_boost.sh +./build_sodium.sh +./build_zmq.sh +./build_haven.sh \ No newline at end of file diff --git a/scripts/ios/config.sh b/scripts/ios/config.sh index 128701469..22ce9d3e3 100755 --- a/scripts/ios/config.sh +++ b/scripts/ios/config.sh @@ -2,7 +2,7 @@ export IOS_SCRIPTS_DIR=`pwd` export CW_ROOT=${IOS_SCRIPTS_DIR}/../.. -export EXTERNAL_DIR=${CW_ROOT}/cw_monero/ios/External +export EXTERNAL_DIR=${CW_ROOT}/cw_shared_external/ios/External export EXTERNAL_IOS_DIR=${EXTERNAL_DIR}/ios export EXTERNAL_IOS_SOURCE_DIR=${EXTERNAL_IOS_DIR}/sources export EXTERNAL_IOS_LIB_DIR=${EXTERNAL_IOS_DIR}/lib diff --git a/scripts/ios/setup.sh b/scripts/ios/setup.sh new file mode 100755 index 000000000..53c8f9864 --- /dev/null +++ b/scripts/ios/setup.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +. ./config.sh + +cd $EXTERNAL_IOS_LIB_DIR +libtool -static -o libboost.a ./boost/*.a +libtool -static -o libhaven.a ./haven/*.a +libtool -static -o libmonero.a ./monero/*.a + +CW_HAVEN_EXTERNAL_LIB=../../../cw_haven/ios/External/ios/lib +CW_HAVEN_EXTERNAL_INCLUDE=../../../cw_haven/ios/External/ios/include +CW_MONERO_EXTERNAL=../../../cw_haven/ios/External/ios/lib + +mkdir -p $CW_HAVEN_EXTERNAL_INCLUDE +mkdir -p $CW_HAVEN_EXTERNAL_LIB +mkdir -p $CW_MONERO_EXTERNAL + +ln -s ./libboost.a $CW_HAVEN_EXTERNAL_LIB +ln -s ./libcrypto.a $CW_HAVEN_EXTERNAL_LIB +ln -s ./libssl.a $CW_HAVEN_EXTERNAL_LIB +ln -s ./libsodium.a $CW_HAVEN_EXTERNAL_LIB +cp ./libhaven.a $CW_HAVEN_EXTERNAL_LIB +cp ../include/haven/* $CW_HAVEN_EXTERNAL_INCLUDE + +#ln -s ./libboost.a $CW_HAVEN_EXTERNAL_LIB +#ln -s ./libcrypto.a $CW_HAVEN_EXTERNAL_LIB +#ln -s ./libssl.a $CW_HAVEN_EXTERNAL_LIB +#ln -s ./libsodium.a $CW_HAVEN_EXTERNAL_LIB +#cp ./libhaven.a $CW_HAVEN_EXTERNAL_LIB \ No newline at end of file diff --git a/tool/configure.dart b/tool/configure.dart index 0987fa7a4..843d665b3 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -3,6 +3,7 @@ import 'dart:io'; const bitcoinOutputPath = 'lib/bitcoin/bitcoin.dart'; const moneroOutputPath = 'lib/monero/monero.dart'; +const havenOutputPath = 'lib/haven/haven.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -11,10 +12,11 @@ Future main(List args) async { const prefix = '--'; final hasBitcoin = args.contains('${prefix}bitcoin'); final hasMonero = args.contains('${prefix}monero'); + final hasHaven = args.contains('${prefix}haven'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); - await generatePubspec(hasMonero: hasMonero, hasBitcoin: hasBitcoin); - await generateWalletTypes(hasMonero: hasMonero, hasBitcoin: hasBitcoin); + await generatePubspec(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven); + await generateWalletTypes(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven); } Future generateBitcoin(bool hasImplementation) async { @@ -123,15 +125,15 @@ import 'package:cake_wallet/view_model/send/output.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:hive/hive.dart';"""; const moneroCWHeaders = """ -import 'package:cw_monero/get_height_by_date.dart'; -import 'package:cw_monero/monero_amount_format.dart'; -import 'package:cw_monero/monero_transaction_priority.dart'; +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_monero/monero_wallet_service.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:cw_monero/monero_transaction_info.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_monero/account.dart' as monero_account; +import 'package:cw_core/account.dart' as monero_account; import 'package:cw_monero/api/wallet.dart' as monero_wallet_api; import 'package:cw_monero/mnemonics/english.dart'; import 'package:cw_monero/mnemonics/chinese_simplified.dart'; @@ -228,7 +230,7 @@ abstract class Monero { double formatterMoneroAmountToDouble({int amount}); int formatterMoneroParseAmount({String amount}); Account getCurrentAccount(Object wallet); - void setCurrentAccount(Object wallet, Account account); + void setCurrentAccount(Object wallet, int id, String label); void onStartup(); int getTransactionInfoAccountId(TransactionInfo tx); WalletService createMoneroWalletService(Box walletInfoSource); @@ -271,7 +273,171 @@ abstract class MoneroAccountList { await outputFile.writeAsString(output); } -Future generatePubspec({bool hasMonero, bool hasBitcoin}) async { +Future generateHaven(bool hasImplementation) async { + final outputFile = File(moneroOutputPath); + const havenCommonHeaders = """ +import 'package:mobx/mobx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart';"""; + const havenCWHeaders = """ +import 'package:cw_core/get_height_by_date.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_haven/haven_wallet_service.dart'; +import 'package:cw_haven/haven_wallet.dart'; +import 'package:cw_haven/haven_transaction_info.dart'; +import 'package:cw_haven/haven_transaction_history.dart'; +import 'package:cw_core/account.dart' as monero_account; +import 'package:cw_haven/api/wallet.dart' as monero_wallet_api; +import 'package:cw_haven/mnemonics/english.dart'; +import 'package:cw_haven/mnemonics/chinese_simplified.dart'; +import 'package:cw_haven/mnemonics/dutch.dart'; +import 'package:cw_haven/mnemonics/german.dart'; +import 'package:cw_haven/mnemonics/japanese.dart'; +import 'package:cw_haven/mnemonics/russian.dart'; +import 'package:cw_haven/mnemonics/spanish.dart'; +import 'package:cw_haven/mnemonics/portuguese.dart'; +import 'package:cw_haven/mnemonics/french.dart'; +import 'package:cw_haven/mnemonics/italian.dart'; +import 'package:cw_haven/haven_transaction_creation_credentials.dart'; +"""; + const havenCwPart = "part 'cw_haven.dart';"; + const havenContent = """ +class Account { + Account({this.id, this.label}); + final int id; + final String label; +} + +class Subaddress { + Subaddress({this.id, this.accountId, this.label, this.address}); + final int id; + final int accountId; + final String label; + final String address; +} + +class HavenBalance extends Balance { + HavenBalance({@required this.fullBalance, @required this.unlockedBalance}) + : formattedFullBalance = haven.formatterMoneroAmountToString(amount: fullBalance), + formattedUnlockedBalance = + haven.formatterMoneroAmountToString(amount: unlockedBalance), + super(unlockedBalance, fullBalance); + + HavenBalance.fromString( + {@required this.formattedFullBalance, + @required this.formattedUnlockedBalance}) + : fullBalance = haven.formatterMoneroParseAmount(amount: formattedFullBalance), + unlockedBalance = haven.formatterMoneroParseAmount(amount: formattedUnlockedBalance), + super(haven.formatterMoneroParseAmount(amount: formattedUnlockedBalance), + haven.formatterMoneroParseAmount(amount: formattedFullBalance)); + + final int fullBalance; + final int unlockedBalance; + final String formattedFullBalance; + final String formattedUnlockedBalance; + + @override + String get formattedAvailableBalance => formattedUnlockedBalance; + + @override + String get formattedAdditionalBalance => formattedFullBalance; +} + +abstract class HavenWalletDetails { + @observable + Account account; + + @observable + HavenBalance balance; +} + +abstract class Haven { + HavenAccountList getAccountList(Object wallet); + + MoneroSubaddressList getSubaddressList(Object wallet); + + TransactionHistoryBase getTransactionHistory(Object wallet); + + HavenWalletDetails getMoneroWalletDetails(Object wallet); + + String getTransactionAddress(Object wallet, int accountIndex, int addressIndex); + + int getHeigthByDate({DateTime date}); + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority deserializeMoneroTransactionPriority({int raw}); + List getTransactionPriorities(); + List getMoneroWordList(String language); + + WalletCredentials createHavenRestoreWalletFromKeysCredentials({ + String name, + String spendKey, + String viewKey, + String address, + String password, + String language, + int height}); + WalletCredentials createHavenRestoreWalletFromSeedCredentials({String name, String password, int height, String mnemonic}); + WalletCredentials createHavenNewWalletCredentials({String name, String password, String language}); + Map getKeys(Object wallet); + Object createHavenTransactionCreationCredentials({List outputs, TransactionPriority priority, String assetType}); + String formatterMoneroAmountToString({int amount}); + double formatterMoneroAmountToDouble({int amount}); + int formatterMoneroParseAmount({String amount}); + Account getCurrentAccount(Object wallet); + void setCurrentAccount(Object wallet, int id, String label); + void onStartup(); + int getTransactionInfoAccountId(TransactionInfo tx); + WalletService createHavenWalletService(Box walletInfoSource); +} + +abstract class MoneroSubaddressList { + ObservableList get subaddresses; + void update(Object wallet, {int accountIndex}); + void refresh(Object wallet, {int accountIndex}); + List getAll(Object wallet); + Future addSubaddress(Object wallet, {int accountIndex, String label}); + Future setLabelSubaddress(Object wallet, + {int accountIndex, int addressIndex, String label}); +} + +abstract class HavenAccountList { + ObservableList get accounts; + void update(Object wallet); + void refresh(Object wallet); + List getAll(Object wallet); + Future addAccount(Object wallet, {String label}); + Future setLabelAccount(Object wallet, {int accountIndex, String label}); +} + """; + + const havenEmptyDefinition = 'Monero monero;\n'; + const havenCWDefinition = 'Monero monero = CWMonero();\n'; + + final output = '$havenCommonHeaders\n' + + (hasImplementation ? '$havenCWHeaders\n' : '\n') + + (hasImplementation ? '$havenCwPart\n\n' : '\n') + + (hasImplementation ? havenCWDefinition : havenEmptyDefinition) + + '\n' + + havenContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + +Future generatePubspec({bool hasMonero, bool hasBitcoin, bool hasHaven}) async { const cwCore = """ cw_core: path: ./cw_core @@ -284,6 +450,14 @@ Future generatePubspec({bool hasMonero, bool hasBitcoin}) async { cw_bitcoin: path: ./cw_bitcoin """; + const cwHaven = """ + cw_haven: + path: ./cw_haven + """; + const cwSharedExternal = """ + cw_shared_external: + path: ./cw_shared_external + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -291,13 +465,19 @@ Future generatePubspec({bool hasMonero, bool hasBitcoin}) async { var output = cwCore; if (hasMonero) { - output += '\n$cwMonero'; + output += '\n$cwMonero\n$cwSharedExternal'; } if (hasBitcoin) { output += '\n$cwBitcoin'; } + if (hasHaven && !hasMonero) { + output += '\n$cwSharedExternal\n$cwHaven'; + } else if (hasHaven) { + output += '\n$cwHaven'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -310,7 +490,7 @@ Future generatePubspec({bool hasMonero, bool hasBitcoin}) async { await outputFile.writeAsString(outputContent); } -Future generateWalletTypes({bool hasMonero, bool hasBitcoin}) async { +Future generateWalletTypes({bool hasMonero, bool hasBitcoin, bool hasHaven}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -329,6 +509,10 @@ Future generateWalletTypes({bool hasMonero, bool hasBitcoin}) async { outputContent += '\tWalletType.bitcoin,\n\tWalletType.litecoin,\n'; } + if (hasHaven) { + outputContent += '\tWalletType.haven,\n'; + } + outputContent += '];\n'; await walletTypesFile.writeAsString(outputContent); } \ No newline at end of file