From 3ce4000dcf47fa99053cf7beb52d456cb4feae9e Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 4 Aug 2023 20:01:49 +0300 Subject: [PATCH] Cw 78 ethereum (#862) * Add initial flow for ethereum * Add initial create Eth wallet flow * Complete Ethereum wallet creation flow * Fix web3dart versioning issue * Add primary receive address extracted from private key * Implement open wallet functionality * Implement restore wallet from seed functionality * Fixate web3dart version as higher versions cause some issues * Add Initial Transaction priorities for eth Add estimated gas price * Rename priority value to tip * Re-order wallet types * Change ethereum node Fix connection issues * Fix estimating gas for priority * Add case for ethereum to fetch it's seeds * Add case for ethereum to request node * Fix Exchange screen initial pairs * Add initial send transaction flow * Add missing configure for ethereum class * Add Eth address initial setup * Fix Private key for Ethereum wallets * Change sign/send transaction flow * - Fix Conflicts with main - Remove unused function from Haven configure.dart * Add build command for ethereum package * Add missing Node list file to pubspec * - Fix balance display - Fix parsing of Ethereum amount - Add more Ethereum Nodes * - Fix extracting Ethereum Private key from seeds - Integrate signing/sending transaction with the send view model * - Update and Fix Conflicts with main * Add Balances for ERC20 tokens * Fix conflicts with main * Add erc20 abi json * Add send erc20 tokens initial function * add missing getHeightByDate in Haven * Allow contacts and wallets from the same tag * Add Shiba Inu icon * Add send ERC-20 tokens initial flow * Add missing import in generated file * Add initial approach for transaction sending for ERC-20 tokens * Refactor signing/sending transactions * Add initial flow for transactions subscription * Refactor signing/sending transactions * Add home settings icon * Fix conflicts with main * Initial flow for home settings * Add logic flow for adding erc20 tokens * Fix initial UI * Finalize UI for Tokens * Integrate UI with Ethereum flow * Add "Enable/Disable" feature for ERC20 tokens * Add initial Erc20 tokens * Add Sorting and Pin Native Token features * Fix price sorting * Sort tokens list as well when Sort criteria changes * - Improve sorting balances flow - Add initial add token from search bar flow * Fix Accounts Popup UI * Fix Pin native token * Fix Enabling/Disabling tokens Fix sorting by fiat once app is opened Improve token availability mechanism * Fix deleting token Fix renaming tokens * Fix issue with search * Add more tokens * - Fix scroll issue - Add ERC20 tokens placeholder image in picker * - Separate and organize default erc20 tokens - Fix scrolling - Add token placeholder images in picker - Sort disabled tokens alphabetically * Change BNB token initial availability * Fix Conflicts with main * Fix Conflicts with main * Add Verse ERC20 token to the initial tokens list * Add rename wallet to Ethereum * Integrate EtherScan API for fetching address transactions Generate Ethereum specific secrets in Ethereum package * Adjust transactions fiat price for ERC20 tokens * Free Up GitHub Actions Ubuntu Runner Disk Space * Free Up GitHub Actions Ubuntu Runner Disk space (trial 2) * Fix Transaction Fee display * Save transaction history * Enhance loading time for erc20 tokens transactions * Minor Fixes and Enhancements * Fix sending erc20 fix block explorer issue * Fix int overflow * Fix transaction amount conversions * Minor: `slow` -> `Slow` * Update build guide * Fix fetching fiat rate taking a lot of time by only fetching enabled tokens only and making the API calls in parallel not sequential * Update transactions on a periodic basis * For fee, use ETH spot price, not ERC-20 spot price * Add Etherscan History privacy option to enable/disable Etherscan API * Show estimated fee amounts in the send screen * fix send fiat fields parsing issue * Fix transactions estimated fee less than actual fee * handle balance sorting when balance is disabled Handle empty transactions list * Fix Delete Ethereum wallet Fix balance < 0.01 * Fix Decimal place for Ethereum amount Fix sending amount issue * Change words count * Remove balance hint and Full balance row from Ethereum wallets * support changing the asset type in send templates * Fix Templates for ERC tokens issues * Fix conflicts in send templates * Disable batch sending in Ethereum * Fix Fee calculation with different priorities * Fix Conflicts with main * Add offline error to ignored exceptions --------- Co-authored-by: Justin Ehrenhofer --- .github/workflows/pr_test_build.yml | 2 + .gitignore | 3 + assets/ethereum_server_list.yml | 10 + assets/images/home_screen_settings_icon.png | Bin 0 -> 394 bytes configure_cake_wallet_android.sh | 10 + .../lib/electrum_transaction_history.dart | 3 +- cw_bitcoin/lib/electrum_transaction_info.dart | 7 +- cw_bitcoin/lib/electrum_wallet.dart | 1 + cw_bitcoin/pubspec.yaml | 2 +- cw_core/lib/currency_for_wallet_type.dart | 2 + cw_core/lib/erc20_token.dart | 64 + cw_core/lib/node.dart | 19 +- cw_core/lib/wallet_base.dart | 2 + cw_core/lib/wallet_service.dart | 2 +- cw_core/lib/wallet_type.dart | 16 +- cw_ethereum/.gitignore | 30 + cw_ethereum/.metadata | 10 + cw_ethereum/CHANGELOG.md | 3 + cw_ethereum/LICENSE | 1 + cw_ethereum/README.md | 39 + cw_ethereum/analysis_options.yaml | 4 + cw_ethereum/lib/cw_ethereum.dart | 7 + cw_ethereum/lib/default_erc20_tokens.dart | 302 +++ cw_ethereum/lib/erc20_balance.dart | 47 + cw_ethereum/lib/ethereum_client.dart | 230 ++ cw_ethereum/lib/ethereum_exceptions.dart | 11 + cw_ethereum/lib/ethereum_formatter.dart | 25 + cw_ethereum/lib/ethereum_mnemonics.dart | 2058 +++++++++++++++++ .../lib/ethereum_transaction_credentials.dart | 17 + .../lib/ethereum_transaction_history.dart | 77 + .../lib/ethereum_transaction_info.dart | 74 + .../lib/ethereum_transaction_model.dart | 47 + .../lib/ethereum_transaction_priority.dart | 52 + cw_ethereum/lib/ethereum_wallet.dart | 473 ++++ .../lib/ethereum_wallet_addresses.dart | 33 + .../ethereum_wallet_creation_credentials.dart | 23 + cw_ethereum/lib/ethereum_wallet_service.dart | 108 + cw_ethereum/lib/file.dart | 39 + .../lib/pending_ethereum_transaction.dart | 36 + cw_ethereum/pubspec.yaml | 68 + cw_ethereum/test/cw_ethereum_test.dart | 12 + cw_haven/lib/haven_wallet.dart | 1 + cw_monero/lib/monero_transaction_info.dart | 13 +- cw_monero/lib/monero_wallet.dart | 1 + howto-build-android.md | 12 +- lib/bitcoin/cw_bitcoin.dart | 2 +- lib/core/address_validator.dart | 23 +- lib/core/backup_service.dart | 18 + lib/core/fiat_conversion_service.dart | 17 +- lib/core/seed_validator.dart | 3 + lib/di.dart | 33 +- lib/entities/default_settings_migration.dart | 46 +- lib/entities/main_actions.dart | 2 + lib/entities/node_list.dart | 16 + lib/entities/preferences_key.dart | 5 + lib/entities/priority_for_wallet_type.dart | 3 + lib/entities/sort_balance_types.dart | 19 + lib/entities/template.dart | 2 +- lib/ethereum/cw_ethereum.dart | 126 + lib/main.dart | 2 +- lib/reactions/fiat_rate_update.dart | 15 + lib/reactions/on_current_wallet_change.dart | 16 +- lib/router.dart | 37 +- lib/routes.dart | 2 + .../desktop_wallet_selection_dropdown.dart | 3 + .../screens/dashboard/edit_token_page.dart | 309 +++ .../screens/dashboard/home_settings_page.dart | 164 ++ .../dashboard/widgets/address_page.dart | 152 +- .../dashboard/widgets/balance_page.dart | 376 +-- .../dashboard/widgets/menu_widget.dart | 34 +- .../exchange/widgets/exchange_card.dart | 21 +- .../new_wallet/new_wallet_type_page.dart | 7 +- .../restore/restore_wallet_options_page.dart | 85 - lib/src/screens/seed/pre_seed_page.dart | 15 +- lib/src/screens/send/send_page.dart | 202 +- lib/src/screens/send/send_template_page.dart | 71 +- .../widgets/prefix_currency_icon_widget.dart | 54 +- lib/src/screens/send/widgets/send_card.dart | 749 +++--- .../send/widgets/send_template_card.dart | 277 ++- lib/src/screens/settings/privacy_page.dart | 10 +- .../widgets/settings_switcher_cell.dart | 19 +- .../screens/wallet_list/wallet_list_page.dart | 3 + lib/src/widgets/checkbox_widget.dart | 67 +- lib/src/widgets/picker.dart | 93 +- lib/src/widgets/standard_list.dart | 11 +- lib/store/settings_store.dart | 81 +- lib/utils/exception_handler.dart | 1 + .../contact_list/contact_list_view_model.dart | 9 +- .../dashboard/balance_view_model.dart | 68 +- .../dashboard/home_settings_view_model.dart | 121 + .../dashboard/transaction_list_item.dart | 8 + .../exchange/exchange_view_model.dart | 4 + .../node_list/node_list_view_model.dart | 3 + lib/view_model/send/output.dart | 14 +- .../send/send_template_view_model.dart | 20 +- lib/view_model/send/send_view_model.dart | 49 +- lib/view_model/send/template_view_model.dart | 34 +- .../settings/privacy_settings_view_model.dart | 19 +- .../transaction_details_view_model.dart | 206 +- .../wallet_address_list_view_model.dart | 31 + lib/view_model/wallet_keys_view_model.dart | 8 +- lib/view_model/wallet_new_vm.dart | 3 + lib/view_model/wallet_restore_view_model.dart | 6 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 - model_generator.sh | 1 + pubspec_base.yaml | 1 + res/values/strings_ar.arb | 28 +- res/values/strings_bg.arb | 28 +- res/values/strings_cs.arb | 28 +- res/values/strings_de.arb | 28 +- res/values/strings_en.arb | 28 +- res/values/strings_es.arb | 28 +- res/values/strings_fr.arb | 28 +- res/values/strings_ha.arb | 28 +- res/values/strings_hi.arb | 28 +- res/values/strings_hr.arb | 28 +- res/values/strings_id.arb | 28 +- res/values/strings_it.arb | 28 +- res/values/strings_ja.arb | 28 +- res/values/strings_ko.arb | 28 +- res/values/strings_my.arb | 28 +- res/values/strings_nl.arb | 28 +- res/values/strings_pl.arb | 28 +- res/values/strings_pt.arb | 28 +- res/values/strings_ru.arb | 28 +- res/values/strings_th.arb | 28 +- res/values/strings_tr.arb | 28 +- res/values/strings_uk.arb | 28 +- res/values/strings_ur.arb | 28 +- res/values/strings_yo.arb | 28 +- res/values/strings_zh.arb | 28 +- scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- tool/configure.dart | 112 +- tool/generate_secrets_config.dart | 20 +- tool/import_secrets_config.dart | 22 +- tool/utils/secret_key.dart | 4 + 137 files changed, 7164 insertions(+), 1492 deletions(-) create mode 100644 assets/ethereum_server_list.yml create mode 100644 assets/images/home_screen_settings_icon.png create mode 100644 configure_cake_wallet_android.sh create mode 100644 cw_core/lib/erc20_token.dart create mode 100644 cw_ethereum/.gitignore create mode 100644 cw_ethereum/.metadata create mode 100644 cw_ethereum/CHANGELOG.md create mode 100644 cw_ethereum/LICENSE create mode 100644 cw_ethereum/README.md create mode 100644 cw_ethereum/analysis_options.yaml create mode 100644 cw_ethereum/lib/cw_ethereum.dart create mode 100644 cw_ethereum/lib/default_erc20_tokens.dart create mode 100644 cw_ethereum/lib/erc20_balance.dart create mode 100644 cw_ethereum/lib/ethereum_client.dart create mode 100644 cw_ethereum/lib/ethereum_exceptions.dart create mode 100644 cw_ethereum/lib/ethereum_formatter.dart create mode 100644 cw_ethereum/lib/ethereum_mnemonics.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_history.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_info.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_model.dart create mode 100644 cw_ethereum/lib/ethereum_transaction_priority.dart create mode 100644 cw_ethereum/lib/ethereum_wallet.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_addresses.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_creation_credentials.dart create mode 100644 cw_ethereum/lib/ethereum_wallet_service.dart create mode 100644 cw_ethereum/lib/file.dart create mode 100644 cw_ethereum/lib/pending_ethereum_transaction.dart create mode 100644 cw_ethereum/pubspec.yaml create mode 100644 cw_ethereum/test/cw_ethereum_test.dart create mode 100644 lib/entities/sort_balance_types.dart create mode 100644 lib/ethereum/cw_ethereum.dart create mode 100644 lib/src/screens/dashboard/edit_token_page.dart create mode 100644 lib/src/screens/dashboard/home_settings_page.dart delete mode 100644 lib/src/screens/restore/restore_wallet_options_page.dart create mode 100644 lib/view_model/dashboard/home_settings_view_model.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 16b036344..de8396d59 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -92,6 +92,7 @@ jobs: cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs - name: Add secrets @@ -124,6 +125,7 @@ jobs: echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties diff --git a/.gitignore b/.gitignore index 70d99f753..6fd8f33d6 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,9 @@ android/key.properties **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.ethereum-secrets-config.json **/lib/.secrets.g.dart +**/cw_ethereum/lib/.secrets.g.dart vendor/ @@ -121,6 +123,7 @@ cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart +lib/ethereum/ethereum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml new file mode 100644 index 000000000..125085d88 --- /dev/null +++ b/assets/ethereum_server_list.yml @@ -0,0 +1,10 @@ +- + uri: ethereum.publicnode.com +- + uri: eth.llamarpc.com +- + uri: rpc.flashbots.net +- + uri: eth-mainnet.public.blastapi.io +- + uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/images/home_screen_settings_icon.png b/assets/images/home_screen_settings_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6c750f5f678d5e34224c36c6e342b5bd1fc7096c GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O(A@}4e^Ar*{ILvjTV8SwbN7iegC#K5_=fysf< zfNA{}w<+Vy~nwu6} zurhn~!14%>Ou&jQx86?;dw4W#JNvHwGq#_StKF~p9#l0|UOpk9joCPLyHY8O<>Qc?Q|0-bP0l+XkK=AfE8 literal 0 HcmV?d00001 diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh new file mode 100644 index 000000000..b80ebc46e --- /dev/null +++ b/configure_cake_wallet_android.sh @@ -0,0 +1,10 @@ +cd scripts/android +source ./app_env.sh cakewallet +./app_config.sh +cd ../.. && flutter pub get +cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 553795470..be039fa36 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_bitcoin/file.dart'; @@ -67,7 +66,7 @@ abstract class ElectrumTransactionHistoryBase Future _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map ?? {}; + final txs = content['transactions'] as Map? ?? {}; txs.entries.forEach((entry) { final val = entry.value; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b034c06b1..bf5ec2c4f 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; @@ -217,9 +216,9 @@ class ElectrumTransactionInfo extends TransactionInfo { height: info.height, amount: info.amount, fee: info.fee, - direction: direction ?? info.direction, - date: date ?? info.date, - isPending: isPending ?? info.isPending, + direction: direction, + date: date, + isPending: isPending, confirmations: info.confirmations); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index aadf87572..f9437e668 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -431,6 +431,7 @@ abstract class ElectrumWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletFile = File(currentWalletPath); diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 455ceb4a7..481a41ac5 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: unorm_dart: ^0.2.0 cryptography: ^2.0.5 encrypt: ^5.0.1 - + dev_dependencies: flutter_test: sdk: flutter diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 3904fc049..8ac8c1fc6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart new file mode 100644 index 000000000..2e205e484 --- /dev/null +++ b/cw_core/lib/erc20_token.dart @@ -0,0 +1,64 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:hive/hive.dart'; + +part 'erc20_token.g.dart'; + +@HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + @HiveField(1) + final String symbol; + @HiveField(2) + final String contractAddress; + @HiveField(3) + final int decimal; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: "ETH", + iconPath: iconPath, + ); + + Erc20Token.copyWith(Erc20Token other, String? icon) + : this.name = other.name, + this.symbol = other.symbol, + this.contractAddress = other.contractAddress, + this.decimal = other.decimal, + this._enabled = other.enabled, + this.iconPath = icon, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: "ETH", + iconPath: icon, + ); + + static const typeId = 12; + static const boxName = 'Erc20Tokens'; + + @override + bool operator ==(other) => other is Erc20Token && other.contractAddress == contractAddress; + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 0848e8d94..3fa45b44c 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -75,6 +75,8 @@ class Node extends HiveObject with Keyable { return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); + case WalletType.ethereum: + return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -124,6 +126,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); + case WalletType.ethereum: + return requestElectrumServer(); default: return false; } @@ -166,7 +170,7 @@ class Node extends HiveObject with Keyable { } catch (_) { return false; } -} + } Future requestNodeWithProxy(String proxy) async { @@ -193,4 +197,17 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } } diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index e5f84f467..019f87631 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -75,4 +75,6 @@ abstract class WalletBase< Future? updateBalance(); void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; + + Future renameWalletFiles(String newWalletName); } diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 5e216e225..f95bc1a44 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -18,5 +18,5 @@ abstract class WalletService remove(String wallet); - Future rename(String name, String password, String newName); + Future rename(String currentName, String password, String newName); } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 61a571fcf..a65839041 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -7,7 +7,8 @@ const walletTypes = [ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, - WalletType.haven + WalletType.haven, + WalletType.ethereum, ]; const walletTypeTypeId = 5; @@ -27,6 +28,9 @@ enum WalletType { @HiveField(4) haven, + + @HiveField(5) + ethereum, } int serializeToInt(WalletType type) { @@ -39,6 +43,8 @@ int serializeToInt(WalletType type) { return 2; case WalletType.haven: return 3; + case WalletType.ethereum: + return 4; default: return -1; } @@ -54,6 +60,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.litecoin; case 3: return WalletType.haven; + case 4: + return WalletType.ethereum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -69,6 +77,8 @@ String walletTypeToString(WalletType type) { return 'Litecoin'; case WalletType.haven: return 'Haven'; + case WalletType.ethereum: + return 'Ethereum'; default: return ''; } @@ -84,6 +94,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Litecoin (LTC)'; case WalletType.haven: return 'Haven (XHV)'; + case WalletType.ethereum: + return 'Ethereum (ETH)'; default: return ''; } @@ -99,6 +111,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_ethereum/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_ethereum/.metadata b/cw_ethereum/.metadata new file mode 100644 index 000000000..1e05dac7f --- /dev/null +++ b/cw_ethereum/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: eb6d86ee27deecba4a83536aa20f366a6044895c + channel: stable + +project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_ethereum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_ethereum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_ethereum/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_ethereum/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart new file mode 100644 index 000000000..af9ea7ee0 --- /dev/null +++ b/cw_ethereum/lib/cw_ethereum.dart @@ -0,0 +1,7 @@ +library cw_ethereum; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_ethereum/lib/default_erc20_tokens.dart b/cw_ethereum/lib/default_erc20_tokens.dart new file mode 100644 index 000000000..241e301ce --- /dev/null +++ b/cw_ethereum/lib/default_erc20_tokens.dart @@ -0,0 +1,302 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Gitcoin", + symbol: "GTC", + contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Compound", + symbol: "COMP", + contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Aave Token", + symbol: "AAVE", + contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap", + symbol: "UNI", + contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentraland", + symbol: "MANA", + contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Storj", + symbol: "STORJ", + contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Maker", + symbol: "MKR", + contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Orchid", + symbol: "OXT", + contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Paxos Gold", + symbol: "PAXG", + contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Binance Coin", + symbol: "BNB", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "stETH", + symbol: "stETH", + contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Lido DAO", + symbol: "LDO", + contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Graph Token", + symbol: "GRT", + contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Frax", + symbol: "FRAX", + contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Gemini dollar", + symbol: "GUSD", + contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + decimal: 2, + enabled: false, + ), + Erc20Token( + name: "Compound Ether", + symbol: "cETH", + contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Binance USD", + symbol: "BUSD", + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "TrueUSD", + symbol: "TUSD", + contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Cronos Coin", + symbol: "CRO", + contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Pax Dollar", + symbol: "USDP", + contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Fantom Token", + symbol: "FTM", + contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BitTorrent", + symbol: "BTT", + contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Nexo", + symbol: "NEXO", + contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "dYdX", + symbol: "DYDX", + contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PancakeSwap Token", + symbol: "Cake", + contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BAT", + symbol: "BAT", + contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "1INCH Token", + symbol: "1INCH", + contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Ethereum Name Service", + symbol: "ENS", + contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ZRX", + symbol: "ZRX", + contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Verse", + symbol: "VERSE", + contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + decimal: 18, + enabled: false, + ), + ]; + + List get initialErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + if (iconPath != null) { + return Erc20Token.copyWith(token, iconPath); + } + + return token; + }).toList(); +} diff --git a/cw_ethereum/lib/erc20_balance.dart b/cw_ethereum/lib/erc20_balance.dart new file mode 100644 index 000000000..7d11f8e45 --- /dev/null +++ b/cw_ethereum/lib/erc20_balance.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/balance.dart'; + +class ERC20Balance extends Balance { + ERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), + balance.toInt()); + + final BigInt balance; + final int exponent; + + @override + String get formattedAdditionalBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + @override + String get formattedAvailableBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + String toJSON() => json.encode({ + 'balanceInWei': balance.toString(), + 'exponent': exponent, + }); + + static ERC20Balance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return ERC20Balance( + BigInt.parse(decoded['balanceInWei']), + exponent: decoded['exponent'], + ); + } catch (e) { + return ERC20Balance(BigInt.zero); + } + } +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..f00e2ef7b --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/pending_ethereum_transaction.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/contracts/erc20.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/.secrets.g.dart' as secrets; + +class EthereumClient { + final _httpClient = Client(); + Web3Client? _client; + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), _httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future getBalance(EthereumAddress address) async => + await _client!.getBalance(address); + + Future getGasUnitPrice() async { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } + + Future getEstimatedGas() async { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } + + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EthereumTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + }) async { + assert(currency == CryptoCurrency.eth || contractAddress != null); + + bool _isEthereum = currency == CryptoCurrency.eth; + + final price = await _client!.getGasPrice(); + + final Transaction transaction = Transaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxGas: gas, + gasPrice: price, + maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), + value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + ); + + final signedTransaction = await _client!.signTransaction(privateKey, transaction); + + final Function _sendTransaction; + + if (_isEthereum) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = Erc20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + BigInt.parse(amount), + credentials: privateKey, + ); + }; + } + + return PendingEthereumTransaction( + signedTransaction: signedTransaction, + amount: amount, + fee: BigInt.from(gas) * price.getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Future sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(signedTransaction); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + + // Print the receipt information + print('Transaction Hash: ${receipt.transactionHash}'); + print('Block Hash: ${receipt.blockHash}'); + print('Block Number: ${receipt.blockNumber}'); + print('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] +I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + print("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + // Print the receipt information + print('Transaction Hash: ${transactionInformation.hash}'); + print('Block Hash: ${transactionInformation.blockHash}'); + print('Block Number: ${transactionInformation.blockNumber}'); + print('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 +I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return ERC20Balance(balance, exponent: exponent); + } + + Future getErc20Token(String contractAddress) async { + try { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + void stop() { + _client?.dispose(); + } + + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final _jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) { + return (_jsonResponse['result'] as List) + .map((e) => EthereumTransactionModel.fromJson(e as Map)) + .toList(); + } + + return []; + } catch (e) { + print(e); + return []; + } + } + +// Future _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_ethereum/lib/ethereum_exceptions.dart b/cw_ethereum/lib/ethereum_exceptions.dart new file mode 100644 index 000000000..518f46275 --- /dev/null +++ b/cw_ethereum/lib/ethereum_exceptions.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EthereumTransactionCreationException implements Exception { + final String exceptionMessage; + + EthereumTransactionCreationException(CryptoCurrency currency) : + this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_ethereum/lib/ethereum_formatter.dart b/cw_ethereum/lib/ethereum_formatter.dart new file mode 100644 index 000000000..468c536f8 --- /dev/null +++ b/cw_ethereum/lib/ethereum_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const ethereumAmountLength = 12; +const ethereumAmountDivider = 1000000000000; +final ethereumAmountFormat = NumberFormat() + ..maximumFractionDigits = ethereumAmountLength + ..minimumFractionDigits = 1; + +class EthereumFormatter { + static int parseEthereumAmount(String amount) { + try { + return (double.parse(amount) * ethereumAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEthereumAmountToDouble(int amount) { + try { + return amount / ethereumAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics.dart b/cw_ethereum/lib/ethereum_mnemonics.dart new file mode 100644 index 000000000..8af7b10f3 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics.dart @@ -0,0 +1,2058 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class EthereumMnemonics { + static const englishWordlist = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' + ]; +} diff --git a/cw_ethereum/lib/ethereum_transaction_credentials.dart b/cw_ethereum/lib/ethereum_transaction_credentials.dart new file mode 100644 index 000000000..b015b7141 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_credentials.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + +class EthereumTransactionCredentials { + EthereumTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); + + final List outputs; + final EthereumTransactionPriority? priority; + final int? feeRate; + final CryptoCurrency currency; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart new file mode 100644 index 000000000..4511f4436 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; + +part 'ethereum_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'transactions.json'; + +class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; + +abstract class EthereumTransactionHistoryBase + extends TransactionHistoryBase with Store { + EthereumTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + print('Error while save ethereum transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = EthereumTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart new file mode 100644 index 000000000..efdc61407 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class EthereumTransactionInfo extends TransactionInfo { + EthereumTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + this.tokenSymbol = "ETH", + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + }) : this.amount = ethAmount.toInt(), + this.fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH'; + + factory EthereumTransactionInfo.fromJson(Map data) { + return EthereumTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + ); + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + }; +} diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_ethereum/lib/ethereum_transaction_model.dart new file mode 100644 index 000000000..c1260795a --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_model.dart @@ -0,0 +1,47 @@ +class EthereumTransactionModel { + final DateTime date; + final String hash; + final String from; + final String to; + final BigInt amount; + final int gasUsed; + final BigInt gasPrice; + final String contractAddress; + final int confirmations; + final int blockNumber; + final String? tokenSymbol; + final int? tokenDecimal; + final bool isError; + + EthereumTransactionModel({ + required this.date, + required this.hash, + required this.from, + required this.to, + required this.amount, + required this.gasUsed, + required this.gasPrice, + required this.contractAddress, + required this.confirmations, + required this.blockNumber, + required this.tokenSymbol, + required this.tokenDecimal, + required this.isError, + }); + + factory EthereumTransactionModel.fromJson(Map json) => EthereumTransactionModel( + date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), + hash: json["hash"], + from: json["from"], + to: json["to"], + amount: BigInt.parse(json["value"]), + gasUsed: int.parse(json["gasUsed"]), + gasPrice: BigInt.parse(json["gasPrice"]), + contractAddress: json["contractAddress"], + confirmations: int.parse(json["confirmations"]), + blockNumber: int.parse(json["blockNumber"]), + tokenSymbol: json["tokenSymbol"] ?? "ETH", + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_ethereum/lib/ethereum_transaction_priority.dart b/cw_ethereum/lib/ethereum_transaction_priority.dart new file mode 100644 index 000000000..ff5668397 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EthereumTransactionPriority extends TransactionPriority { + final int tip; + + const EthereumTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const EthereumTransactionPriority slow = + EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EthereumTransactionPriority medium = + EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EthereumTransactionPriority fast = + EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EthereumTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EthereumTransactionPriority.slow: + label = 'Slow'; + break; + case EthereumTransactionPriority.medium: + label = 'Medium'; + break; + case EthereumTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart new file mode 100644 index 000000000..46cb5c39f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/default_erc20_tokens.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_exceptions.dart'; +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_ethereum/ethereum_transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:hive/hive.dart'; +import 'package:hex/hex.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; + +part 'ethereum_wallet.g.dart'; + +class EthereumWallet = EthereumWalletBase with _$EthereumWallet; + +abstract class EthereumWalletBase + extends WalletBase + with Store { + EthereumWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + ERC20Balance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _isTransactionUpdating = false, + _client = EthereumClient(), + walletAddresses = EthereumWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password); + + if (!Hive.isAdapterRegistered(Erc20Token.typeId)) { + Hive.registerAdapter(Erc20TokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _mnemonic; + final String _password; + + late final Box erc20TokensBox; + + late final EthPrivateKey _privateKey; + + late EthereumClient _client; + + int? _gasPrice; + int? _estimatedGas; + bool _isTransactionUpdating; + + // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer _sharedPrefs = Completer(); + + Future init() async { + erc20TokensBox = await Hive.openBox(Erc20Token.boxName); + await walletAddresses.init(); + await transactionHistory.init(); + _privateKey = await getPrivateKey(_mnemonic, _password); + walletAddresses.address = _privateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EthereumTransactionPriority) { + final priorityFee = + EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Ethereum Node connection failed"); + } + + _client.setListeners(_privateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as EthereumTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final _erc20Balance = balance[_credentials.currency]!; + BigInt totalAmount = BigInt.zero; + int exponent = + _credentials.currency is Erc20Token ? (_credentials.currency as Erc20Token).decimal : 18; + num amountToEthereumMultiplier = pow(10, exponent); + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EthereumTransactionCreationException(_credentials.currency); + } + + final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } else { + final output = outputs.first; + final BigInt allAmount = + _erc20Balance.balance - BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + final totalOriginalAmount = + EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } + + final pendingEthereumTransaction = await _client.signTransaction( + privateKey: _privateKey, + toAddress: _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: _credentials.currency, + exponent: exponent, + contractAddress: _credentials.currency is Erc20Token + ? (_credentials.currency as Erc20Token).contractAddress + : null, + ); + + return pendingEthereumTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true; + if (!isEtherscanEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _privateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + ); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String get seed => _mnemonic; + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await _updateTransactions(); + _gasPrice = await _client.getGasUnitPrice(); + _estimatedGas = await _client.getEstimatedGas(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); + Timer.periodic(const Duration(seconds: 10), + (timer) async => _estimatedGas = await _client.getEstimatedGas()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'balance': balance[currency]!.toJSON(), + }); + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; + final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); + + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchEthBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchEthBalance() async { + final balance = await _client.getBalance(_privateKey.address); + return ERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in erc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _privateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future getPrivateKey(String mnemonic, String password) async { + final seed = bip39.mnemonicToSeed(mnemonic); + + final root = bip32.BIP32.fromSeed(seed); + + const _hdPathEthereum = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$_hdPathEthereum/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => erc20TokensBox.values.toList(); + + Future addErc20Token(Erc20Token token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final _token = Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + iconPath: iconPath, + ); + + await erc20TokensBox.put(_token.contractAddress, _token); + + if (_token.enabled) { + balance[_token] = await _client.fetchERC20Balances( + _privateKey.address, + _token.contractAddress, + ); + } else { + balance.remove(_token); + } + } + + Future deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future getErc20Token(String contractAddress) async => + await _client.getErc20Token(contractAddress); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + void addInitialTokens() { + final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens; + + initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token)); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(Duration(seconds: 10), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + void updateEtherscanUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_ethereum/lib/ethereum_wallet_addresses.dart new file mode 100644 index 000000000..4a3492e6f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'ethereum_wallet_addresses.g.dart'; + +class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; + +abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { + EthereumWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart new file mode 100644 index 000000000..12d0d53e2 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EthereumNewWalletCredentials extends WalletCredentials { + EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { + EthereumRestoreWalletFromSeedCredentials( + {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials { + EthereumRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart new file mode 100644 index 000000000..318f287fc --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; + +class EthereumWalletService extends WalletService { + EthereumWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(EthereumNewWalletCredentials credentials) async { + final mnemonic = bip39.generateMnemonic(); + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + WalletType getType() => WalletType.ethereum; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await EthereumWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values.firstWhereOrNull( + (info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromKeys(credentials) { + throw UnimplementedError(); + } + + @override + Future restoreFromSeed( + EthereumRestoreWalletFromSeedCredentials credentials) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = EthereumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_ethereum/lib/file.dart b/cw_ethereum/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_ethereum/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_ethereum/lib/pending_ethereum_transaction.dart b/cw_ethereum/lib/pending_ethereum_transaction.dart new file mode 100644 index 000000000..23dfa3b87 --- /dev/null +++ b/cw_ethereum/lib/pending_ethereum_transaction.dart @@ -0,0 +1,36 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingEthereumTransaction with PendingTransaction { + final Function sendTransaction; + final Uint8List signedTransaction; + final BigInt fee; + final String amount; + final int exponent; + + PendingEthereumTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + required this.exponent, + }); + + @override + String get amountFormatted => (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted => (fee / BigInt.from(pow(10, 18))).toString(); + + @override + String get hex => bytesToHex(signedTransaction, include0x: true); + + @override + String get id => ''; +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml new file mode 100644 index 000000000..cb1046d5a --- /dev/null +++ b/cw_ethereum/pubspec.yaml @@ -0,0 +1,68 @@ +name: cw_ethereum +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: 2.3.5 + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^0.13.4 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart new file mode 100644 index 000000000..72026a4c0 --- /dev/null +++ b/cw_ethereum/test/cw_ethereum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_ethereum/cw_ethereum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index a38c4721c..226ace6a1 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -254,6 +254,7 @@ abstract class HavenWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: name, type: type); final currentCacheFile = File(currentWalletPath); diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 90cc3c279..748b65329 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromMap(Map map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), isPending = parseBoolFromString(map['isPending'] as String), amount = map['amount'] as int, accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, confirmations = map['confirmations'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int ?? 0 { + fee = map['fee'] as int? ?? 0 { additionalInfo = { 'key': key, 'accountIndex': accountIndex, @@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, + direction = parseTransactionDirectionFromInt(row.direction), date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), isPending = row.isPending != 0, amount = row.getAmount(), diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 104b3ebe8..ef25b6b93 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -269,6 +269,7 @@ abstract class MoneroWalletBase extends WalletBase renameWalletFiles(String newWalletName) async { final currentWalletDirPath = await pathForWalletDir(name: name, type: type); diff --git a/howto-build-android.md b/howto-build-android.md index 4ef385b9f..d37f1b417 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -6,9 +6,9 @@ The following are the system requirements to build CakeWallet for your Android d ``` Ubuntu >= 16.04 -Android SDK 28 +Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 2 or above +Flutter 3.7.x ``` ## Building CakeWallet on Android @@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Need to install flutter with version `3.x.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,9 +66,9 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.x.x, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 28) -[✓] Android Studio (version 4.0) +[✓] Flutter (Channel stable, 3.7.x, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) +[✓] Android Studio (version 4.0 or higher) ``` ### 5. Generate a secure keystore for Android diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630ecf27f..dfd3b1538 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority != null ? priority as BitcoinTransactionPriority : null, + priority: priority as BitcoinTransactionPriority, feeRate: feeRate); @override diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 2b3056343..f2a235363 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) @@ -14,6 +15,9 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { + if (type is Erc20Token) { + return '0x[0-9a-zA-Z]'; + } switch (type) { case CryptoCurrency.xmr: return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; @@ -56,6 +60,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -116,17 +121,14 @@ class AddressValidator extends TextValidator { } static List? getLength(CryptoCurrency type) { + if (type is Erc20Token) { + return [42]; + } switch (type) { case CryptoCurrency.xmr: return null; case CryptoCurrency.ada: return null; - case CryptoCurrency.avaxc: - return [42]; - case CryptoCurrency.bch: - return [42]; - case CryptoCurrency.bnb: - return [42]; case CryptoCurrency.btc: return null; case CryptoCurrency.dash: @@ -166,6 +168,10 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: + case CryptoCurrency.avaxc: + case CryptoCurrency.bch: + case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; @@ -203,11 +209,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xusd: return [98, 99, 106]; case CryptoCurrency.btt: - return [34]; case CryptoCurrency.bttc: - return [34]; case CryptoCurrency.doge: - return [34]; case CryptoCurrency.firo: return [34]; case CryptoCurrency.hbar: @@ -258,6 +261,8 @@ class AddressValidator extends TextValidator { return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.eth: + return '0x[0-9a-zA-Z]{42}'; default: return null; } diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 3f3eedd57..a109b75cd 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -240,6 +240,9 @@ class BackupService { data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; + final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; + final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; + final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -349,6 +352,15 @@ class BackupService { PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + if (sortBalanceTokensBy != null) + await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + + if (pinNativeTokenAtTop != null) + await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + + if (useEtherscan != null) + await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + await preferencesFile.delete(); } @@ -492,6 +504,12 @@ class BackupService { _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + PreferencesKey.sortBalanceBy: + _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.pinNativeTokenAtTop: + _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: + _sharedPreferences.getBool(PreferencesKey.useEtherscan), }; return json.encode(preferences); diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 11aef1374..9690c430a 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,21 +5,20 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; - const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; const _fiatApiPath = '/v2/rates'; Future _fetchPrice(Map args) async { - final crypto = args['crypto'] as CryptoCurrency; - final fiat = args['fiat'] as FiatCurrency; + final crypto = args['crypto'] as String; + final fiat = args['fiat'] as String; final torOnly = args['torOnly'] as bool; final Map queryParams = { 'interval_count': '1', - 'base': crypto.toString(), - 'quote': fiat.toString(), - 'key' : secrets.fiatApiKey, + 'base': crypto, + 'quote': fiat, + 'key': secrets.fiatApiKey, }; double price = 0.0; @@ -52,7 +51,11 @@ Future _fetchPrice(Map args) async { } Future _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); + compute(_fetchPrice, { + 'fiat': fiat.toString(), + 'crypto': crypto.toString(), + 'torOnly': torOnly, + }); class FiatConversionService { static Future fetchPrice({ diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index fe9a25f85..eba1bbda4 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; @@ -25,6 +26,8 @@ class SeedValidator extends Validator { return monero!.getMoneroWordList(language); case WalletType.haven: return haven!.getMoneroWordList(language); + case WalletType.ethereum: + return ethereum!.getEthereumWordList(language); default: return []; } diff --git a/lib/di.dart b/lib/di.dart index f287b0e9a..c27659e7a 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; @@ -16,6 +17,8 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; @@ -40,6 +43,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; @@ -70,6 +74,7 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -239,9 +244,9 @@ Future setup({ getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); } - final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) && - (secrets.wyreApiKey.isNotEmpty ?? false) && - (secrets.wyreAccountId.isNotEmpty ?? false); + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && + (secrets.wyreApiKey.isNotEmpty) && + (secrets.wyreAccountId.isNotEmpty); final settingsStore = await SettingsStoreBase.load( nodeSource: _nodeSource, @@ -638,7 +643,7 @@ Future setup({ }); getIt.registerFactory(() { - return PrivacySettingsViewModel(getIt.get()); + return PrivacySettingsViewModel(getIt.get(), getIt.get().wallet!); }); getIt.registerFactory(() { @@ -745,6 +750,8 @@ Future setup({ return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + case WalletType.ethereum: + return ethereum!.createEthereumWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -787,8 +794,8 @@ Future setup({ transactionDetailsViewModel: getIt.get(param1: transactionInfo))); - getIt.registerFactoryParam( - (param1, _) => NewWalletTypePage(onTypeSelected: param1)); + getIt.registerFactoryParam( + (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( (WalletType type, _) => PreSeedPage(type)); @@ -1034,5 +1041,19 @@ Future setup({ getIt.registerFactoryParam( (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); + getIt.registerFactoryParam((balanceViewModel, _) => + HomeSettingsPage(getIt.get(param1: balanceViewModel))); + + getIt.registerFactoryParam( + (balanceViewModel, _) => HomeSettingsViewModel(getIt.get(), balanceViewModel)); + + getIt.registerFactoryParam>( + (homeSettingsViewModel, arguments) => EditTokenPage( + homeSettingsViewModel: homeSettingsViewModel, + erc20token: arguments['token'] as Erc20Token?, + initialContractAddress: arguments['contractAddress'] as String?, + ), + ); + _isSetupFinished = true; } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 77298c2b5..b4cb23131 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; +const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; Future defaultSettingsMigration( {required int version, @@ -157,6 +158,12 @@ Future defaultSettingsMigration( case 20: await migrateExchangeStatus(sharedPreferences); break; + case 21: + await addEthereumNodeList(nodes: nodes); + await changeEthereumCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + default: break; } @@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); } +Node? getEthereumDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == ethereumDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -438,6 +451,8 @@ Future checkCurrentNodes( .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final currentHavenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull( (node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( @@ -446,6 +461,8 @@ Future checkCurrentNodes( (node) => node.key == currentLitecoinElectrumSeverId); final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -479,6 +496,13 @@ Future checkCurrentNodes( await sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); } + + if (currentEthereumNodeServer == null) { + final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -522,8 +546,26 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { return; } - await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled ? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw); - + await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } + +Future addEthereumNodeList({required Box nodes}) async { + final nodeList = await loadDefaultEthereumNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeEthereumCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box nodes}) async { + final node = getEthereumDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); +} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 0cf3cead4..d6a7445f9 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -46,6 +46,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledBuyAction) { final uri = getIt.get().requestUrl(); if (DeviceInfo.instance.isMobile) { @@ -116,6 +117,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); final uri = await moonPaySellProvider.requestUrl( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 58847ccfa..b06351a79 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -70,6 +70,22 @@ Future> loadDefaultHavenNodes() async { return nodes; } +Future> loadDefaultEthereumNodes() async { + final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 027e05f55..62c47ea02 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -31,6 +32,7 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; + static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; @@ -38,6 +40,9 @@ class PreferencesKey { static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; + static const sortBalanceBy = 'sort_balance_by'; + static const pinNativeTokenAtTop = 'pin_native_token_at_top'; + static const useEtherscan = 'use_etherscan'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 927ab8803..eb9417763 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -14,6 +15,8 @@ List priorityForWalletType(WalletType type) { return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.haven: return haven!.getTransactionPriorities(); + case WalletType.ethereum: + return ethereum!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/sort_balance_types.dart b/lib/entities/sort_balance_types.dart new file mode 100644 index 000000000..5db64884e --- /dev/null +++ b/lib/entities/sort_balance_types.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum SortBalanceBy { + FiatBalance, + GrossBalance, + Alphabetical; + + @override + String toString() { + switch (this) { + case SortBalanceBy.FiatBalance: + return S.current.fiat_balance; + case SortBalanceBy.GrossBalance: + return S.current.gross_balance; + case SortBalanceBy.Alphabetical: + return S.current.alphabetical; + } + } +} \ No newline at end of file diff --git a/lib/entities/template.dart b/lib/entities/template.dart index 8224ecdd8..6955136e0 100644 --- a/lib/entities/template.dart +++ b/lib/entities/template.dart @@ -55,5 +55,5 @@ class Template extends HiveObject { String get amount => amountRaw ?? ''; - List