From 2da3ba5730c020f9908a831611c365d66e9e3a43 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:30:23 +0100 Subject: [PATCH] CW-1194-Add-Arbitrum-Wallet (#2587) * feat: initialize cw_arbitrum package with essential files - Implement ArbitrumClient for transaction handling - Add ArbitrumWallet and related classes for wallet management - Establish transaction history and info classes for Arbitrum transactions - Add ArbitrumWalletService for wallet operations and management * feat: Add arbitrum related secrets to workflow * feat: Add nodes for arbitrum * feat: Add Arbitrum support to cryptocurrency handling * refactor: Update Arbitrum currency references from 'ARB' to 'ETH' * feat: Add Arbitrum support to wallet type and transaction handling * feat: Add Arbitrum URL schemes to Android and iOS configurations * feat: Add Arbitrum SVG icons and a WebP image * feat: Setup Arbitrum proxy * fix: Add localization * feat: Integrate Arbitrum support across various configurations and files - Updated cakewallet.bat to include Arbitrum in app configuration. - Added Arbitrum node list to pubspec_base.yaml. - Modified Android, iOS, Linux, and macOS app configuration scripts to support Arbitrum. - Introduced Arbitrum output path and generation logic in configure.dart. - Enhanced wallet type and pubspec generation to include Arbitrum. - Added Arbitrum-related secrets in secret_key.dart. * feat: Enhance Arbitrum integration with new URI class and updates across various components - Introduced ArbitrumURI class for handling Arbitrum payment URIs. - Updated seed validator, wallet creation service, and other components to support Arbitrum. - Added Arbitrum node handling in default settings migration and node list management. - Enhanced preferences and settings store to include Arbitrum-specific configurations. - Updated QR utility and token utilities to accommodate Arbitrum functionalities. * feat: Add Arbitrum support to various buy providers - Updated DFXBuyProvider, KryptonimBuyProvider, OnRamperBuyProvider, and RobinhoodBuyProvider to include Arbitrum wallet type and blockchain references. - Enhanced integration for Arbitrum across multiple components to ensure consistent support. * feat: Implement Arbitrum support in wallet selection and settings - Added Arbitrum wallet type handling in common test flows and wallet services. - Integrated Arbitrum icons in desktop wallet selection and menu widgets. - Updated privacy settings to include ArbiScan options. - Enhanced key service to support Arbitrum private and public key retrieval. * feat: Expand Arbitrum support across various view models and components - Integrated Arbitrum wallet type handling in advanced privacy settings, transaction details, and wallet management. - Added Arbitrum-specific methods for transaction and wallet creation, restoration, and fee management. - Updated dashboard and home settings to accommodate Arbitrum functionalities, including token management and transaction display. - Enhanced exchange and send view models to support Arbitrum transactions and currency handling. - Included Arbitrum in wallet address list and privacy settings for improved user experience. * refactor: Simplify token initialization in Arbitrum wallet service - Removed the migration flag from the addInitialTokens method in ArbitrumWalletService. - Updated addInitialTokens method in ArbitrumWallet to handle existing tokens more efficiently. - Enhanced initial token retrieval logic in DefaultArbitrumErc20Tokens to prioritize iconPath assignment. - Improved token update logic in BaseWallet for consistency across wallet services. * chore: Update Dart SDK constraints in pubspec.yaml * fix: Update file paths for arbitrum assets * fix: Correct box name formatting in Arbitrum wallet initialization * Update .github/workflows/pr_test_build_android.yml [skip ci * Update .github/workflows/pr_test_build_android.yml * fix: Add missing evm params * fix: Remove duplicate method declaration * refactor: simplify isSwapsXyzSendingEVMTokenSwap check * fix: Update arbitrum configs with sql storage update * feat: Add estimated fee retrieval for Arbitrum and revamp priority fee handling for EVMChainWallet * refactor: Update transaction priority handling and estimated fee calculations across EVMChain chains, also remove priority fee levels for Arbitrum --------- Co-authored-by: Omar Hatem --- .../workflows/automated_integration_test.yml | 2 + .github/workflows/no_restricted_imports.yaml | 1 + .github/workflows/pr_test_build_android.yml | 2 + .github/workflows/pr_test_build_linux.yml | 2 + .gitignore | 1 + README.md | 1 + analysis_options.yaml | 1 + android/app/src/main/AndroidManifestBase.xml | 3 + assets/arbitrum_node_list.yml | 12 + assets/images/arbitrum_chain_QR.svg | 27 ++ assets/images/arbitrum_chain_mono.svg | 3 + assets/images/crypto/arbitrum.webp | Bin 0 -> 29132 bytes cakewallet.bat | 4 +- cw_arbitrum/.gitignore | 31 ++ cw_arbitrum/.metadata | 10 + cw_arbitrum/CHANGELOG.md | 3 + cw_arbitrum/LICENSE | 1 + cw_arbitrum/README.md | 39 ++ cw_arbitrum/analysis_options.yaml | 4 + .../lib/arbirtrum_mnemonics_exception.dart | 5 + cw_arbitrum/lib/arbitrum_client.dart | 107 ++++++ .../lib/arbitrum_transaction_history.dart | 20 ++ .../lib/arbitrum_transaction_info.dart | 41 +++ cw_arbitrum/lib/arbitrum_wallet.dart | 179 ++++++++++ cw_arbitrum/lib/arbitrum_wallet_service.dart | 174 +++++++++ cw_arbitrum/lib/cw_arbitrum.dart | 5 + .../lib/default_arbitrum_erc20_tokens.dart | 71 ++++ cw_arbitrum/pubspec.yaml | 73 ++++ cw_arbitrum/test/cw_arbitrum_test.dart | 12 + cw_base/lib/base_wallet.dart | 10 +- cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/currency_for_wallet_type.dart | 2 + cw_core/lib/erc20_token.dart | 1 + cw_core/lib/node.dart | 2 + cw_core/lib/wallet_base.dart | 2 + cw_core/lib/wallet_type.dart | 14 + cw_evm/lib/evm_chain_client.dart | 17 +- cw_evm/lib/evm_chain_wallet.dart | 334 ++++++++++-------- .../components/common_test_flows.dart | 4 + ios/Runner/InfoBase.plist | 20 ++ lib/arbitrum/cw_arbitrum.dart | 244 +++++++++++++ lib/base/cw_base.dart | 19 +- lib/buy/dfx/dfx_buy_provider.dart | 1 + lib/buy/kryptonim/kryptonim.dart | 2 + lib/buy/onramper/onramper_buy_provider.dart | 2 + lib/buy/robinhood/robinhood_buy_provider.dart | 1 + .../open_cryptopay_service.dart | 2 + lib/core/payment_uris.dart | 15 + lib/core/seed_validator.dart | 3 + lib/core/wallet_creation_service.dart | 1 + lib/di.dart | 3 + lib/entities/default_settings_migration.dart | 22 ++ lib/entities/ens_record.dart | 6 + lib/entities/node_list.dart | 7 +- lib/entities/parse_address_from_domain.dart | 1 + lib/entities/preferences_key.dart | 2 + lib/entities/priority_for_wallet_type.dart | 3 +- lib/ethereum/cw_ethereum.dart | 26 +- .../provider/sideshift_exchange_provider.dart | 3 + lib/main.dart | 2 +- lib/polygon/cw_polygon.dart | 134 ++++--- lib/reactions/fiat_rate_update.dart | 5 + lib/reactions/on_current_wallet_change.dart | 5 + lib/reactions/wallet_connect.dart | 12 + lib/reactions/wallet_utils.dart | 2 + .../desktop_wallet_selection_dropdown.dart | 3 + .../dashboard/widgets/menu_widget.dart | 4 + .../exchange_trade_card_item_widget.dart | 8 +- lib/src/screens/send/widgets/send_card.dart | 11 +- lib/src/screens/settings/privacy_page.dart | 8 + .../wallet_connect_key_service.dart | 5 + lib/store/settings_store.dart | 28 ++ lib/utils/qr_util.dart | 2 + lib/utils/token_utilities.dart | 3 +- .../advanced_privacy_settings_view_model.dart | 2 + .../dashboard/balance_view_model.dart | 1 + .../dashboard/dashboard_view_model.dart | 1 + .../dashboard/home_settings_view_model.dart | 47 ++- .../dashboard/transaction_list_item.dart | 12 + .../exchange/exchange_trade_view_model.dart | 33 +- .../exchange/exchange_view_model.dart | 4 + .../node_create_or_edit_view_model.dart | 1 + .../payment/payment_view_model.dart | 1 - .../restore/wallet_restore_from_qr_code.dart | 1 + lib/view_model/send/fees_view_model.dart | 5 +- lib/view_model/send/output.dart | 106 ++++-- .../send/send_template_view_model.dart | 1 + lib/view_model/send/send_view_model.dart | 57 ++- .../settings/privacy_settings_view_model.dart | 12 + .../transaction_details_view_model.dart | 57 +++ .../wallet_address_list_view_model.dart | 17 + lib/view_model/wallet_keys_view_model.dart | 3 + lib/view_model/wallet_new_vm.dart | 8 + lib/view_model/wallet_restore_view_model.dart | 16 + pubspec_base.yaml | 1 + res/values/strings_ar.arb | 1 + res/values/strings_bg.arb | 1 + res/values/strings_cs.arb | 1 + res/values/strings_de.arb | 1 + res/values/strings_en.arb | 1 + res/values/strings_es.arb | 7 +- res/values/strings_fr.arb | 1 + res/values/strings_ha.arb | 1 + res/values/strings_hi.arb | 1 + res/values/strings_hr.arb | 1 + res/values/strings_hy.arb | 1 + res/values/strings_id.arb | 1 + res/values/strings_it.arb | 1 + res/values/strings_ja.arb | 1 + res/values/strings_ko.arb | 1 + res/values/strings_my.arb | 1 + res/values/strings_nl.arb | 1 + res/values/strings_pl.arb | 21 +- res/values/strings_pt.arb | 1 + res/values/strings_ru.arb | 1 + res/values/strings_th.arb | 1 + res/values/strings_tl.arb | 1 + res/values/strings_tr.arb | 1 + res/values/strings_uk.arb | 1 + res/values/strings_ur.arb | 1 + res/values/strings_vi.arb | 1 + res/values/strings_yo.arb | 1 + res/values/strings_zh.arb | 1 + scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/linux/app_config.sh | 2 +- scripts/macos/app_config.sh | 2 +- tool/configure.dart | 155 +++++++- tool/utils/secret_key.dart | 2 + 129 files changed, 2123 insertions(+), 319 deletions(-) create mode 100644 assets/arbitrum_node_list.yml create mode 100644 assets/images/arbitrum_chain_QR.svg create mode 100644 assets/images/arbitrum_chain_mono.svg create mode 100644 assets/images/crypto/arbitrum.webp create mode 100644 cw_arbitrum/.gitignore create mode 100644 cw_arbitrum/.metadata create mode 100644 cw_arbitrum/CHANGELOG.md create mode 100644 cw_arbitrum/LICENSE create mode 100644 cw_arbitrum/README.md create mode 100644 cw_arbitrum/analysis_options.yaml create mode 100644 cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart create mode 100644 cw_arbitrum/lib/arbitrum_client.dart create mode 100644 cw_arbitrum/lib/arbitrum_transaction_history.dart create mode 100644 cw_arbitrum/lib/arbitrum_transaction_info.dart create mode 100644 cw_arbitrum/lib/arbitrum_wallet.dart create mode 100644 cw_arbitrum/lib/arbitrum_wallet_service.dart create mode 100644 cw_arbitrum/lib/cw_arbitrum.dart create mode 100644 cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart create mode 100644 cw_arbitrum/pubspec.yaml create mode 100644 cw_arbitrum/test/cw_arbitrum_test.dart create mode 100644 lib/arbitrum/cw_arbitrum.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index 5ccf4aacb..8ff5c137a 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -164,6 +164,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds2 = '${{ secrets.SOLANA_TEST_WALLET_SEEDS2 }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -178,6 +179,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/no_restricted_imports.yaml b/.github/workflows/no_restricted_imports.yaml index 2b07c27cd..18b228dcb 100644 --- a/.github/workflows/no_restricted_imports.yaml +++ b/.github/workflows/no_restricted_imports.yaml @@ -21,6 +21,7 @@ jobs: "cw_nano" "cw_polygon" "cw_base" + "cw_arbitrum" "cw_solana" "cw_tron" "cw_wownero" diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index a8ea6aab2..02f6e1b4c 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -158,6 +158,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -170,6 +171,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 0e736ee64..903f77a75 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -151,6 +151,7 @@ jobs: echo "const bitcoinCashTestWalletSeeds = '${{ secrets.BITCOIN_CASH_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletSeeds = '${{ secrets.POLYGON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletSeeds = '${{ secrets.BASE_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletSeeds = '${{ secrets.ARBITRUM_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletSeeds = '${{ secrets.SOLANA_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletSeeds = '${{ secrets.TRON_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletSeeds = '${{ secrets.NANO_TEST_WALLET_SEEDS }}';" >> lib/.secrets.g.dart @@ -164,6 +165,7 @@ jobs: echo "const bitcoinCashTestWalletReceiveAddress = '${{ secrets.BITCOIN_CASH_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const polygonTestWalletReceiveAddress = '${{ secrets.POLYGON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const baseTestWalletReceiveAddress = '${{ secrets.BASE_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart + echo "const arbitrumTestWalletReceiveAddress = '${{ secrets.ARBITRUM_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const solanaTestWalletReceiveAddress = '${{ secrets.SOLANA_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const tronTestWalletReceiveAddress = '${{ secrets.TRON_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart echo "const nanoTestWalletReceiveAddress = '${{ secrets.NANO_TEST_WALLET_RECEIVE_ADDRESS }}';" >> lib/.secrets.g.dart diff --git a/.gitignore b/.gitignore index be83f1a62..cd2230504 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,7 @@ lib/zano/zano.dart lib/decred/decred.dart lib/dogecoin/dogecoin.dart lib/base/base.dart +lib/arbitrum/arbitrum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png diff --git a/README.md b/README.md index 1266d08a5..43a72e0ad 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Cake Wallet includes support for several cryptocurrencies, including: * Bitcoin Cash (BCH) * Polygon (POL) * Base (BASE) +* Arbitrum (ARB) * Solana (SOL) * Tron (TRX) * Nano (XNO) diff --git a/analysis_options.yaml b/analysis_options.yaml index 8b07015a2..4f6f0f6e9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,6 +25,7 @@ analyzer: lib/wownero/cw_wownero.dart, lib/zano/cw_zano.dart, lib/base/cw_base.dart, + lib/arbitrum/cw_arbitrum.dart, ] language: strict-casts: true diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 287554cd3..6b3b2cb2d 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -109,6 +109,9 @@ + + + diff --git a/assets/arbitrum_node_list.yml b/assets/arbitrum_node_list.yml new file mode 100644 index 000000000..63894f18a --- /dev/null +++ b/assets/arbitrum_node_list.yml @@ -0,0 +1,12 @@ +- + uri: arbitrum.nownodes.io + is_default: true + useSSL: true + isEnabledForAutoSwitching: true +- + uri: arbitrum.drpc.org + useSSL: true +- + uri: arbitrum-one-rpc.publicnode.com + useSSL: true + isEnabledForAutoSwitching: true \ No newline at end of file diff --git a/assets/images/arbitrum_chain_QR.svg b/assets/images/arbitrum_chain_QR.svg new file mode 100644 index 000000000..fa3338346 --- /dev/null +++ b/assets/images/arbitrum_chain_QR.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/arbitrum_chain_mono.svg b/assets/images/arbitrum_chain_mono.svg new file mode 100644 index 000000000..480dd1dad --- /dev/null +++ b/assets/images/arbitrum_chain_mono.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/crypto/arbitrum.webp b/assets/images/crypto/arbitrum.webp new file mode 100644 index 0000000000000000000000000000000000000000..b0c78bd9f1495bb37171841c70d65bd6c83f822b GIT binary patch literal 29132 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
M{9NO^Rd4LolM+C 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 ij1r(f1J{vImMF#`a#@-0yS literal 0 HcmV?d00001 diff --git a/cakewallet.bat b/cakewallet.bat index 8baa910e8..d601741d5 100644 --- a/cakewallet.bat +++ b/cakewallet.bat @@ -1,5 +1,5 @@ @echo off -set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --dogecoin --base +set cw_win_app_config=--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --dogecoin --base --arbitrum set cw_root=%cd% set cw_archive_name=Cake Wallet.zip set cw_archive_path=%cw_root%\%cw_archive_name% @@ -24,7 +24,7 @@ IF NOT EXIST "%secrets_file_path%" ( ) ELSE (echo === Using previously/already generated secrets file: %secrets_file_path% ===) echo === Generating mobx models === -for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron cw_base.) do ( +for /d %%i in (cw_core cw_monero cw_bitcoin cw_ethereum cw_evm cw_polygon cw_nano cw_bitcoin_cash cw_solana cw_tron cw_base cw_arbitrum.) do ( cd %%i call flutter pub get > nul call dart run build_runner build --delete-conflicting-outputs > nul diff --git a/cw_arbitrum/.gitignore b/cw_arbitrum/.gitignore new file mode 100644 index 000000000..eb6c05cd3 --- /dev/null +++ b/cw_arbitrum/.gitignore @@ -0,0 +1,31 @@ +# 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/ +.flutter-plugins +.flutter-plugins-dependencies +build/ diff --git a/cw_arbitrum/.metadata b/cw_arbitrum/.metadata new file mode 100644 index 000000000..6f8d1aa9c --- /dev/null +++ b/cw_arbitrum/.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: "fcf2c11572af6f390246c056bc905eca609533a0" + channel: "[user-branch]" + +project_type: package diff --git a/cw_arbitrum/CHANGELOG.md b/cw_arbitrum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_arbitrum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_arbitrum/LICENSE b/cw_arbitrum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_arbitrum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_arbitrum/README.md b/cw_arbitrum/README.md new file mode 100644 index 000000000..4a260d8d2 --- /dev/null +++ b/cw_arbitrum/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_arbitrum/analysis_options.yaml b/cw_arbitrum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_arbitrum/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_arbitrum/lib/arbirtrum_mnemonics_exception.dart b/cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart new file mode 100644 index 000000000..8af5612db --- /dev/null +++ b/cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class ArbitrumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Arbitrum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_arbitrum/lib/arbitrum_client.dart b/cw_arbitrum/lib/arbitrum_client.dart new file mode 100644 index 000000000..f9fb36d34 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_client.dart @@ -0,0 +1,107 @@ +import 'dart:convert'; + +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class ArbitrumClient extends EVMChainClient { + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + }) { + EtherAmount? finalGasPrice = gasPrice; + + if (gasPrice == null && maxFeePerGas != null) { + // If we have EIP-1559 parameters but no legacy gasPrice, then use maxFeePerGas as gasPrice + finalGasPrice = maxFeePerGas; + } + + return Transaction( + from: from, + to: to, + value: amount, + data: data, + maxGas: maxGas, + gasPrice: finalGasPrice, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 42161; + + @override + Future> fetchTransactions( + String address, { + String? contractAddress, + }) async { + try { + final response = await client.get( + Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", + "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) { + final res = (jsonResponse['result'] as List); + + res.removeWhere((e) => e['value'] == '0'); + + return res + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) + .toList(); + } + + return []; + } catch (e) { + return []; + } + } + + @override + Future> fetchInternalTransactions(String address) async { + try { + final response = await client.get( + Uri.https("api.etherscan.io", "/v2/api", { + "chainid": "$chainId", + "module": "account", + "action": "txlistinternal", + "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) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) + .toList(); + } + + return []; + } catch (_) { + return []; + } + } +} diff --git a/cw_arbitrum/lib/arbitrum_transaction_history.dart b/cw_arbitrum/lib/arbitrum_transaction_history.dart new file mode 100644 index 000000000..1319c7aea --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_transaction_history.dart @@ -0,0 +1,20 @@ +import 'dart:core'; + +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_arbitrum/arbitrum_transaction_info.dart'; + +class ArbitrumTransactionHistory extends EVMChainTransactionHistory { + ArbitrumTransactionHistory({ + required super.walletInfo, + required super.password, + required super.encryptionFileUtils, + }); + + @override + String getTransactionHistoryFileName() => 'arbitrum_transactions.json'; + + @override + EVMChainTransactionInfo getTransactionInfo(Map val) => + ArbitrumTransactionInfo.fromJson(val); +} diff --git a/cw_arbitrum/lib/arbitrum_transaction_info.dart b/cw_arbitrum/lib/arbitrum_transaction_info.dart new file mode 100644 index 000000000..d12499991 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_transaction_info.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; + +class ArbitrumTransactionInfo extends EVMChainTransactionInfo { + ArbitrumTransactionInfo({ + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.contractAddress, + super.exponent, + }); + + factory ArbitrumTransactionInfo.fromJson(Map data) { + return ArbitrumTransactionInfo( + 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, + to: data['to'], + from: data['from'], + contractAddress: data['contractAddress'], + ); + } + + @override + String get feeCurrency => 'ETH'; +} diff --git a/cw_arbitrum/lib/arbitrum_wallet.dart b/cw_arbitrum/lib/arbitrum_wallet.dart new file mode 100644 index 000000000..6181f11a2 --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_wallet.dart @@ -0,0 +1,179 @@ +import 'dart:convert'; + +import 'package:cw_arbitrum/arbitrum_client.dart'; +import 'package:cw_arbitrum/arbitrum_transaction_history.dart'; +import 'package:cw_arbitrum/arbitrum_transaction_info.dart'; +import 'package:cw_arbitrum/default_arbitrum_erc20_tokens.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_keys_file.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; + +class ArbitrumWallet extends EVMChainWallet { + ArbitrumWallet({ + required super.walletInfo, + required super.password, + required super.derivationInfo, + super.mnemonic, + super.initialBalance, + super.privateKey, + required super.client, + required super.encryptionFileUtils, + super.passphrase, + }) : super(nativeCurrency: CryptoCurrency.arbEth); + + @override + bool get hasPriorityFee => false; + + @override + Future initErc20TokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.arbitrumBoxName}"; + + evmChainErc20TokensBox = await CakeHive.openBox(boxName); + } + + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultArbitrumErc20Tokens().initialArbitrumErc20Tokens; + + for (final token in initialErc20Tokens) { + if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } else { + // update existing token + final existingToken = evmChainErc20TokensBox.get(token.contractAddress); + evmChainErc20TokensBox.put( + token.contractAddress, + Erc20Token.copyWith(token, enabled: existingToken!.enabled), + ); + } + } + } + + @override + List get getDefaultTokenContractAddresses => DefaultArbitrumErc20Tokens() + .initialArbitrumErc20Tokens + .map((e) => e.contractAddress) + .toList(); + + @override + Future checkIfScanProviderIsEnabled() async { + return (await sharedPrefs.future).getBool("use_arbiscan") ?? true; + } + + @override + String getTransactionHistoryFileName() => 'arbitrum_transactions.json'; + + @override + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? 'ETH', + iconPath: iconPath, + isPotentialScam: token.isPotentialScam, + ); + } + + @override + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, + String address, + ) { + final model = ArbitrumTransactionInfo( + 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", + to: transactionModel.to, + from: transactionModel.from, + contractAddress: transactionModel.contractAddress, + ); + return model; + } + + @override + EVMChainTransactionHistory setUpTransactionHistory( + WalletInfo walletInfo, + String password, + EncryptionFileUtils encryptionFileUtils, + ) { + return ArbitrumTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + ); + } + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, + }) async { + final hasKeysFile = await WalletKeysFile.hasKeysFile(name, walletInfo.type); + final path = await pathForWallet(name: name, type: walletInfo.type); + + Map? data; + try { + final jsonSource = await encryptionFileUtils.read(path: path, password: password); + + data = json.decode(jsonSource) as Map; + } catch (e) { + if (!hasKeysFile) rethrow; + } + + final balance = + EVMChainERC20Balance.fromJSON(data?['balance'] as String?) ?? + EVMChainERC20Balance(BigInt.zero); + + final WalletKeysData keysData; + // Migrate wallet from the old scheme to then new .keys file scheme + if (!hasKeysFile) { + final mnemonic = data!['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final passphrase = data['passphrase'] as String?; + + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase); + } else { + keysData = await WalletKeysFile.readKeysFile( + name, + walletInfo.type, + password, + encryptionFileUtils, + ); + } + + return ArbitrumWallet( + walletInfo: walletInfo, + derivationInfo: await walletInfo.getDerivationInfo(), + password: password, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, + passphrase: keysData.passphrase, + initialBalance: balance, + client: ArbitrumClient(), + encryptionFileUtils: encryptionFileUtils, + ); + } +} diff --git a/cw_arbitrum/lib/arbitrum_wallet_service.dart b/cw_arbitrum/lib/arbitrum_wallet_service.dart new file mode 100644 index 000000000..91e0b86ee --- /dev/null +++ b/cw_arbitrum/lib/arbitrum_wallet_service.dart @@ -0,0 +1,174 @@ +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_arbitrum/arbirtrum_mnemonics_exception.dart'; +import 'package:cw_core/encryption_file_utils.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; +import 'package:cw_arbitrum/arbitrum_wallet.dart'; +import 'package:cw_arbitrum/arbitrum_client.dart'; + +class ArbitrumWalletService extends EVMChainWalletService { + ArbitrumWalletService(super.isDirect, {required this.client}); + + late ArbitrumClient client; + + @override + WalletType getType() => WalletType.arbitrum; + + @override + Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + + final wallet = ArbitrumWallet( + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + mnemonic: mnemonic, + password: credentials.password!, + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = await WalletInfo.get(name, getType()); + if (walletInfo == null) { + throw Exception('Wallet not found'); + } + + try { + final wallet = await ArbitrumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await ArbitrumWallet.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys( + EVMChainRestoreWalletFromPrivateKey credentials, { + bool? isTestnet, + }) async { + final wallet = ArbitrumWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials, + ) async { + final derivationInfo = await credentials.walletInfo!.getDerivationInfo(); + derivationInfo.derivationType = DerivationType.bip39; + derivationInfo.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"; + await derivationInfo.save(); + credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; + credentials.walletInfo!.address = credentials.hwAccountData.address; + await credentials.walletInfo!.save(); + + final wallet = ArbitrumWallet( + walletInfo: credentials.walletInfo!, + derivationInfo: derivationInfo, + password: credentials.password!, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed( + EVMChainRestoreWalletFromSeedCredentials credentials, { + bool? isTestnet, + }) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw ArbitrumMnemonicIsIncorrectException(); + } + + final wallet = ArbitrumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = await WalletInfo.get(currentName, getType()); + if (currentWalletInfo == null) { + throw Exception('Wallet not found'); + } + final currentWallet = await ArbitrumWallet.open( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + newWalletInfo.save(); + } +} diff --git a/cw_arbitrum/lib/cw_arbitrum.dart b/cw_arbitrum/lib/cw_arbitrum.dart new file mode 100644 index 000000000..298576d81 --- /dev/null +++ b/cw_arbitrum/lib/cw_arbitrum.dart @@ -0,0 +1,5 @@ +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart b/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart new file mode 100644 index 000000000..25cc5739b --- /dev/null +++ b/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart @@ -0,0 +1,71 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultArbitrumErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDC.e", + symbol: "USDC.e", + contractAddress: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + decimal: 8, + enabled: true, + ), + Erc20Token( + name: "Chainlink Token", + symbol: "LINK", + contractAddress: "0xf97f4df75117a78c1A5a0DBb814Af92458539FB4", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped liquid staked Ether 2.0", + symbol: "wstETH", + contractAddress: "0x0fBcbaEA96Ce0cF7Ee00A8c19c3ab6f5Dc8E1921", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "DAI", + symbol: "DAI", + contractAddress: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + decimal: 18, + enabled: false, + ), + ]; + + List get initialArbitrumErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + if (token.iconPath?.isEmpty ?? true) { + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + } else { + iconPath = token.iconPath; + } + + return Erc20Token.copyWith(token, icon: iconPath, tag: 'ARB'); + }).toList(); +} diff --git a/cw_arbitrum/pubspec.yaml b/cw_arbitrum/pubspec.yaml new file mode 100644 index 000000000..f6cb36577 --- /dev/null +++ b/cw_arbitrum/pubspec.yaml @@ -0,0 +1,73 @@ +name: cw_arbitrum +description: "A new Flutter package project." +version: 0.0.1 +publish_to: none +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_ethereum: + path: ../cw_ethereum + cw_evm: + path: ../cw_evm + web3dart: ^2.7.1 + hive: ^2.2.3 + bip39: ^1.0.6 + collection: ^1.17.1 + +dependency_overrides: + web3dart: + git: + url: https://github.com/cake-tech/web3dart.git + ref: cake + watcher: ^1.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.4.15 + +# 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/to/asset-from-package + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # 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/to/font-from-package diff --git a/cw_arbitrum/test/cw_arbitrum_test.dart b/cw_arbitrum/test/cw_arbitrum_test.dart new file mode 100644 index 000000000..5cf35628f --- /dev/null +++ b/cw_arbitrum/test/cw_arbitrum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_arbitrum/cw_arbitrum.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_base/lib/base_wallet.dart b/cw_base/lib/base_wallet.dart index d1287da78..f3e5878a4 100644 --- a/cw_base/lib/base_wallet.dart +++ b/cw_base/lib/base_wallet.dart @@ -45,9 +45,13 @@ class BaseWallet extends EVMChainWallet { for (final token in initialErc20Tokens) { if (!evmChainErc20TokensBox.containsKey(token.contractAddress)) { evmChainErc20TokensBox.put(token.contractAddress, token); - } else { // update existing token + } else { + // update existing token final existingToken = evmChainErc20TokensBox.get(token.contractAddress); - evmChainErc20TokensBox.put(token.contractAddress, Erc20Token.copyWith(token, enabled: existingToken!.enabled)); + evmChainErc20TokensBox.put( + token.contractAddress, + Erc20Token.copyWith(token, enabled: existingToken!.enabled), + ); } } } @@ -58,7 +62,7 @@ class BaseWallet extends EVMChainWallet { @override Future checkIfScanProviderIsEnabled() async { - return (await sharedPrefs.future).getBool("use_basescan") ?? true; + return (await sharedPrefs.future).getBool("use_basescan") ?? true; } @override diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 55938227e..4ae3aca19 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -117,6 +117,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.deps, CryptoCurrency.baseEth, CryptoCurrency.usde, + CryptoCurrency.arbEth, ]; static const havenCurrencies = [ @@ -245,6 +246,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const cbbtc = CryptoCurrency(title: 'CBBTC', tag: 'ETH', fullName: 'Coinbase Wrapped BTC', raw: 103, name: 'cbbtc', iconPath: 'assets/images/cbbtc_icon.png', decimals: 8); static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/base_icon.webp', decimals: 18); static const usde = CryptoCurrency(title: 'USDE', tag: 'BASE', fullName: 'Ethena USDE', raw: 105, name: 'usde', iconPath: 'assets/images/crypto/ethena-usde-logo.png', decimals: 18); + static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Arbitrum', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/arbitrum.webp', decimals: 18); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 5bceeb1b4..fe6899c4b 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -18,6 +18,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.eth; case WalletType.base: return CryptoCurrency.baseEth; + case WalletType.arbitrum: + return CryptoCurrency.arbEth; case WalletType.bitcoinCash: return CryptoCurrency.bch; case WalletType.nano: diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index d090acdca..cbc42adcb 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -71,6 +71,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { static const ethereumBoxName = 'EthereumErc20Tokens'; static const polygonBoxName = 'PolygonErc20Tokens'; static const baseBoxName = 'BaseErc20Tokens'; + static const arbitrumBoxName = 'ArbitrumErc20Tokens'; @override bool operator ==(other) => diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 4908641b8..a8bd39aec 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -107,6 +107,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.zano: @@ -171,6 +172,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index dfcc2c460..4aa0bd96c 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -86,6 +86,8 @@ abstract class WalletBase updateEstimatedFeesParams(TransactionPriority priority) async {} + // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, // {void Function() onFinished}); diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 01ae3725d..70a61d12f 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -20,6 +20,7 @@ const walletTypes = [ WalletType.decred, WalletType.dogecoin, WalletType.base, + WalletType.arbitrum, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -74,6 +75,9 @@ enum WalletType { @HiveField(16) base, + + @HiveField(17) + arbitrum, } int serializeToInt(WalletType type) { @@ -110,6 +114,8 @@ int serializeToInt(WalletType type) { return 14; case WalletType.base: return 15; + case WalletType.arbitrum: + return 16; case WalletType.none: return -1; } @@ -149,6 +155,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.dogecoin; case 15: return WalletType.base; + case 16: + return WalletType.arbitrum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -188,6 +196,8 @@ String walletTypeToString(WalletType type) { return 'Dogecoin'; case WalletType.base: return 'Base'; + case WalletType.arbitrum: + return 'Arbitrum'; case WalletType.none: return ''; } @@ -227,6 +237,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Dogecoin (DOGE)'; case WalletType.base: return 'Base (BASE)'; + case WalletType.arbitrum: + return 'Arbitrum (ARB)'; case WalletType.none: return ''; } @@ -266,6 +278,8 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.dogecoin; case CryptoCurrency.baseEth: return WalletType.base; + case CryptoCurrency.arbEth: + return WalletType.arbitrum; default: return null; } diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart index fbc4bad95..574c2b4f9 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/evm_chain_client.dart @@ -163,7 +163,7 @@ abstract class EVMChainClient { required BigInt gasFee, required int estimatedGasUnits, required int maxFeePerGas, - required EVMChainTransactionPriority priority, + required EVMChainTransactionPriority? priority, required CryptoCurrency currency, required String feeCurrency, required int exponent, @@ -172,15 +172,20 @@ abstract class EVMChainClient { int? gasPrice, }) async { assert(currency == CryptoCurrency.eth || - currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.maticpoly || + currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.arbEth || contractAddress != null); - bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.baseEth; + bool isNativeToken = currency == CryptoCurrency.eth || + currency == CryptoCurrency.maticpoly || + currency == CryptoCurrency.baseEth || + currency == CryptoCurrency.arbEth; final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(toAddress), - maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + maxPriorityFeePerGas: priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(), data: data != null ? hexToBytes(data) : null, maxGas: estimatedGasUnits, @@ -229,7 +234,7 @@ abstract class EVMChainClient { required String feeCurrency, required int estimatedGasUnits, required int maxFeePerGas, - required EVMChainTransactionPriority priority, + required EVMChainTransactionPriority? priority, required int exponent, required String contractAddress, int? gasPrice, @@ -237,7 +242,7 @@ abstract class EVMChainClient { final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(contractAddress), - maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + maxPriorityFeePerGas:priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, amount: EtherAmount.zero(), maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index e1058da64..75b08b0fc 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -115,11 +115,13 @@ abstract class EVMChainWalletBase late final EVMChainClient _client; - int gasPrice = 0; - int? gasBaseFee = 0; - int estimatedGasUnits = 0; + bool hasPriorityFee = true; - Timer? _updateFeesTimer; + @observable + String? nativeTxEstimatedFee; + + @observable + String? erc20TxEstimatedFee; bool _isTransactionUpdating; @@ -168,6 +170,16 @@ abstract class EVMChainWalletBase EncryptionFileUtils encryptionFileUtils, ); + String _getUSDCContractAddress() { + return switch (_client.chainId) { + 1 => "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + 137 => "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", + 8453 => "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + 42161 => "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + _ => throw Exception("Unsupported chain ID: ${_client.chainId}"), + }; + } + @override Future checkNodeHealth() async { try { @@ -175,26 +187,9 @@ abstract class EVMChainWalletBase await _client.getBalance(_evmChainPrivateKey.address, throwOnError: true); // Check USDC token balance - String usdcContractAddress; + String usdcContractAddress = _getUSDCContractAddress(); - switch (_client.chainId) { - case 1: - usdcContractAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; - break; - case 137: - usdcContractAddress = "0x2791bca1f2de4661ed88a30c99a7a9449aa84174"; - break; - case 8453: - usdcContractAddress = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"; - break; - default: - return true; - } - - await _client.fetchERC20Balances( - _evmChainPrivateKey.address, - usdcContractAddress, - ); + await _client.fetchERC20Balances(_evmChainPrivateKey.address, usdcContractAddress); return true; } catch (e) { @@ -279,30 +274,77 @@ abstract class EVMChainWalletBase } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - { - try { + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future updateEstimatedFeesParams(TransactionPriority priority) async => + await _getEstimatedFees(priority); + + Future _getEstimatedFees(TransactionPriority? priority) async { + final nativeFee = await _getNativeTxFee(priority); + nativeTxEstimatedFee = nativeFee.toString(); + + final erc20Fee = await _getErc20TxFee(priority); + erc20TxEstimatedFee = erc20Fee.toString(); + + printV('Native Estimated Fee: $nativeTxEstimatedFee'); + printV('ERC20 Estimated Fee: $erc20TxEstimatedFee'); + } + + Future _getNativeTxFee(TransactionPriority? priority) async { + try { + int priorityFee = 0; + if (hasPriorityFee) { if (priority is EVMChainTransactionPriority) { - final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - - int maxFeePerGas; - if (gasBaseFee != null) { - // MaxFeePerGas with EIP1559; - maxFeePerGas = gasBaseFee! + priorityFee; - } else { - // MaxFeePerGas with gasPrice; - maxFeePerGas = gasPrice; - printV('MaxFeePerGas with gasPrice: $maxFeePerGas'); - } - - final totalGasFee = estimatedGasUnits * maxFeePerGas; - return totalGasFee; + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); } - - return 0; - } catch (e) { - return 0; } + + final gasPrice = await _client.getGasUnitPrice(); + final gasBaseFee = await _client.getGasBaseFee(); + + final gasUnits = await _client.getEstimatedGasUnitsForTransaction( + senderAddress: evmChainPrivateKey.address, + toAddress: evmChainPrivateKey.address, + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.from(0.0000000001)), + ); + + int maxFeePerGas = gasBaseFee != null ? (gasBaseFee + priorityFee) : (gasPrice + priorityFee); + final totalGasFee = gasUnits * maxFeePerGas; + return totalGasFee; + } catch (e) { + printV(e.toString()); + return 0; + } + } + + Future _getErc20TxFee(TransactionPriority? priority) async { + try { + int priorityFee = 0; + if (hasPriorityFee) { + if (priority is EVMChainTransactionPriority) { + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + } + } + + final gasPrice = await _client.getGasUnitPrice(); + final gasBaseFee = await _client.getGasBaseFee(); + + final gasUnits = await _client.getEstimatedGasUnitsForTransaction( + senderAddress: evmChainPrivateKey.address, + toAddress: evmChainPrivateKey.address, + contractAddress: _getUSDCContractAddress(), // Using USDC for default estimation + gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), + value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.from(0.0000000001)), + ); + + int maxFeePerGas = gasBaseFee != null ? (gasBaseFee + priorityFee) : (gasPrice + priorityFee); + final totalGasFee = gasUnits * maxFeePerGas; + return totalGasFee; + } catch (e) { + printV(e.toString()); + return 0; } } @@ -316,76 +358,81 @@ abstract class EVMChainWalletBase required amount, required String? contractAddress, required String receivingAddressHex, - required TransactionPriority priority, + required TransactionPriority? priority, Uint8List? data, }) async { try { - if (priority is EVMChainTransactionPriority) { - final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - - int maxFeePerGas; - int adjustedGasPrice; - - bool isPolygon = _client.chainId == 137; - - if (gasBaseFee != null) { - // MaxFeePerGas with EIP1559; - maxFeePerGas = gasBaseFee! + priorityFee; - } else { - // MaxFeePerGas with gasPrice - maxFeePerGas = gasPrice + priorityFee; + int priorityFee = 0; + if (hasPriorityFee && priority != null) { + if (priority is EVMChainTransactionPriority) { + priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); } - - adjustedGasPrice = maxFeePerGas; - - // Polygon has a minimum priority fee of 25 gwei - if (isPolygon) { - int minPriorityFee = 25; - int minPriorityFeeWei = - EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); - - // Calculate user selected priority-based additional fee on top of minimum - int additionalPriorityFee = 0; - switch (priority) { - case EVMChainTransactionPriority.slow: - // We use minimum priority fee only - additionalPriorityFee = 0; - break; - case EVMChainTransactionPriority.medium: - // We add 15 gwei on top of minimum - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); - break; - case EVMChainTransactionPriority.fast: - // We add 35 gwei on top of minimum - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); - break; - } - - int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; - adjustedGasPrice = gasPrice + totalPriorityFee; - maxFeePerGas = gasPrice + totalPriorityFee; - } - - final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( - contractAddress: contractAddress, - senderAddress: _evmChainPrivateKey.address, - value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), - gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), - toAddress: EthereumAddress.fromHex(receivingAddressHex), - maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), - data: data, - ); - - final totalGasFee = estimatedGas * adjustedGasPrice; - - return GasParamsHandler( - estimatedGasUnits: estimatedGas, - estimatedGasFee: totalGasFee, - maxFeePerGas: maxFeePerGas, - gasPrice: adjustedGasPrice, - ); } - return GasParamsHandler.zero(); + + final gasBaseFee = await _client.getGasBaseFee(); + final gasPrice = await _client.getGasUnitPrice(); + + int maxFeePerGas; + int adjustedGasPrice; + + bool isPolygon = _client.chainId == 137; + + if (gasBaseFee != null) { + // MaxFeePerGas with EIP1559; + maxFeePerGas = gasBaseFee + priorityFee; + } else { + // MaxFeePerGas with gasPrice + maxFeePerGas = gasPrice + priorityFee; + } + + adjustedGasPrice = maxFeePerGas; + + // Polygon has a minimum priority fee of 25 gwei + if (isPolygon) { + int minPriorityFee = 25; + int minPriorityFeeWei = + EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); + + // Calculate user selected priority-based additional fee on top of minimum + int additionalPriorityFee = 0; + switch (priority) { + case EVMChainTransactionPriority.slow: + // We use minimum priority fee only + additionalPriorityFee = 0; + break; + case EVMChainTransactionPriority.medium: + // We add 15 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); + break; + case EVMChainTransactionPriority.fast: + // We add 35 gwei on top of minimum + additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); + break; + } + + int totalPriorityFee = minPriorityFeeWei + additionalPriorityFee; + adjustedGasPrice = gasPrice + totalPriorityFee; + maxFeePerGas = gasPrice + totalPriorityFee; + } + + final estimatedGas = await _client.getEstimatedGasUnitsForTransaction( + contractAddress: contractAddress, + senderAddress: _evmChainPrivateKey.address, + value: EtherAmount.fromBigInt(EtherUnit.wei, amount!), + gasPrice: EtherAmount.fromInt(EtherUnit.wei, adjustedGasPrice), + toAddress: EthereumAddress.fromHex(receivingAddressHex), + maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), + data: data, + ); + + final totalGasFee = estimatedGas * adjustedGasPrice; + + return GasParamsHandler( + estimatedGasUnits: estimatedGas, + estimatedGasFee: totalGasFee, + maxFeePerGas: maxFeePerGas, + gasPrice: adjustedGasPrice, + ); } catch (e) { return GasParamsHandler.zero(); } @@ -400,7 +447,6 @@ abstract class EVMChainWalletBase Future close({bool shouldCleanup = false}) async { _client.stop(); _transactionsUpdateTimer?.cancel(); - _updateFeesTimer?.cancel(); } @action @@ -440,30 +486,16 @@ abstract class EVMChainWalletBase await _updateBalance(); await _updateTransactions(); - await _updateEstimatedGasFeeParams(); + await _getEstimatedFees( + hasPriorityFee ? EVMChainTransactionPriority.medium : null, + ); // We're using medium priority for default estimation - _updateFeesTimer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { - await _updateEstimatedGasFeeParams(); - }); syncStatus = SyncedSyncStatus(); } catch (e) { syncStatus = FailedSyncStatus(); } } - Future _updateEstimatedGasFeeParams() async { - gasBaseFee = await _client.getGasBaseFee(); - - gasPrice = await _client.getGasUnitPrice(); - - estimatedGasUnits = await _client.getEstimatedGasUnitsForTransaction( - senderAddress: _evmChainPrivateKey.address, - toAddress: _evmChainPrivateKey.address, - gasPrice: EtherAmount.fromInt(EtherUnit.wei, gasPrice), - value: EtherAmount.fromBigInt(EtherUnit.wei, BigInt.one), - ); - } - @override Future createTransaction(Object credentials) async { final _credentials = credentials as EVMChainTransactionCredentials; @@ -511,7 +543,7 @@ abstract class EVMChainWalletBase final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -539,7 +571,7 @@ abstract class EVMChainWalletBase final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -562,7 +594,7 @@ abstract class EVMChainWalletBase final refinedGasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, receivingAddressHex: toAddress, - priority: _credentials.priority!, + priority: _credentials.priority, contractAddress: contractAddress, ); @@ -577,7 +609,7 @@ abstract class EVMChainWalletBase totalAmount = (currencyBalance.balance - estimatedFeesForTransaction); } - // check the fees on the base currency (Eth/Polygon) + // check the fees on the base currency if (estimatedFeesForTransaction > balance[currency]!.balance) { throw EVMChainTransactionFeesException(currency.title); } @@ -599,9 +631,9 @@ abstract class EVMChainWalletBase toAddress: toAddress, amount: totalAmount, gasFee: estimatedFeesForTransaction, - priority: _credentials.priority!, + priority: _credentials.priority, currency: transactionCurrency, - feeCurrency: switch (_client.chainId) { 1 => "ETH", 137 => "POL", 8453 => "ETH", _ => "ETH" }, + feeCurrency: switch (_client.chainId) { 137 => "POL", _ => "ETH" }, maxFeePerGas: maxFeePerGasForTransaction, exponent: exponent, contractAddress: @@ -614,12 +646,11 @@ abstract class EVMChainWalletBase } Future createCallDataTransaction( - String to, - String dataHex, - BigInt valueWei, - EVMChainTransactionPriority priority, - ) async { - + String to, + String dataHex, + BigInt valueWei, + EVMChainTransactionPriority? priority, + ) async { // Estimate gas with the SAME call (sender, to, value, data) final gas = await calculateActualEstimatedFeeForCreateTransaction( amount: valueWei, // native value (usually 0 for ERC20 transfer) @@ -632,7 +663,8 @@ abstract class EVMChainWalletBase final nativeCurrency = switch (_client.chainId) { 137 => CryptoCurrency.maticpoly, 8453 => CryptoCurrency.baseEth, - _ => CryptoCurrency.eth, + 42161 => CryptoCurrency.arbEth, + _ => CryptoCurrency.eth, }; // Fallback for nodes that fail estimate (non-zero) @@ -656,9 +688,13 @@ abstract class EVMChainWalletBase ); } - - Future createApprovalTransaction(BigInt amount, String spender, - CryptoCurrency token, EVMChainTransactionPriority priority, String feeCurrency) async { + Future createApprovalTransaction( + BigInt amount, + String spender, + CryptoCurrency token, + EVMChainTransactionPriority? priority, + String feeCurrency, + ) async { final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == token.title); assert(transactionCurrency is Erc20Token); @@ -840,10 +876,7 @@ abstract class EVMChainWalletBase } Future isApprovalRequired( - String tokenContract, - String spender, - BigInt requiredAmount) async { - + String tokenContract, String spender, BigInt requiredAmount) async { const zero = '0x0000000000000000000000000000000000000000'; const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; @@ -859,9 +892,7 @@ abstract class EVMChainWalletBase chainId: _client.chainId, ); - final allowance = await erc20.allowance( - owner, - EthereumAddress.fromHex(spender)); + final allowance = await erc20.allowance(owner, EthereumAddress.fromHex(spender)); return allowance < requiredAmount; } catch (e) { @@ -898,7 +929,6 @@ abstract class EVMChainWalletBase @override Future updateTransactionsHistory() async => await _updateTransactions(); - List get erc20Currencies => evmChainErc20TokensBox.values.toList(); Future addErc20Token(Erc20Token token) async { @@ -998,6 +1028,8 @@ abstract class EVMChainWalletBase /// PolygonScan for Polygon. /// /// BaseScan for Base. + /// + /// ArbiScan for Arbitrum. void updateScanProviderUsageState(bool isEnabled) { if (isEnabled) { _updateTransactions(); diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index 53baf5dbd..333a6599d 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -356,6 +356,8 @@ class CommonTestFlows { return secrets.solanaTestWalletSeeds; case WalletType.base: return secrets.baseTestWalletSeeds; + case WalletType.arbitrum: + return secrets.arbitrumTestWalletSeeds; case WalletType.tron: return secrets.tronTestWalletSeeds; case WalletType.nano: @@ -392,6 +394,8 @@ class CommonTestFlows { return secrets.polygonTestWalletReceiveAddress; case WalletType.base: return secrets.baseTestWalletReceiveAddress; + case WalletType.arbitrum: + return secrets.arbitrumTestWalletReceiveAddress; case WalletType.solana: return secrets.solanaTestWalletReceiveAddress; case WalletType.tron: diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 3b11e216b..59e3bb924 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -322,6 +322,26 @@ base-wallet + + CFBundleTypeRole + Editor + CFBundleURLName + arbitrum + CFBundleURLSchemes + + arbitrum + + + + CFBundleTypeRole + Viewer + CFBundleURLName + arbitrum-wallet + CFBundleURLSchemes + + arbitrum-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/arbitrum/cw_arbitrum.dart b/lib/arbitrum/cw_arbitrum.dart new file mode 100644 index 000000000..e4e5b6d64 --- /dev/null +++ b/lib/arbitrum/cw_arbitrum.dart @@ -0,0 +1,244 @@ +part of 'arbitrum.dart'; + +class CWArbitrum extends Arbitrum { + @override + List getArbitrumWordList(String language) => EVMChainMnemonics.englishWordlist; + + WalletService createArbitrumWalletService(bool isDirect) => + ArbitrumWalletService(isDirect, client: ArbitrumClient()); + + @override + WalletCredentials createArbitrumNewWalletCredentials({ + required String name, + String? mnemonic, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }) => + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + + @override + WalletCredentials createArbitrumRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + String? passphrase, + }) => + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + + @override + WalletCredentials createArbitrumRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + WalletCredentials createArbitrumHardwareWalletCredentials({ + required String name, + required HardwareAccountData hwAccountData, + WalletInfo? walletInfo, + }) => + EVMChainRestoreWalletFromHardware( + name: name, + hwAccountData: hwAccountData, + walletInfo: walletInfo, + ); + + @override + String getAddress(WalletBase wallet) => (wallet as ArbitrumWallet).walletAddresses.address; + + @override + String getPrivateKey(WalletBase wallet) { + final privateKeyHolder = (wallet as ArbitrumWallet).evmChainPrivateKey; + if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); + return ""; + } + + @override + String getPublicKey(WalletBase wallet) { + final privateKeyInUnitInt = (wallet as ArbitrumWallet).evmChainPrivateKey; + return privateKeyInUnitInt.address.hex; + } + + Object createArbitrumTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + int? feeRate, + }) => + EVMChainTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + priority: null, + currency: currency, + feeRate: feeRate, + ); + + Object createArbitrumTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + required int feeRate, + }) => + EVMChainTransactionCredentials( + outputs, + priority: null, + currency: currency, + feeRate: feeRate, + ); + + @override + int formatterArbitrumParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); + + @override + double formatterArbitrumAmountToDouble({ + TransactionInfo? transaction, + BigInt? amount, + int exponent = 18, + }) { + assert(transaction != null || amount != null); + + if (transaction != null) { + transaction as EVMChainTransactionInfo; + return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); + } else { + return (amount!) / BigInt.from(10).pow(exponent); + } + } + + @override + List getERC20Currencies(WalletBase wallet) => + (wallet as ArbitrumWallet).erc20Currencies; + + @override + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).addErc20Token(token as Erc20Token); + + @override + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).deleteErc20Token(token as Erc20Token); + + @override + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => + (wallet as ArbitrumWallet).removeTokenTransactionsInHistory(token as Erc20Token); + + @override + Future getErc20Token(WalletBase wallet, String contractAddress) => + (wallet as ArbitrumWallet).getErc20Token(contractAddress, 'arbitrum'); + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as EVMChainTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.arbEth.title || + transaction.tokenSymbol == "ARB") { + return CryptoCurrency.arbEth; + } + + wallet as ArbitrumWallet; + + return wallet.erc20Currencies.firstWhere( + (element) => + transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), + ); + } + + @override + void updateArbitrumScanUsageState(WalletBase wallet, bool isEnabled) => + (wallet as ArbitrumWallet).updateScanProviderUsageState(isEnabled); + + @override + Web3Client? getWeb3Client(WalletBase wallet) => (wallet as ArbitrumWallet).getWeb3Client(); + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; + + @override + Future createTokenApproval( + WalletBase wallet, + BigInt amount, + String spender, + CryptoCurrency token, + ) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, + spender, + token, + null, + "ARB", + ); + + @override + Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { + if (service is EVMChainLedgerService) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmLedgerCredentials).setLedgerConnection( + service.ledgerConnection, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + } else if (service is EVMChainBitboxService) { + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) + .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + } + return Future.value(); + } + + @override + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => + EVMChainLedgerService(connection); + + @override + HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) => + EVMChainBitboxService(manager, chainId: 42161); + + @override + List getDefaultTokenContractAddresses() => DefaultArbitrumErc20Tokens() + .initialArbitrumErc20Tokens + .map((e) => e.contractAddress) + .toList(); + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final arbitrumWallet = wallet as ArbitrumWallet; + return arbitrumWallet.erc20Currencies.any( + (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), + ); + } + + @override + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => + (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); + + @override + Future createRawCallDataTransaction( + WalletBase wallet, String to, String dataHex, BigInt valueWei) => + (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, null); + + @override + String? getArbitrumNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getArbitrumERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; +} diff --git a/lib/base/cw_base.dart b/lib/base/cw_base.dart index 42a9ad03c..5b17abd02 100644 --- a/lib/base/cw_base.dart +++ b/lib/base/cw_base.dart @@ -190,7 +190,8 @@ class CWBase extends Base { String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; @override - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); @override @@ -210,10 +211,10 @@ class CWBase extends Base { ); @override - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); - + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); @override Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { @@ -246,4 +247,12 @@ class CWBase extends Base { (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), ); } + + @override + String? getBaseNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getBaseERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index c590d82b3..5f4c66854 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -136,6 +136,7 @@ class DFXBuyProvider extends BuyProvider { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return wallet.signMessage(message); diff --git a/lib/buy/kryptonim/kryptonim.dart b/lib/buy/kryptonim/kryptonim.dart index cbe684686..4bedd9485 100644 --- a/lib/buy/kryptonim/kryptonim.dart +++ b/lib/buy/kryptonim/kryptonim.dart @@ -219,6 +219,7 @@ class KryptonimBuyProvider extends BuyProvider { String? blockchain = switch (cur.tag) { 'ETH' => 'Ethereum', 'BASE' => 'Base', + 'ARB' => 'Arbitrum', 'POL' => 'Polygon', 'AVAXC' => 'Avalanche', 'SOL' => 'Solana', @@ -231,6 +232,7 @@ class KryptonimBuyProvider extends BuyProvider { CryptoCurrency.ltc => 'Litecoin', CryptoCurrency.eth => 'Ethereum', CryptoCurrency.baseEth => 'Base', + CryptoCurrency.arbEth => 'Arbitrum', CryptoCurrency.maticpoly => 'Matic', _ => null, }; diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 615d510bc..8d809a74f 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -324,6 +324,8 @@ class OnRamperBuyProvider extends BuyProvider { switch (tag) { case 'POL': return 'POLYGON'; + case 'ARB': + return 'ARBITRUM'; case 'ETH': return 'ETHEREUM'; case 'TRX': diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index ee529d3db..57ce1ad87 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -106,6 +106,7 @@ class RobinhoodBuyProvider extends BuyProvider { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/lib/core/open_crypto_pay/open_cryptopay_service.dart b/lib/core/open_crypto_pay/open_cryptopay_service.dart index d913a619c..cc94740f0 100644 --- a/lib/core/open_crypto_pay/open_cryptopay_service.dart +++ b/lib/core/open_crypto_pay/open_cryptopay_service.dart @@ -162,6 +162,8 @@ class OpenCryptoPayService { return "Ethereum"; case "POL": return "Polygon"; + case "ARB": + return "Arbitrum"; case "TRX": return "Tron"; case "SOL": diff --git a/lib/core/payment_uris.dart b/lib/core/payment_uris.dart index be4e79007..38f4f6d2e 100644 --- a/lib/core/payment_uris.dart +++ b/lib/core/payment_uris.dart @@ -101,6 +101,21 @@ class BaseURI extends PaymentURI { } } +class ArbitrumURI extends PaymentURI { + ArbitrumURI({required super.amount, required super.address}); + + @override + String toString() { + var base = 'arbitrum:$address'; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + class BitcoinCashURI extends PaymentURI { BitcoinCashURI({required super.amount, required super.address}); diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 2643fd0d7..e7facde3b 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/validator.dart'; @@ -55,6 +56,8 @@ class SeedValidator extends Validator { return decred!.getDecredWordList(); case WalletType.base: return base!.getBaseWordList(language); + case WalletType.arbitrum: + return arbitrum!.getArbitrumWordList(language); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index dec1c8022..7f09e7bfb 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -79,6 +79,7 @@ class WalletCreationService { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: diff --git a/lib/di.dart b/lib/di.dart index 1d4a0ac41..fde64bd72 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/anypay/anypay_api.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -1226,6 +1227,8 @@ Future setup({ return decred!.createDecredWalletService(_unspentCoinsInfoSource); case WalletType.base: return base!.createBaseWalletService(SettingsStoreBase.walletPasswordDirectInput); + case WalletType.arbitrum: + return arbitrum!.createArbitrumWalletService(SettingsStoreBase.walletPasswordDirectInput); case WalletType.haven: return HavenWalletService(); case WalletType.none: diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 83671474d..9f2ac0bce 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -50,6 +50,7 @@ const moneroWorldNodeUri = '.moneroworld.com'; const decredDefaultUri = "default-spv-nodes"; const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022'; const baseDefaultNodeUri = 'base.nownodes.io'; +const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io'; Future defaultSettingsMigration( {required int version, @@ -545,6 +546,16 @@ Future defaultSettingsMigration( currentNodePreferenceKey: PreferencesKey.currentBaseNodeIdKey, ); break; + case 53: + await addWalletNodeList(nodes: nodes, type: WalletType.arbitrum); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.arbitrum, + currentNodePreferenceKey: PreferencesKey.currentArbitrumNodeIdKey, + ); + break; + default: break; } @@ -655,6 +666,8 @@ String _getDefaultNodeUri(WalletType type) { return dogecoinDefaultNodeUri; case WalletType.base: return baseDefaultNodeUri; + case WalletType.arbitrum: + return arbitrumDefaultNodeUri; case WalletType.banano: case WalletType.none: return ''; @@ -1057,6 +1070,7 @@ Future checkCurrentNodes( final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentBaseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final currentArbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); @@ -1082,6 +1096,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentBaseNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentBaseNodeId); + final currentArbitrumNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentArbitrumNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); final currentDecredNodeServer = @@ -1179,6 +1195,12 @@ Future checkCurrentNodes( await sharedPreferences.setInt(PreferencesKey.currentBaseNodeIdKey, node.key as int); } + if (currentArbitrumNodeServer == null) { + final node = Node(uri: arbitrumDefaultNodeUri, type: WalletType.arbitrum); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentArbitrumNodeIdKey, node.key as int); + } + if (currentSolanaNodeServer == null) { final node = Node(uri: solanaDefaultNodeUri, type: WalletType.solana); await nodeSource.add(node); diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index b49afc0b3..35ed46995 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -26,6 +27,10 @@ class EnsRecord { _client = base!.getWeb3Client(wallet); } + if (wallet != null && wallet.type == WalletType.arbitrum) { + _client = arbitrum!.getWeb3Client(wallet); + } + if (_client == null) { late final client = ProxyWrapper().getHttpIOClient(); @@ -48,6 +53,7 @@ class EnsRecord { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: default: return (await ens.withName(name).getAddress()).hex; } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 90bb9ddcb..4f856db7d 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -52,6 +52,9 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.base: path = 'assets/base_node_list.yml'; break; + case WalletType.arbitrum: + path = 'assets/arbitrum_node_list.yml'; + break; case WalletType.banano: case WalletType.none: path = ''; @@ -104,6 +107,7 @@ Future resetToDefault(Box nodeSource) async { final zanoNodes = await loadDefaultNodes(WalletType.zano); final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin); final baseNodes = await loadDefaultNodes(WalletType.base); + final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -118,7 +122,8 @@ Future resetToDefault(Box nodeSource) async { zanoNodes + decredNodes + dogecoinElectrumServerList + - baseNodes; + baseNodes + + arbitrumNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index cbcf62dad..442810875 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -115,6 +115,7 @@ class AddressResolver { "pog", "polygon", "base", + "arbitrum" "press", "privacy", "pro", diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index eb678a07a..a9acc2a6b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -9,6 +9,7 @@ class PreferencesKey { static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentBaseNodeIdKey = 'current_node_id_base'; + static const currentArbitrumNodeIdKey = 'current_node_id_arbitrum'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; static const currentDecredNodeIdKey = 'current_node_id_decred'; @@ -80,6 +81,7 @@ class PreferencesKey { static const useEtherscan = 'use_etherscan'; static const usePolygonScan = 'use_polygonscan'; static const useBaseScan = 'use_base_scan'; + static const useArbiScan = 'use_arbitrum_scan'; static const useTronGrid = 'use_trongrid'; static const useMempoolFeeAPI = 'use_mempool_fee_api'; static const defaultNanoRep = 'default_nano_representative'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 37ad2ab7e..4538da8e6 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -31,7 +32,7 @@ List priorityForWalletType(WalletType type) { return polygon!.getTransactionPriorities(); case WalletType.base: return base!.getTransactionPriorities(); - // no such thing for nano/banano/solana/tron: + case WalletType.arbitrum: case WalletType.nano: case WalletType.banano: case WalletType.solana: diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index 4433a5494..26bb8cd19 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -189,8 +189,8 @@ class CWEthereum extends Ethereum { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials) - .setTrezorConnect(service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( + service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } } @@ -211,15 +211,16 @@ class CWEthereum extends Ethereum { return DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); } - @override bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { final ethereumWallet = wallet as EthereumWallet; - return ethereumWallet.erc20Currencies.any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + return ethereumWallet.erc20Currencies + .any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); } @override - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); @override @@ -229,9 +230,10 @@ class CWEthereum extends Ethereum { amount, spender, token, priority as EVMChainTransactionPriority, "ETH"); @override - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); // Integrations @override @@ -270,4 +272,12 @@ class CWEthereum extends Ethereum { @override Future enableDEuroSaving(WalletBase wallet, TransactionPriority priority) => DEuro(wallet as EthereumWallet).enableSavings(priority as EVMChainTransactionPriority); + + @override + String? getEthereumNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getEthereumERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 763064ec1..629597b34 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -466,6 +466,8 @@ class SideShiftExchangeProvider extends ExchangeProvider { return 'lightning'; case 'POL': return 'polygon'; + case 'ARB': + return 'arbitrum'; case 'ZEC': return 'zcash'; case 'AVAXC': @@ -481,6 +483,7 @@ class SideShiftExchangeProvider extends ExchangeProvider { 'tron' => 'TRX', 'lightning' => 'LN', 'polygon' => 'POL', + 'arbitrum' => 'ARB', 'zcash' => 'ZEC', 'avax' => 'AVAXC', _ => network, diff --git a/lib/main.dart b/lib/main.dart index a5b9be867..fe6e9477a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -260,7 +260,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 52, + initialMigrationVersion: 53, ); } diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 8c8c2a78d..94396f83c 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -14,13 +14,14 @@ class CWPolygon extends Polygon { WalletInfo? walletInfo, String? password, String? passphrase, - }) => EVMChainNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); + }) => + EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ @@ -28,30 +29,33 @@ class CWPolygon extends Polygon { required String mnemonic, required String password, String? passphrase, - }) => EVMChainRestoreWalletFromSeedCredentials( - name: name, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); + }) => + EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ required String name, required String privateKey, required String password, - }) => EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + }) => + EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); @override WalletCredentials createPolygonHardwareWalletCredentials({ required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo, - }) => EVMChainRestoreWalletFromHardware( - name: name, - hwAccountData: hwAccountData, - walletInfo: walletInfo, - ); + }) => + EVMChainRestoreWalletFromHardware( + name: name, + hwAccountData: hwAccountData, + walletInfo: walletInfo, + ); @override String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; @@ -87,37 +91,39 @@ class CWPolygon extends Polygon { required TransactionPriority priority, required CryptoCurrency currency, int? feeRate, - }) => EVMChainTransactionCredentials( - outputs - .map( - (out) => OutputInfo( - fiatAmount: out.fiatAmount, - cryptoAmount: out.cryptoAmount, - address: out.address, - note: out.note, - sendAll: out.sendAll, - extractedAddress: out.extractedAddress, - isParsedAddress: out.isParsedAddress, - formattedCryptoAmount: out.formattedCryptoAmount, - ), - ) - .toList(), - priority: priority as EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); + }) => + EVMChainTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + priority: priority as EVMChainTransactionPriority, + currency: currency, + feeRate: feeRate, + ); Object createPolygonTransactionCredentialsRaw( List outputs, { TransactionPriority? priority, required CryptoCurrency currency, required int feeRate, - }) => EVMChainTransactionCredentials( - outputs, - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - ); + }) => + EVMChainTransactionCredentials( + outputs, + priority: priority as EVMChainTransactionPriority?, + currency: currency, + feeRate: feeRate, + ); @override int formatterPolygonParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); @@ -169,7 +175,8 @@ class CWPolygon extends Polygon { wallet as PolygonWallet; return wallet.erc20Currencies.firstWhere( - (element) => transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), + (element) => + transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), ); } @@ -183,12 +190,14 @@ class CWPolygon extends Polygon { @override String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; - Future isApprovalRequired(WalletBase wallet, String tokenContract,String spender, BigInt requiredAmount) => + Future isApprovalRequired( + WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => (wallet as PolygonWallet).isApprovalRequired(tokenContract, spender, requiredAmount); - Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei, - TransactionPriority priority) => - (wallet as EVMChainWallet).createCallDataTransaction(to, dataHex, valueWei, priority as EVMChainTransactionPriority); + Future createRawCallDataTransaction(WalletBase wallet, String to, + String dataHex, BigInt valueWei, TransactionPriority priority) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, dataHex, valueWei, priority as EVMChainTransactionPriority); @override Future createTokenApproval( @@ -197,13 +206,14 @@ class CWPolygon extends Polygon { String spender, CryptoCurrency token, TransactionPriority priority, - ) => (wallet as EVMChainWallet).createApprovalTransaction( - amount, - spender, - token, - priority as EVMChainTransactionPriority, - "POL", - ); + ) => + (wallet as EVMChainWallet).createApprovalTransaction( + amount, + spender, + token, + priority as EVMChainTransactionPriority, + "POL", + ); @override Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service) async { @@ -214,8 +224,8 @@ class CWPolygon extends Polygon { ((wallet as EVMChainWallet).evmChainPrivateKey as EvmBitboxCredentials) .setBitbox(service.manager, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials) - .setTrezorConnect(service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); + ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( + service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); } } @@ -242,4 +252,12 @@ class CWPolygon extends Polygon { (element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase(), ); } + + @override + String? getPolygonNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getPolygonERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index abd8b097e..a821254c7 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -49,6 +50,10 @@ Future startFiatRateUpdate( currencies = base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.arbitrum) { + currencies = + arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } if (appStore.wallet!.type == WalletType.solana) { currencies = diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 53da945c1..d6c872c05 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/di.dart'; @@ -133,6 +134,10 @@ void startCurrentWalletChangeReaction( currencies = base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } + if (wallet.type == WalletType.arbitrum) { + currencies = + arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } if (wallet.type == WalletType.solana) { currencies = solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 7e969f344..0357b12c8 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -9,6 +9,7 @@ bool isEVMCompatibleChain(WalletType walletType) { case WalletType.polygon: case WalletType.ethereum: case WalletType.base: + case WalletType.arbitrum: return true; default: return false; @@ -21,6 +22,7 @@ bool isNFTACtivatedChain(WalletType walletType) { case WalletType.ethereum: case WalletType.base: case WalletType.solana: + case WalletType.arbitrum: return true; default: return false; @@ -33,6 +35,7 @@ bool isWalletConnectCompatibleChain(WalletType walletType) { case WalletType.ethereum: case WalletType.base: case WalletType.solana: + case WalletType.arbitrum: return true; default: return false; @@ -47,6 +50,8 @@ String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) { return EVMChainId.polygon.chain(); case WalletType.base: return EVMChainId.base.chain(); + case WalletType.arbitrum: + return EVMChainId.arbitrum.chain(); case WalletType.solana: return SolanaChainId.mainnet.chain(); default: @@ -59,6 +64,7 @@ List getChainSupportedMethodsOnWalletType(WalletType walletType) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: return EVMSupportedMethods.values.map((e) => e.name).toList(); case WalletType.solana: return SolanaSupportedMethods.values.map((e) => e.name).toList(); @@ -73,6 +79,8 @@ int getChainIdBasedOnWalletType(WalletType walletType) { return 137; case WalletType.base: return 8453; + case WalletType.arbitrum: + return 42161; // For now, we return eth chain Id as the default, we'll modify as we add more wallets case WalletType.ethereum: default: @@ -88,6 +96,8 @@ String getChainNameBasedOnWalletType(WalletType walletType) { return 'polygon'; case WalletType.base: return 'base'; + case WalletType.arbitrum: + return 'arbitrum'; case WalletType.solana: return 'mainnet'; default: @@ -103,6 +113,8 @@ String getTokenNameBasedOnWalletType(WalletType walletType) { return 'MATIC'; case WalletType.base: return 'BASE'; + case WalletType.arbitrum: + return 'ARB'; case WalletType.solana: return 'SOL'; default: diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 8250dfd5b..2b48a49ea 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -5,6 +5,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.bitcoin: @@ -43,6 +44,7 @@ bool hasTokens(WalletType walletType) { case WalletType.tron: case WalletType.zano: case WalletType.base: + case WalletType.arbitrum: return true; default: return false; diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index b77346a30..655abb5a6 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -39,6 +39,7 @@ class _DesktopWalletSelectionDropDownState extends State { this.bitcoinCashIcon = Image.asset('assets/images/crypto/bitcoin-cash.webp'), this.polygonIcon = Image.asset('assets/images/crypto/polygon.webp'), this.baseIcon = Image.asset('assets/images/crypto/base_icon.webp'), + this.arbitrumIcon = Image.asset('assets/images/crypto/arbitrum.webp'), this.solanaIcon = Image.asset('assets/images/crypto/solana.webp'), this.tronIcon = Image.asset('assets/images/crypto/tron.webp'), this.wowneroIcon = Image.asset('assets/images/crypto/wownero.webp'), @@ -62,6 +63,7 @@ class MenuWidgetState extends State { Image bananoIcon; Image polygonIcon; Image baseIcon; + Image arbitrumIcon; Image solanaIcon; Image tronIcon; Image wowneroIcon; @@ -251,6 +253,8 @@ class MenuWidgetState extends State { return solanaIcon; case WalletType.base: return baseIcon; + case WalletType.arbitrum: + return arbitrumIcon; case WalletType.tron: return tronIcon; case WalletType.wownero: diff --git a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart index dabca54ed..85be26c92 100644 --- a/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart +++ b/lib/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart @@ -60,7 +60,7 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { FeeSelectionWidget( feesViewModel: feesViewModel, output: output, - onTap: () => pickTransactionPriority(context), + onTap: () => pickTransactionPriority(context, output), ), if (exchangeTradeViewModel.sendViewModel.hasCoinControl) CoinControlWidget( @@ -75,7 +75,7 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { ); } - Future pickTransactionPriority(BuildContext context) async { + Future pickTransactionPriority(BuildContext context, Output output) async { final items = priorityForWalletType(feesViewModel.walletType); final selectedItem = items.indexOf(feesViewModel.transactionPriority); final customItemIndex = feesViewModel.getCustomPriorityIndex(items); @@ -102,10 +102,10 @@ class ExchangeTradeCardItemWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, sliderValue: customFeeRate, onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue), - onItemSelected: (TransactionPriority priority) { + onItemSelected: (TransactionPriority priority) async { feesViewModel.setTransactionPriority(priority); setState(() => selectedIdx = items.indexOf(priority)); - + await output.calculateEstimatedFee(); if (feesViewModel.isLowFee) { _showFeeAlert(context); } diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index fed3ebfd2..0b960d31f 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -542,7 +542,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin GestureDetector( key: ValueKey('send_page_select_fee_priority_button_key'), onTap: sendViewModel.feesViewModel.hasFeesPriority - ? () => pickTransactionPriority(context) + ? () => pickTransactionPriority(context, output) : () {}, child: Container( padding: EdgeInsets.only(top: 24), @@ -799,9 +799,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin pickTransactionPriority(BuildContext context) async { + Future pickTransactionPriority(BuildContext context, Output output) async { final items = priorityForWalletType(sendViewModel.walletType); final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority); final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items); @@ -867,9 +865,10 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin setState(() => customFeeRate = newValue), - onItemSelected: (TransactionPriority priority) { + onItemSelected: (TransactionPriority priority) async { sendViewModel.feesViewModel.setTransactionPriority(priority); setState(() => selectedIdx = items.indexOf(priority)); + await output.calculateEstimatedFee(); }, ); }, diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index e753efc13..fa0a05b30 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -126,6 +126,14 @@ class PrivacyPage extends BasePage { _privacySettingsViewModel.setUseBaseScan(value); }, ), + if (_privacySettingsViewModel.canUseArbiScan) + SettingsSwitcherCell( + title: S.current.arbiscan_history, + value: _privacySettingsViewModel.useArbiScan, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setUseArbiScan(value); + }, + ), if (_privacySettingsViewModel.canUseTronGrid) SettingsSwitcherCell( title: S.current.trongrid_history, diff --git a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart index 7e489288e..8672da0f5 100644 --- a/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart +++ b/lib/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; @@ -26,6 +27,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return polygon!.getPrivateKey(wallet); case WalletType.base: return base!.getPrivateKey(wallet); + case WalletType.arbitrum: + return arbitrum!.getPrivateKey(wallet); case WalletType.solana: return solana!.getPrivateKey(wallet); default: @@ -41,6 +44,8 @@ class KeyServiceImpl implements WalletConnectKeyService { return polygon!.getPublicKey(wallet); case WalletType.base: return base!.getPublicKey(wallet); + case WalletType.arbitrum: + return arbitrum!.getPublicKey(wallet); case WalletType.solana: return solana!.getPublicKey(wallet); default: diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6b3dbc108..a825ee243 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -110,6 +110,7 @@ abstract class SettingsStoreBase with Store { required this.pinNativeTokenAtTop, required this.useEtherscan, required this.useBaseScan, + required this.useArbiScan, required this.usePolygonScan, required this.useTronGrid, required this.useMempoolFeeAPI, @@ -467,6 +468,11 @@ abstract class SettingsStoreBase with Store { (_) => useBaseScan, (bool useBaseScan) => _sharedPreferences.setBool(PreferencesKey.useBaseScan, useBaseScan)); + + reaction( + (_) => useArbiScan, + (bool useArbiScan) => + _sharedPreferences.setBool(PreferencesKey.useArbiScan, useArbiScan)); reaction((_) => useTronGrid, (bool useTronGrid) => _sharedPreferences.setBool(PreferencesKey.useTronGrid, useTronGrid)); @@ -826,6 +832,9 @@ abstract class SettingsStoreBase with Store { @observable bool useBaseScan; + @observable + bool useArbiScan; + @observable bool useTronGrid; @@ -1067,6 +1076,7 @@ abstract class SettingsStoreBase with Store { final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true; + final useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true; final useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; final useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1113,6 +1123,7 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1135,6 +1146,8 @@ abstract class SettingsStoreBase with Store { nodeSource.values.firstWhereOrNull((e) => e.uriRaw == polygonDefaultNodeUri); final baseNode = nodeSource.get(baseNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == baseDefaultNodeUri); + final arbitrumNode = nodeSource.get(arbitrumNodeId) ?? + nodeSource.values.firstWhereOrNull((e) => e.uriRaw == arbitrumDefaultNodeUri); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == cakeWalletBitcoinCashDefaultNodeUri); final nanoNode = nodeSource.get(nanoNodeId) ?? @@ -1212,6 +1225,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.base] = baseNode; } + if (arbitrumNode != null) { + nodes[WalletType.arbitrum] = arbitrumNode; + } + if (bitcoinCashElectrumServer != null) { nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; } @@ -1391,6 +1408,7 @@ abstract class SettingsStoreBase with Store { useEtherscan: useEtherscan, usePolygonScan: usePolygonScan, useBaseScan: useBaseScan, + useArbiScan: useArbiScan, useTronGrid: useTronGrid, useMempoolFeeAPI: useMempoolFeeAPI, defaultNanoRep: defaultNanoRep, @@ -1588,6 +1606,7 @@ abstract class SettingsStoreBase with Store { useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; useBaseScan = sharedPreferences.getBool(PreferencesKey.useBaseScan) ?? true; + useArbiScan = sharedPreferences.getBool(PreferencesKey.useArbiScan) ?? true; useTronGrid = sharedPreferences.getBool(PreferencesKey.useTronGrid) ?? true; useMempoolFeeAPI = sharedPreferences.getBool(PreferencesKey.useMempoolFeeAPI) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -1619,6 +1638,7 @@ abstract class SettingsStoreBase with Store { final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final baseNodeId = sharedPreferences.getInt(PreferencesKey.currentBaseNodeIdKey); + final arbitrumNodeId = sharedPreferences.getInt(PreferencesKey.currentArbitrumNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); @@ -1633,6 +1653,7 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final baseNode = nodeSource.get(baseNodeId); + final arbitrumNode = nodeSource.get(arbitrumNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); @@ -1670,6 +1691,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.base] = baseNode; } + if (arbitrumNode != null) { + nodes[WalletType.arbitrum] = arbitrumNode; + } + if (bitcoinCashNode != null) { nodes[WalletType.bitcoinCash] = bitcoinCashNode; } @@ -1833,6 +1858,9 @@ abstract class SettingsStoreBase with Store { case WalletType.base: await _sharedPreferences.setInt(PreferencesKey.currentBaseNodeIdKey, node.key as int); break; + case WalletType.arbitrum: + await _sharedPreferences.setInt(PreferencesKey.currentArbitrumNodeIdKey, node.key as int); + break; case WalletType.solana: await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); break; diff --git a/lib/utils/qr_util.dart b/lib/utils/qr_util.dart index 7e7aec066..3a9981bd6 100644 --- a/lib/utils/qr_util.dart +++ b/lib/utils/qr_util.dart @@ -30,6 +30,8 @@ String getQrImage(WalletType type) { return 'assets/images/doge_chain_qr.svg'; case WalletType.base: return 'assets/images/base_chain_QR.svg'; + case WalletType.arbitrum: + return 'assets/images/arbitrum_chain_QR.svg'; case WalletType.banano: case WalletType.haven: case WalletType.none: diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index 024c16853..184eb56c1 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -116,6 +116,7 @@ class TokenUtilities { WalletType.ethereum => '${walletKey}_${Erc20Token.ethereumBoxName}', WalletType.polygon => '${walletKey}_${Erc20Token.polygonBoxName}', WalletType.base => '${walletKey}_${Erc20Token.baseBoxName}', + WalletType.arbitrum => '${walletKey}_${Erc20Token.arbitrumBoxName}', _ => '${walletKey}_${Erc20Token.ethereumBoxName}', }; @@ -193,7 +194,7 @@ class TokenUtilities { } // Arbitrum One - if (title == 'arbitrum' || title == 'arb' || tag == 'arbitrum') { + if (title == 'arbitrum' || title == 'arb' || tag == 'arb') { return 42161; } diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index efc2270a7..429bdf0e1 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -40,6 +40,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.dogecoin: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return true; @@ -81,6 +82,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.ethereum, WalletType.polygon, WalletType.base, + WalletType.arbitrum, WalletType.tron, WalletType.solana, WalletType.monero, diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 78701b44c..552d318e4 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -167,6 +167,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: return S.current.xmr_full_balance; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 25a571f00..50e8d62b7 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -879,6 +879,7 @@ abstract class DashboardViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.nano: case WalletType.banano: diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index c54d8b1b3..46c0248b4 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -111,6 +112,17 @@ abstract class HomeSettingsViewModelBase with Store { ); await base!.addErc20Token(_balanceViewModel.wallet, baseToken); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + final arbitrumToken = Erc20Token( + name: token.name, + symbol: token.title, + decimal: token.decimals, + contractAddress: contractAddress.toLowerCase(), + iconPath: token.iconPath, + isPotentialScam: token.isPotentialScam, + ); + await arbitrum!.addErc20Token(_balanceViewModel.wallet, arbitrumToken); + } if (_balanceViewModel.wallet.type == WalletType.solana) { final splToken = token.copyWith(enabled: true); @@ -153,6 +165,10 @@ abstract class HomeSettingsViewModelBase with Store { return base!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return arbitrum!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return solana!.isTokenAlreadyAdded(_balanceViewModel.wallet, contractAddress); } @@ -184,6 +200,10 @@ abstract class HomeSettingsViewModelBase with Store { await base!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + await arbitrum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { await solana!.deleteSPLToken(_balanceViewModel.wallet, token); } @@ -240,6 +260,8 @@ abstract class HomeSettingsViewModelBase with Store { break; case WalletType.base: defaultTokenAddresses = base!.getDefaultTokenContractAddresses(); + case WalletType.arbitrum: + defaultTokenAddresses = arbitrum!.getDefaultTokenContractAddresses(); break; case WalletType.solana: defaultTokenAddresses = solana!.getDefaultTokenContractAddresses(); @@ -380,6 +402,10 @@ abstract class HomeSettingsViewModelBase with Store { return await base!.getErc20Token(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return await arbitrum!.getErc20Token(_balanceViewModel.wallet, contractAddress); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); } @@ -426,6 +452,11 @@ abstract class HomeSettingsViewModelBase with Store { if (!value) base!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + arbitrum!.addErc20Token(_balanceViewModel.wallet, token as Erc20Token); + if (!value) arbitrum!.removeTokenTransactionsInHistory(_balanceViewModel.wallet, token); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { final address = solana!.getTokenAddress(token); solana!.addSPLToken(_balanceViewModel.wallet, token, address); @@ -487,6 +518,14 @@ abstract class HomeSettingsViewModelBase with Store { ..sort(_sortFunc)); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + tokens.addAll(arbitrum! + .getERC20Currencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { tokens.addAll(solana! .getSPLTokenCurrencies(_balanceViewModel.wallet) @@ -528,7 +567,7 @@ abstract class HomeSettingsViewModelBase with Store { bool _matchesSearchText(CryptoCurrency asset) { final address = getTokenAddressBasedOnWallet(asset); - // The homes settings would only be displayed for either of Tron, Ethereum, Polygon, Base or Solana Wallets. + // The homes settings would only be displayed for either of Tron, EVM or Solana Wallets. if (address == null) return false; return searchText.isEmpty || @@ -558,11 +597,15 @@ abstract class HomeSettingsViewModelBase with Store { return base!.getTokenAddress(asset); } + if (_balanceViewModel.wallet.type == WalletType.arbitrum) { + return arbitrum!.getTokenAddress(asset); + } + if (_balanceViewModel.wallet.type == WalletType.zano) { return zano!.getZanoAssetAddress(asset); } - // We return null if it's neither Tron, Polygon, Base, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets). + // We return null if it's neither Tron, EVM or Solana wallet (which is actually impossible because we only display home settings for either of these four wallets). return null; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 874da36a6..33df3ea24 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; @@ -139,6 +140,10 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = base!.assetOfTransaction(balanceViewModel.wallet, transaction); return asset; } + if (balanceViewModel.wallet.type == WalletType.arbitrum) { + final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } if (balanceViewModel.wallet.type == WalletType.solana) { final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); @@ -199,6 +204,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: base!.formatterBaseAmountToDouble(transaction: transaction), price: price); break; + case WalletType.arbitrum: + final asset = arbitrum!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: arbitrum!.formatterArbitrumAmountToDouble(transaction: transaction), + price: price); + break; case WalletType.nano: amount = calculateFiatAmountRaw( cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 8fdf029b5..fec47f3f3 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -18,6 +18,7 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -109,14 +110,10 @@ abstract class ExchangeTradeViewModelBase with Store { @observable bool isSendable; - - bool get isSwapsXyzSendingEVMTokenSwap => (_provider is SwapsXyzExchangeProvider) && - (wallet.type == WalletType.ethereum && - wallet.currency != trade.from || - (wallet.type == WalletType.polygon && - wallet.currency != trade.from) || - (wallet.type == WalletType.base && - wallet.currency != trade.from)); + bool get isSwapsXyzSendingEVMTokenSwap => + (_provider is SwapsXyzExchangeProvider) && + isEVMCompatibleChain(wallet.type) && + wallet.currency != trade.from; String get extraInfo => trade.extraId != null && trade.extraId!.isNotEmpty ? '\n\n' + S.current.exchange_extra_info @@ -203,7 +200,8 @@ abstract class ExchangeTradeViewModelBase with Store { sendViewModel.selectedCryptoCurrency = selected; - final pendingTransaction = await sendViewModel.createTransaction(provider: _provider, trade: trade); + final pendingTransaction = + await sendViewModel.createTransaction(provider: _provider, trade: trade); if (_provider is SwapsXyzExchangeProvider) { final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? ''; @@ -336,6 +334,9 @@ abstract class ExchangeTradeViewModelBase with Store { bool _isBaseToken() => wallet.currency == CryptoCurrency.baseEth && tradeFrom?.tag == CryptoCurrency.baseEth.tag; + bool _isArbitrumToken() => + wallet.currency == CryptoCurrency.arbEth && tradeFrom?.tag == CryptoCurrency.arbEth.tag; + bool _isTronToken() => wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title; @@ -348,9 +349,10 @@ abstract class ExchangeTradeViewModelBase with Store { _isPolygonToken() || _isSplToken() || _isTronToken() || - _isBaseToken(); + _isBaseToken() || + _isArbitrumToken(); } - + Future registerSwapsXyzTransaction() async { try { if (!(_provider is SwapsXyzExchangeProvider)) return; @@ -366,7 +368,9 @@ abstract class ExchangeTradeViewModelBase with Store { return; } - final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? sendViewModel.pendingTransaction?.id ?? ''; + final txHash = sendViewModel.pendingTransaction?.evmTxHashFromRawHex ?? + sendViewModel.pendingTransaction?.id ?? + ''; if (txHash.isEmpty) { printV('SwapsXyz: transaction register: skipped (txHash empty)'); @@ -379,7 +383,8 @@ abstract class ExchangeTradeViewModelBase with Store { return; } - printV('SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); + printV( + 'SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId'); final registered = await swaps.registerAltVmTx( txId: trade.id, @@ -423,6 +428,8 @@ abstract class ExchangeTradeViewModelBase with Store { return PolygonURI(amount: amount, address: inputAddress); case WalletType.base: return BaseURI(amount: amount, address: inputAddress); + case WalletType.arbitrum: + return ArbitrumURI(amount: amount, address: inputAddress); case WalletType.solana: return SolanaURI(amount: amount, address: inputAddress); case WalletType.tron: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 29d992b75..8e471804b 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -878,6 +878,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.baseEth; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.arbitrum: + depositCurrency = CryptoCurrency.arbEth; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.solana: depositCurrency = CryptoCurrency.sol; receiveCurrency = CryptoCurrency.xmr; diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 67a08872c..dffa690f8 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -81,6 +81,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.banano: case WalletType.nano: diff --git a/lib/view_model/payment/payment_view_model.dart b/lib/view_model/payment/payment_view_model.dart index 36bf4f193..5676a6b92 100644 --- a/lib/view_model/payment/payment_view_model.dart +++ b/lib/view_model/payment/payment_view_model.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/utils/print_verbose.dart'; -import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; part 'payment_view_model.g.dart'; diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index c3364ffec..3b4709a2f 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -32,6 +32,7 @@ class WalletRestoreFromQRCode { 'ethereum-wallet': WalletType.ethereum, 'polygon-wallet': WalletType.polygon, 'base-wallet': WalletType.base, + 'arbitrum-wallet': WalletType.arbitrum, 'nano-wallet': WalletType.nano, 'nano_wallet': WalletType.nano, 'bitcoincash': WalletType.bitcoinCash, diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index ff878838c..49b2120f2 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/decred/decred.dart'; @@ -102,6 +103,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.banano: case WalletType.solana: case WalletType.tron: + case WalletType.arbitrum: return false; } } @@ -119,7 +121,8 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && - wallet.type != WalletType.tron; + wallet.type != WalletType.tron && + wallet.type != WalletType.arbitrum; @computed bool get isElectrumWallet => diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index a599ae836..1caaf050b 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/entities/contact_base.dart'; @@ -49,8 +50,15 @@ abstract class OutputBase with Store { address = '', note = '', extractedAddress = '', + estimatedFee = 0.0, parsedAddress = ParsedAddress(addresses: []) { _setCryptoNumMaximumFractionDigits(); + autorun((_) { + final status = _wallet.syncStatus; + printV("Sync status changed to $status. Recalculating fees"); + + calculateEstimatedFee(); + }); } Key key; @@ -118,6 +126,9 @@ abstract class OutputBase with Store { case WalletType.base: _amount = base!.formatterBaseParseAmount(_cryptoAmount); break; + case WalletType.arbitrum: + _amount = arbitrum!.formatterArbitrumParseAmount(_cryptoAmount); + break; case WalletType.wownero: _amount = wownero!.formatterWowneroParseAmount(amount: _cryptoAmount); break; @@ -145,28 +156,74 @@ abstract class OutputBase with Store { return amount; } - @computed - double get estimatedFee { - try { - // forces mobx to rebuild the computed value - final _ = _wallet.syncStatus; + @observable + double estimatedFee; + @action + Future calculateEstimatedFee() async { + try { if (_wallet.type == WalletType.tron) { if (cryptoCurrencyHandler() == CryptoCurrency.trx) { - final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? 0; - return double.parse(nativeEstimatedFee.toString()); + final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? '0'; + estimatedFee = double.parse(nativeEstimatedFee); } else { - final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; - return double.parse(trc20EstimatedFee.toString()); + final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? '0'; + estimatedFee = double.parse(trc20EstimatedFee); } } + if (isEVMCompatibleChain(_wallet.type)) { + if (_wallet.type == WalletType.arbitrum) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.arbEth + ? arbitrum!.getArbitrumNativeEstimatedFee(_wallet) + : arbitrum!.getArbitrumERC20EstimatedFee(_wallet); + + estimatedFee = arbitrum! + .formatterArbitrumAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + return; + } + + await _wallet.updateEstimatedFeesParams(_settingsStore.priority[_wallet.type]!); + + double calculatedFee = 0.0; + + if (_wallet.type == WalletType.ethereum) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.eth + ? ethereum!.getEthereumNativeEstimatedFee(_wallet) + : ethereum!.getEthereumERC20EstimatedFee(_wallet); + + calculatedFee = ethereum! + .formatterEthereumAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + if (_wallet.type == WalletType.polygon) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.maticpoly + ? polygon!.getPolygonNativeEstimatedFee(_wallet) + : polygon!.getPolygonERC20EstimatedFee(_wallet); + + calculatedFee = polygon! + .formatterPolygonAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + if (_wallet.type == WalletType.base) { + String? fee = cryptoCurrencyHandler() == CryptoCurrency.baseEth + ? base!.getBaseNativeEstimatedFee(_wallet) + : base!.getBaseERC20EstimatedFee(_wallet); + calculatedFee = + base!.formatterBaseAmountToDouble(amount: BigInt.from(double.parse(fee ?? '0.0'))); + } + + estimatedFee = calculatedFee; + } + if (_wallet.type == WalletType.solana) { - return solana!.getEstimateFees(_wallet) ?? 0.0; + estimatedFee = solana!.getEstimateFees(_wallet) ?? 0.0; } int? fee = _wallet.calculateEstimatedFee( - _settingsStore.priority[_wallet.type]!, formattedCryptoAmount); + _settingsStore.priority[_wallet.type]!, + formattedCryptoAmount, + ); if (_wallet.type == WalletType.bitcoin) { if (_settingsStore.priority[_wallet.type] == @@ -175,48 +232,34 @@ abstract class OutputBase with Store { _wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount); } - return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); + estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash || _wallet.type == WalletType.dogecoin) { - return bitcoin!.formatterBitcoinAmountToDouble(amount: fee); + estimatedFee = bitcoin!.formatterBitcoinAmountToDouble(amount: fee); } if (_wallet.type == WalletType.monero) { - return monero!.formatterMoneroAmountToDouble(amount: fee); + estimatedFee = monero!.formatterMoneroAmountToDouble(amount: fee); } if (_wallet.type == WalletType.wownero) { - return wownero!.formatterWowneroAmountToDouble(amount: fee); - } - - if (_wallet.type == WalletType.ethereum) { - return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee)); - } - - if (_wallet.type == WalletType.polygon) { - return polygon!.formatterPolygonAmountToDouble(amount: BigInt.from(fee)); - } - - if (_wallet.type == WalletType.base) { - return base!.formatterBaseAmountToDouble(amount: BigInt.from(fee)); + estimatedFee = wownero!.formatterWowneroAmountToDouble(amount: fee); } if (_wallet.type == WalletType.zano) { - return zano!.formatterIntAmountToDouble( + estimatedFee = zano!.formatterIntAmountToDouble( amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } if (_wallet.type == WalletType.decred) { - return decred!.formatterDecredAmountToDouble(amount: fee); + estimatedFee = decred!.formatterDecredAmountToDouble(amount: fee); } } catch (e) { printV(e.toString()); } - - return 0; } @computed @@ -337,6 +380,7 @@ abstract class OutputBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: case WalletType.haven: diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 43154868e..29f576d66 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -57,6 +57,7 @@ abstract class SendTemplateViewModelBase with Store { _wallet.type != WalletType.ethereum && _wallet.type != WalletType.polygon && _wallet.type != WalletType.base && + _wallet.type != WalletType.arbitrum && _wallet.type != WalletType.solana && _wallet.type != WalletType.tron; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 24a575e48..2c038b036 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -29,6 +29,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -137,7 +138,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get isMwebEnabled => balanceViewModel.mwebEnabled; bool get isEVMWallet => isEVMCompatibleChain(walletType); - @action void setShowAddressBookPopup(bool value) { _settingsStore.showAddressBookPopupEnabled = value; @@ -223,6 +223,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.tron: case WalletType.solana: return wallet.currency; @@ -603,6 +604,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor priority, ); + _isSwapsXYZCallDataTx = true; + state = ExecutedSuccessfullyState(); + return pendingTransaction; // do NOT fall back to regular flow + } + if (walletType == WalletType.arbitrum) { + _pendingApprovalTx = await buildApprovalIfNeeded( + spender: routerTo!, + tokenContract: tokenContract, + requiredAmount: requiredAmount, + sourceTokenDecimals: trade.sourceTokenDecimals, + ); + + // Build the callData tx + pendingTransaction = await arbitrum!.createRawCallDataTransaction( + wallet, + routerTo, + routerData, + routerValueWei, + ); + _isSwapsXYZCallDataTx = true; state = ExecutedSuccessfullyState(); return pendingTransaction; // do NOT fall back to regular flow @@ -637,7 +658,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor return pendingTransaction; } if (walletType == WalletType.base) { - final priority = _settingsStore.priority[WalletType.polygon]!; + final priority = _settingsStore.priority[WalletType.base]!; pendingTransaction = await base!.createRawCallDataTransaction( wallet, routerTo!, @@ -649,6 +670,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor state = ExecutedSuccessfullyState(); return pendingTransaction; } + if (walletType == WalletType.arbitrum) { + pendingTransaction = await arbitrum!.createRawCallDataTransaction( + wallet, + routerTo!, + routerData, + routerValueWei, + ); + _isSwapsXYZCallDataTx = true; + state = ExecutedSuccessfullyState(); + return pendingTransaction; + } } } @@ -847,7 +879,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && - wallet.type != WalletType.tron) { + wallet.type != WalletType.tron && + wallet.type != WalletType.arbitrum) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -890,6 +923,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.base: return base!.createBaseTransactionCredentials(outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.arbitrum: + return arbitrum! + .createArbitrumTransactionCredentials(outputs, currency: selectedCryptoCurrency); case WalletType.solana: return solana! .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); @@ -1016,6 +1052,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor if (walletType == WalletType.ethereum || walletType == WalletType.polygon || walletType == WalletType.base || + walletType == WalletType.arbitrum || walletType == WalletType.haven) { if (errorMessage.contains('gas required exceeds allowance')) { return S.current.gas_exceeds_allowance; @@ -1154,6 +1191,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor spender, requiredAmount, ); + } else if (walletType == WalletType.arbitrum) { + needsApproval = await arbitrum!.isApprovalRequired( + wallet, + tokenContract, + spender, + requiredAmount, + ); } if (!needsApproval) return null; @@ -1196,6 +1240,13 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor erc20Token, priority, ); + } else if (walletType == WalletType.arbitrum) { + return await arbitrum!.createTokenApproval( + wallet, + requiredAmount, + spender, + erc20Token, + ); } return null; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 150ccdd3d..7d328c81f 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; @@ -79,6 +80,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useBaseScan => _settingsStore.useBaseScan; + @computed + bool get useArbiScan => _settingsStore.useArbiScan; + @computed bool get useTronGrid => _settingsStore.useTronGrid; @@ -118,6 +122,8 @@ abstract class PrivacySettingsViewModelBase with Store { bool get canUseBaseScan => _wallet.type == WalletType.base; + bool get canUseArbiScan => _wallet.type == WalletType.arbitrum; + bool get canUseTronGrid => _wallet.type == WalletType.tron; bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin; @@ -195,6 +201,12 @@ abstract class PrivacySettingsViewModelBase with Store { tron!.updateTronGridUsageState(_wallet, value); } + @action + void setUseArbiScan(bool value) { + _settingsStore.useArbiScan = value; + arbitrum!.updateArbitrumScanUsageState(_wallet, value); + } + @action void setUseMempoolFeeAPI(bool value) => _settingsStore.useMempoolFeeAPI = value; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index dde59058a..ffe22facf 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -77,6 +77,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.base: _addBaseListItems(tx, dateFormat); break; + case WalletType.arbitrum: + _addArbitrumListItems(tx, dateFormat); + break; case WalletType.solana: _addSolanaListItems(tx, dateFormat); break; @@ -204,6 +207,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://blockchair.com/dogecoin/transaction/${txId}'; case WalletType.base: return 'https://basescan.org/tx/${txId}'; + case WalletType.arbitrum: + return 'https://arbiscan.io/tx/${txId}'; case WalletType.none: return ''; } @@ -241,6 +246,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'dcrdata.decred.org'; case WalletType.base: return S.current.view_transaction_on + 'basescan.org'; + case WalletType.arbitrum: + return S.current.view_transaction_on + 'arbiscan.io'; case WalletType.none: return ''; } @@ -606,6 +613,56 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addArbitrumListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.direction == TransactionDirection.incoming && tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + void _addSolanaListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ StandartListItem( diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 7f2a78b0d..259c0158f 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/reactions/wallet_utils.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/decred/decred.dart'; @@ -146,6 +147,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return DogeURI(amount: amount, address: address.address); case WalletType.base: return BaseURI(amount: amount, address: address.address); + case WalletType.arbitrum: + return ArbitrumURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -272,6 +275,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.arbitrum) { + final primaryAddress = arbitrum!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (wallet.type == WalletType.solana) { final primaryAddress = solana!.getAddress(wallet); @@ -450,6 +459,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo 'assets/images/usdc_icon.svg', 'assets/images/more_tokens.svg', ]; + case WalletType.arbitrum: + return [ + 'assets/images/crypto/arbitrum.webp', + 'assets/images/usdc_icon.svg', + 'assets/images/more_tokens.svg', + ]; default: return []; } @@ -473,6 +488,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return 'assets/images/zano_chain_mono.svg'; case WalletType.base: return 'assets/images/base_chain_mono.svg'; + case WalletType.arbitrum: + return 'assets/images/arbitrum_chain_mono.svg'; default: return 'assets/images/eth_chain_mono.svg'; } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index f429cd958..7874ea677 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -127,6 +127,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: items.addAll([ @@ -255,6 +256,8 @@ abstract class WalletKeysViewModelBase with Store { return 'polygon-wallet'; case WalletType.base: return 'base-wallet'; + case WalletType.arbitrum: + return 'arbitrum-wallet'; case WalletType.solana: return 'solana-wallet'; case WalletType.tron: diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 64c5cb62b..79ecbdfb4 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/core/new_wallet_arguments.dart'; import 'package:cake_wallet/dogecoin/dogecoin.dart'; @@ -97,6 +98,13 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { mnemonic: newWalletArguments!.mnemonic, passphrase: passphrase, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + passphrase: passphrase, + ); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashNewWalletCredentials( name: name, diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index ef16982c0..ce427b8a1 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/arbitrum/arbitrum.dart'; import 'package:cake_wallet/base/base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -52,6 +53,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.base: + case WalletType.arbitrum: case WalletType.decred: case WalletType.bitcoin: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; @@ -92,6 +94,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletType.ethereum, WalletType.polygon, WalletType.base, + WalletType.arbitrum, WalletType.nano, WalletType.banano, WalletType.solana, @@ -183,6 +186,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, passphrase: passphrase, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, @@ -276,6 +286,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, privateKey: options['private_key'] as String, ); + case WalletType.arbitrum: + return arbitrum!.createArbitrumRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); case WalletType.solana: return solana!.createSolanaRestoreWalletFromPrivateKey( name: name, diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 510c3ad47..dda4bea3d 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -237,6 +237,7 @@ flutter: - assets/decred_node_list.yml - assets/dogecoin_electrum_server_list.yml - assets/base_node_list.yml + - assets/arbitrum_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index a450aaee9..e0d6b6da4 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -60,6 +60,7 @@ "approve": "ﺪﻤﺘﻌﻳ", "approve_request": "الموافقة على الطلب", "approve_tokens": "الموافقة على الرموز", + "arbiscan_history": "تاريخ أربيسكان", "arrive_in_this_address": "سيصل ${currency} ${tag}إلى هذا العنوان", "ascending": "تصاعدي", "ask_each_time": "اسأل في كل مرة", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index aef0b0db0..482a24069 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -60,6 +60,7 @@ "approve": "Одобряване", "approve_request": "Одобрете искане", "approve_tokens": "Одобрете жетоните", + "arbiscan_history": "Арбисканска история", "arrive_in_this_address": "${currency} ${tag}ще отидат на този адрес", "ascending": "Възходящ", "ask_each_time": "Питайте всеки път", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 7e6940b03..79331c22d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -60,6 +60,7 @@ "approve": "Schvalovat", "approve_request": "Schválit žádost", "approve_tokens": "Schválit tokeny", + "arbiscan_history": "Arbiscan History", "arrive_in_this_address": "${currency} ${tag}přijde na tuto adresu", "ascending": "Vzestupné", "ask_each_time": "Zeptejte se pokaždé", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 267ae5632..49b4e2595 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -60,6 +60,7 @@ "approve": "Genehmigen", "approve_request": "Anfrage genehmigen", "approve_tokens": "Token genehmigen", + "arbiscan_history": "ArbiScan-Geschichte", "arrive_in_this_address": "${currency} ${tag} wird an dieser Adresse ankommen", "ascending": "Aufsteigend", "ask_each_time": "Jedes Mal fragen", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index cc9271dac..c2769eadf 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -60,6 +60,7 @@ "approve": "Approve", "approve_request": "Approve Request", "approve_tokens": "Approve tokens", + "arbiscan_history": "ArbiScan History", "arrive_in_this_address": "${currency} ${tag}will arrive in this address", "ascending": "Ascending", "ask_each_time": "Ask each time", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index a50b8ca90..f4b62918c 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -58,9 +58,10 @@ "apk_update": "Actualización de APK", "appearance": "Apariencia", "approve": "Aprobar", - "approve_request": "Aprobar Solicitud", - "approve_tokens": "Autorizar tokens", - "arrive_in_this_address": "${currency} ${tag} llegará a esta dirección", + "approve_request": "Aprobar la solicitud", + "approve_tokens": "Aprobar tokens", + "arbiscan_history": "Historia de ArbiScan", + "arrive_in_this_address": "${currency} ${tag}llegará a esta dirección", "ascending": "Ascendente", "ask_each_time": "Preguntar cada vez", "auth_store_ban_timeout": "tiempo de espera del bloqueo", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 13ca5e3f7..42d64b3a3 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -60,6 +60,7 @@ "approve": "Approuver", "approve_request": "Approuver la demande", "approve_tokens": "Approuver les jetons", + "arbiscan_history": "Historique d'ArbiScan", "arrive_in_this_address": "${currency} ${tag}arrivera à cette adresse", "ascending": "Ascendant", "ask_each_time": "Demander à chaque fois", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 7ff295654..ad20900cf 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -60,6 +60,7 @@ "approve": "Amincewa", "approve_request": "Amince da bukata", "approve_tokens": "Amince da Alamu", + "arbiscan_history": "Tarihin Arbiscan", "arrive_in_this_address": "${currency} ${tag} zai je wurin wannan adireshi", "ascending": "Hau", "ask_each_time": "Tambaya kowane lokaci", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f45d89abd..967b2fe75 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -60,6 +60,7 @@ "approve": "मंज़ूरी देना", "approve_request": "अनुरोध को स्वीकृत करें", "approve_tokens": "टोकन को मंजूरी देना", + "arbiscan_history": "अरबीस्कैन इतिहास", "arrive_in_this_address": "${currency} ${tag}इस पते पर पहुंचेंगे", "ascending": "आरोही", "ask_each_time": "हर बार पूछें", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 1cdc4966c..70a6c2dbe 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -60,6 +60,7 @@ "approve": "Odobriti", "approve_request": "Odobriti zahtjev", "approve_tokens": "Odobriti tokene", + "arbiscan_history": "Arbiskanska povijest", "arrive_in_this_address": "${currency} ${tag}će stići na ovu adresu", "ascending": "Uzlazni", "ask_each_time": "Pitajte svaki put", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index bc85d346d..137262430 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -60,6 +60,7 @@ "approve": "Հաստատել", "approve_request": "Հաստատում է հայցը", "approve_tokens": "Հաստատում է նշանները", + "arbiscan_history": "Արբիսկանի պատմություն", "arrive_in_this_address": "${currency} ${tag}կժամանի այս հասցեում", "ascending": "Աճող", "ask_each_time": "Հարցնել ամեն անգամ", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 34bbf65f5..0fd485ab9 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -60,6 +60,7 @@ "approve": "Menyetujui", "approve_request": "Menyetujui permintaan", "approve_tokens": "Menyetujui token", + "arbiscan_history": "Sejarah ArbiScan", "arrive_in_this_address": "${currency} ${tag} akan tiba di alamat ini", "ascending": "Naik", "ask_each_time": "Tanyakan setiap kali", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 4b7819d34..5880b7ee4 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -60,6 +60,7 @@ "approve": "Approvare", "approve_request": "Approvare la richiesta", "approve_tokens": "Approvare i token", + "arbiscan_history": "Storia dell'ArbiScan", "arrive_in_this_address": "${currency} ${tag}arriverà a questo indirizzo", "ascending": "Ascendente", "ask_each_time": "Chiedi ogni volta", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 7c0c3381f..9fa6303dc 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -60,6 +60,7 @@ "approve": "承認する", "approve_request": "リクエストを承認します", "approve_tokens": "トークンを承認します", + "arbiscan_history": "ArbiScan の歴史", "arrive_in_this_address": "${currency} ${tag}はこの住所に到着します", "ascending": "上昇", "ask_each_time": "毎回尋ねてください", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index fce8f8fe2..a0445aad4 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -60,6 +60,7 @@ "approve": "승인", "approve_request": "요청 승인", "approve_tokens": "토큰을 승인합니다", + "arbiscan_history": "ArbiScan 역사", "arrive_in_this_address": "${currency} ${tag}이(가) 이 주소로 도착합니다", "ascending": "오름차순", "ask_each_time": "매번 묻기", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 213fbcb57..6f98ae808 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -60,6 +60,7 @@ "approve": "လက်မခံပါ။", "approve_request": "တောင်းဆိုမှုကိုအတည်ပြု", "approve_tokens": "တိုကင်အတည်ပြု", + "arbiscan_history": "arbiscan သမိုင်း", "arrive_in_this_address": "${currency} ${tag}ဤလိပ်စာသို့ ရောက်ရှိပါမည်။", "ascending": "တက်", "ask_each_time": "တစ်ခုချင်းစီကိုအချိန်မေးပါ", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 9c8025e06..4777ba9db 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -60,6 +60,7 @@ "approve": "Goedkeuren", "approve_request": "Het verzoek goedkeuren", "approve_tokens": "Tokens goedkeuren", + "arbiscan_history": "ArbiScan-geschiedenis", "arrive_in_this_address": "${currency} ${tag}komt aan op dit adres", "ascending": "Stijgend", "ask_each_time": "Vraag het elke keer", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 5d95b1ef6..6b0246a58 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -57,16 +57,17 @@ "anonpay_description": "Wygeneruj ${type}. Odbiorca może ${method} przy użyciu dowolnej obsługiwanej kryptowaluty, a otrzymasz środki w tym portfelu.", "apk_update": "Aktualizacja APK", "appearance": "Wygląd", - "approve": "Zatwierdź", - "approve_request": "Zatwierdź żądanie", - "approve_tokens": "Zatwierdź tokeny", - "arrive_in_this_address": "${currency} ${tag} dotrze na ten adres", - "ascending": "Rosnąco", - "ask_each_time": "Pytaj za każdym razem", - "auth_store_ban_timeout": "czas wygaśnięcia blokady", - "auth_store_banned_for": "Zbanowany na ", - "auth_store_banned_minutes": " minut", - "auth_store_incorrect_password": "Nieprawidłowy PIN", + "approve": "Zatwierdzić", + "approve_request": "Zatwierdzić żądanie", + "approve_tokens": "Zatwierdzić tokeny", + "arbiscan_history": "Historia ArbiScanu", + "arrive_in_this_address": "${currency} ${tag}dotrze na ten adres", + "ascending": "Wznoszący się", + "ask_each_time": "Zapytaj za każdym razem", + "auth_store_ban_timeout": "przekroczenie limitu czasu", + "auth_store_banned_for": "Zablokowany na ", + "auth_store_banned_minutes": " minuty", + "auth_store_incorrect_password": "Niepoprawny PIN", "authenticated": "Uwierzytelniony", "authentication": "Uwierzytelnianie", "auto_generate_addresses": "Automatyczne generowanie adresów", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index de4b1fbe6..334c33252 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -60,6 +60,7 @@ "approve": "Aprovar", "approve_request": "Aprovar solicitação", "approve_tokens": "Aprovar tokens", + "arbiscan_history": "História do ArbiScan", "arrive_in_this_address": "${currency} ${tag}chegará neste endereço", "ascending": "Ascendente", "ask_each_time": "Pergunte cada vez", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 93ba682d5..5e3825704 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -60,6 +60,7 @@ "approve": "Утвердить", "approve_request": "Утвердить запрос", "approve_tokens": "Одобрить токены", + "arbiscan_history": "История АрбиСкан", "arrive_in_this_address": "${currency} ${tag}придет на этот адрес", "ascending": "Восходящий", "ask_each_time": "Спросите каждый раз", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0e32a1508..4c353932c 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -60,6 +60,7 @@ "approve": "อนุมัติ", "approve_request": "อนุมัติคำขอ", "approve_tokens": "อนุมัติโทเค็น", + "arbiscan_history": "ประวัติ ArbiScan", "arrive_in_this_address": "${currency} ${tag}จะมาถึงที่อยู่นี้", "ascending": "จากน้อยไปมาก", "ask_each_time": "ถามทุกครั้ง", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index a5a0e9d16..9719ae9cb 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -60,6 +60,7 @@ "approve": "Aprubahan", "approve_request": "Aprubahan ang kahilingan", "approve_tokens": "Aprubahan ang mga token", + "arbiscan_history": "Kasaysayan ng Arbiscan", "arrive_in_this_address": "Ang ${currency} ${tag} ay darating sa address na ito", "ascending": "Umakyat", "ask_each_time": "Magtanong sa tuwing", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index be16e17ac..9b10fecc2 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -60,6 +60,7 @@ "approve": "Onaylamak", "approve_request": "Talebi Onaylama", "approve_tokens": "Jetonları onaylayın", + "arbiscan_history": "ArbiScan Geçmişi", "arrive_in_this_address": "${currency} ${tag}bu adrese ulaşacak", "ascending": "Yükselen", "ask_each_time": "Her seferinde sor", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index ce4a0ee56..42c4dfb43 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -60,6 +60,7 @@ "approve": "Затвердити", "approve_request": "Запитайте запит", "approve_tokens": "Затвердити токени", + "arbiscan_history": "Арбісканська історія", "arrive_in_this_address": "${currency} ${tag}надійде на цю адресу", "ascending": "Висхід", "ask_each_time": "Запитайте кожен раз", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index bad4b8354..94189af3b 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -60,6 +60,7 @@ "approve": "ﻭﺮﮐ ﺭﻮﻈﻨﻣ", "approve_request": "درخواست کو منظور کریں", "approve_tokens": "ٹوکن کو منظور کریں", + "arbiscan_history": "اربیسکن تاریخ", "arrive_in_this_address": "${currency} ${tag}اس پتے پر پہنچے گا۔", "ascending": "چڑھنے", "ask_each_time": "ہر بار پوچھیں", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index 5c5ac559d..086657785 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -60,6 +60,7 @@ "approve": "Phê duyệt", "approve_request": "Phê duyệt yêu cầu", "approve_tokens": "Phê duyệt mã thông báo", + "arbiscan_history": "Lịch sử quét Arbi", "arrive_in_this_address": "${currency} ${tag} sẽ đến địa chỉ này", "ascending": "Tăng dần", "ask_each_time": "Hỏi mỗi lần", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 76d0b3fa5..ae90c358e 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -60,6 +60,7 @@ "approve": "Fi ọwọ si", "approve_request": "IKILỌ RẸ", "approve_tokens": "Ṣe fọwọsi awọn àmi", + "arbiscan_history": "Itan Arbiscan", "arrive_in_this_address": "${currency} ${tag} máa dé sí àdírẹ́sì yìí", "ascending": "Goke", "ask_each_time": "Beere lọwọ kọọkan", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 077148c57..824f9949c 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -60,6 +60,7 @@ "approve": "批准", "approve_request": "批准请求", "approve_tokens": "批准令牌", + "arbiscan_history": "ArbiScan 历史", "arrive_in_this_address": "${currency} ${tag}将到达此地址", "ascending": "上升", "ask_each_time": "每次问", diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 66cd4d615..1d4dcadc0 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" ;; esac diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index f69c9f328..9a8ee840c 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -31,7 +31,7 @@ case $APP_IOS_TYPE in ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" ;; esac diff --git a/scripts/linux/app_config.sh b/scripts/linux/app_config.sh index 70f35d9b7..78f01fa8d 100755 --- a/scripts/linux/app_config.sh +++ b/scripts/linux/app_config.sh @@ -13,7 +13,7 @@ CONFIG_ARGS="" case $APP_LINUX_TYPE in $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --excludeFlutterSecureStorage";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --arbitrum --excludeFlutterSecureStorage";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 0ae44f216..589b5ec1d 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --arbitrum";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/tool/configure.dart b/tool/configure.dart index e6b2267f1..47b337940 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -13,6 +13,7 @@ const zanoOutputPath = 'lib/zano/zano.dart'; const decredOutputPath = 'lib/decred/decred.dart'; const dogecoinOutputPath = 'lib/dogecoin/dogecoin.dart'; const baseOutputPath = 'lib/base/base.dart'; +const arbitrumOutputPath = 'lib/arbitrum/arbitrum.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -34,6 +35,7 @@ Future main(List args) async { final hasDecred = args.contains('${prefix}decred'); final hasDogecoin = args.contains('${prefix}dogecoin'); final hasBase = args.contains('${prefix}base'); + final hasArbitrum = args.contains('${prefix}arbitrum'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -50,6 +52,7 @@ Future main(List args) async { await generateDecred(hasDecred); await generateDogecoin(hasDogecoin); await generateBase(hasBase); + await generateArbitrum(hasArbitrum); await generatePubspec( hasMonero: hasMonero, @@ -67,6 +70,7 @@ Future main(List args) async { hasDecred: hasDecred, hasDogecoin: hasDogecoin, hasBase: hasBase, + hasArbitrum: hasArbitrum, ); await generateWalletTypes( hasMonero: hasMonero, @@ -83,6 +87,7 @@ Future main(List args) async { hasDecred: hasDecred, hasDogecoin: hasDogecoin, hasBase: hasBase, + hasArbitrum: hasArbitrum, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -795,6 +800,8 @@ abstract class Ethereum { HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getEthereumNativeEstimatedFee(WalletBase wallet); + String? getEthereumERC20EstimatedFee(WalletBase wallet); } """; @@ -916,6 +923,8 @@ abstract class Polygon { HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getPolygonNativeEstimatedFee(WalletBase wallet); + String? getPolygonERC20EstimatedFee(WalletBase wallet); } """; @@ -1491,9 +1500,7 @@ abstract class DogeCoin { final output = '$dogecoinCommonHeaders\n' + (hasImplementation ? '$dogecoinCWHeaders\n' : '\n') + (hasImplementation ? '$dogecoinCwPart\n\n' : '\n') + - (hasImplementation - ? dogecoinCWDefinition - : dogecoinEmptyDefinition) + + (hasImplementation ? dogecoinCWDefinition : dogecoinEmptyDefinition) + '\n' + dogecoinContent; @@ -1612,6 +1619,8 @@ abstract class Base { HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + String? getBaseNativeEstimatedFee(WalletBase wallet); + String? getBaseERC20EstimatedFee(WalletBase wallet); } """; @@ -1632,6 +1641,131 @@ abstract class Base { await outputFile.writeAsString(output); } +Future generateArbitrum(bool hasImplementation) async { + final outputFile = File(arbitrumOutputPath); + const arbitrumCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; +import 'package:cw_core/hardware/hardware_wallet_service.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; +import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger; +import 'package:bitbox_flutter/bitbox_flutter.dart' as bitbox; +import 'package:web3dart/web3dart.dart'; + +"""; + const arbitrumCWHeaders = """ +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/hardware/evm_chain_ledger_credentials.dart'; +import 'package:cw_evm/hardware/evm_chain_bitbox_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/hardware/evm_chain_bitbox_service.dart'; +import 'package:cw_evm/hardware/evm_chain_ledger_service.dart'; + +import 'package:cw_arbitrum/arbitrum_client.dart'; +import 'package:cw_arbitrum/arbitrum_wallet.dart'; +import 'package:cw_arbitrum/arbitrum_wallet_service.dart'; +import 'package:cw_arbitrum/default_arbitrum_erc20_tokens.dart'; +import 'package:eth_sig_util/util/utils.dart'; + +"""; + const arbitrumCwPart = "part 'cw_arbitrum.dart';"; + const arbitrumContent = """ +abstract class Arbitrum { + List getArbitrumWordList(String language); + WalletService createArbitrumWalletService(bool isDirect); + WalletCredentials createArbitrumNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + String? password, + String? mnemonic, + String? passphrase}); + WalletCredentials createArbitrumRestoreWalletFromSeedCredentials( + {required String name, + required String mnemonic, + required String password, + String? passphrase}); + WalletCredentials createArbitrumRestoreWalletFromPrivateKey( + {required String name, required String privateKey, required String password}); + WalletCredentials createArbitrumHardwareWalletCredentials( + {required String name, required HardwareAccountData hwAccountData, WalletInfo? walletInfo}); + String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); + + Object createArbitrumTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + int? feeRate, + }); + + Object createArbitrumTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + required int feeRate, + }); + + int formatterArbitrumParseAmount(String amount); + double formatterArbitrumAmountToDouble( + {TransactionInfo? transaction, BigInt? amount, int exponent = 18}); + List getERC20Currencies(WalletBase wallet); + Future addErc20Token(WalletBase wallet, CryptoCurrency token); + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token); + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token); + Future getErc20Token(WalletBase wallet, String contractAddress); + + Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, + CryptoCurrency token); + + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + void updateArbitrumScanUsageState(WalletBase wallet, bool isEnabled); + Web3Client? getWeb3Client(WalletBase wallet); + String getTokenAddress(CryptoCurrency asset); + + Future setHardwareWalletService(WalletBase wallet, HardwareWalletService service); + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); + HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager); + List getDefaultTokenContractAddresses(); + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); + Future isApprovalRequired(WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount); + Future createRawCallDataTransaction(WalletBase wallet, String to, String dataHex, BigInt valueWei); + String? getArbitrumNativeEstimatedFee(WalletBase wallet); + String? getArbitrumERC20EstimatedFee(WalletBase wallet); +} + + """; + + const arbitrumEmptyDefinition = 'Arbitrum? arbitrum;\n'; + const arbitrumCWDefinition = 'Arbitrum? arbitrum = CWArbitrum();\n'; + + final output = '$arbitrumCommonHeaders\n' + + (hasImplementation ? '$arbitrumCWHeaders\n' : '\n') + + (hasImplementation ? '$arbitrumCwPart\n\n' : '\n') + + (hasImplementation ? arbitrumCWDefinition : arbitrumEmptyDefinition) + + '\n' + + arbitrumContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1648,6 +1782,7 @@ Future generatePubspec({ required bool hasDecred, required bool hasDogecoin, required bool hasBase, + required bool hasArbitrum, }) async { const cwCore = """ cw_core: @@ -1720,7 +1855,10 @@ Future generatePubspec({ cw_base: path: ./cw_base """; - + const cwArbitrum = """ + cw_arbitrum: + path: ./cw_arbitrum + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1794,6 +1932,10 @@ Future generatePubspec({ output += '\n$cwBase'; } + if (hasArbitrum) { + output += '\n$cwArbitrum'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -1821,6 +1963,7 @@ Future generateWalletTypes({ required bool hasDecred, required bool hasDogecoin, required bool hasBase, + required bool hasArbitrum, }) async { final walletTypesFile = File(walletTypesPath); @@ -1856,6 +1999,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.base,\n'; } + if (hasArbitrum) { + outputContent += '\tWalletType.arbitrum,\n'; + } + if (hasBitcoinCash) { outputContent += '\tWalletType.bitcoinCash,\n'; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index f6e501877..c02c59176 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -56,6 +56,7 @@ class SecretKey { SecretKey('bitcoinCashTestWalletSeeds', () => ''), SecretKey('polygonTestWalletSeeds', () => ''), SecretKey('baseTestWalletSeeds', () => ''), + SecretKey('arbitrumTestWalletSeeds', () => ''), SecretKey('solanaTestWalletSeeds', () => ''), SecretKey('tronTestWalletSeeds', () => ''), SecretKey('nanoTestWalletSeeds', () => ''), @@ -67,6 +68,7 @@ class SecretKey { SecretKey('bitcoinCashTestWalletReceiveAddress', () => ''), SecretKey('polygonTestWalletReceiveAddress', () => ''), SecretKey('baseTestWalletReceiveAddress', () => ''), + SecretKey('arbitrumTestWalletReceiveAddress', () => ''), SecretKey('solanaTestWalletReceiveAddress', () => ''), SecretKey('tronTestWalletReceiveAddress', () => ''), SecretKey('nanoTestWalletReceiveAddress', () => ''),