From 2a77d359f1732af665fb50c3ee8bfcbd59cd08f8 Mon Sep 17 00:00:00 2001 From: David Adegoke <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 16 Jan 2026 02:45:49 +0100 Subject: [PATCH] CW-1250: Refactor L2 Chains Code (#2677) * feat(evm): add ChainConfig model and related classes - Add ChainConfig class for immutable EVM chain configuration - Add ChainCapabilities class for chain feature flags - Add FeeType enum and FeeModel class for fee configuration * feat(evm): add EvmChainRegistry for centralized chain management - Add singleton EvmChainRegistry class with chain configuration storage - Initialize with Ethereum, Polygon, Base, and Arbitrum chains - Add mappings for WalletType, tag, and CAIP-2 to chainId lookups - Provide lookup methods for chain configurations by various identifiers - Support for querying available chains and registered chain IDs * feat(evm): add EVMChainClientFactory and consolidate chain clients - Create EVMChainClientFactory for creating chain-specific clients based on chainId - Move all chain clients (EthereumClient, PolygonClient, BaseClient, ArbitrumClient) into cw_evm/lib/clients/ folder - Refactor EVMChainClient from abstract to concrete class with default implementations - Add default implementations for fetchTransactions, fetchInternalTransactions, and prepareSignedTransactionForSending - Update all client classes to use super(chainId: X) constructor pattern - Factory returns EVMChainClient directly for unregistered chains instead of throwing - Remove dependencies on cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages from pubspec.yaml - Consolidate all EVM chain client implementations in one location for easier maintenance * add ui for evm switcher on eth * add inputs for evm network list edit switches * refactor(evm): Make EVMChainWallet concrete and unify transaction classes - Make EVMChainWallet and EVMChainWalletBase concrete classes (removed abstract) - Implement all previously abstract methods in EVMChainWallet - Make EVMChainTransactionInfo and EVMChainTransactionHistory concrete - Reorganize default tokens into tokens/ folder: - Refactor EVMChainDefaultTokens to import and use token classes from tokens/ folder - Move clients to clients/ folder: - Move evm_chain_formatter.dart to utils - Update chain-specific transaction history classes to pass walletType to fromJson - Update old wallet classes (EthereumWallet, PolygonWallet, etc.) to pass walletType - Maintain compatibility with existing per-chain wallet classes This refactoring enables the unified EVMChainWallet architecture while maintaining full backward compatibility with existing per-chain wallet implementations. * feat(evm): Add chain selection capability to EVMChainWallet - Add selectedChainId observable field to track currently selected chain - Add selectChain(chainId) action method to switch between EVM chains - Add selectedChainConfig computed getter for accessing chain configuration - Initialize selectedChainId from client.chainId in constructor - Add _getClientForCurrentChain() helper method for future use This enables chain switching functionality while maintaining full backward compatibility. The selectedChainId is initialized from the existing client, and all existing methods continue to work unchanged. Future increments will update methods to use selectedChainId internally. * refactor(evm): Update client immediately on chain selection - Update selectChain() to immediately create new client for selected chain - Remove _getClientForCurrentChain() helper method - Simplify all methods to use _client directly instead of helper calls - Client is now always in sync with selectedChainId This improves code simplicity and performance by eliminating repeated client checks. The client is updated synchronously when selectChain() is called, ensuring state consistency. * featr(evm): Add unified evm proxy setup and fix chain initialization - Add unified Evm proxy - Add evm.dart to .gitignore for generated proxy - Add generateEVM() to configure.dart for proxy generation - Fix selectedChainId to initialize from registry based on walletInfo.type Ensures selectedChainId always matches wallet type on initialization and adds unified proxy infrastructure. * refactor(evm): Update backward compat helpers to use registry via proxy - Add registry helper methods to unified EVM proxy - Update all lib/ files to use proxy instead of direct cw_evm imports - Enforce proxy pattern: no direct imports from cw_evm in main app - Update configure.dart to include registry methods in generated proxy Maintains backward compatibility while centralizing chain data access through the unified proxy pattern. * feat(evm): Add chain selection UI to dashboard - Add chain selection methods to DashboardViewModel (isEVMWallet, availableChains, currentChain, selectChain) - Add chain dropdown widget to balance page for EVM wallets - Dropdown shows current chain and allows switching between all registered EVM chains - Only visible for EVM-compatible wallets - Add chain selection methods to EVM proxy (getAllChains, getCurrentChain, selectChain) Users can now switch between EVM chains (Ethereum, Polygon, Base, Arbitrum) directly from the dashboard without creating separate wallets. All chain-related code follows the established proxy pattern. * feat(evm): Add persistence for selected chain ID - Add initialChainId parameter to EVMChainWallet constructor - Save selectedChainId to wallet JSON in toJSON() method - Load selectedChainId from JSON in open() method - Handle backward compatibility: wallets without saved chain ID use wallet type's default chain ID - Client is created with saved chain ID when opening wallet Selected chain preference is now persisted across app sessions. Users will see their previously selected chain when reopening wallets. * chore: Minor cleanup * refactor(evm): Remove old per-chain packages, consolidate to unified cw_evm - Delete cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages - Remove old proxy files (lib/ethereum/, lib/polygon/, lib/base/, lib/arbitrum/) - Update all view models to use unified evm! proxy - Move DEuro functionality to cw_evm/lib/deuro/ - Remove dependencies from pubspec.yaml and configure.dart - Remove walletType parameter from EVM proxy methods All EVM chains now use unified cw_evm package through single proxy interface. lib/ only depends on cw_evm, simplifying architecture and enabling easy addition of new L2 chains. BREAKING CHANGE: Old per-chain packages removed * refactor(evm): Update wallet handling to use unified EVM proxy - Replace individual chain imports (Ethereum, Polygon, Base, Arbitrum) with a single EVM proxy import. - Refactor wallet-related methods across various view models to utilize the new evm proxy for various usecases. This change enhances code maintainability and prepares the codebase for the addition of new EVM chains. * refactor(evm): Update model generator and enhance EVM classes - Modify model_generator.sh to remove previous chain folders from the loop. - Change EVMChainTransactionHistoryBase and EVMChainWalletBase classes to be abstract. - Update import paths in configure.dart to reflect the new structure of the cw_evm package. * add evm switcher icons to pubspec_base * change evm switcher to sliding animation * formatting fix * dpi agnostic width * feat(evm): Enhance chain handling and transaction history management - Introduce getCurrentChainId function to EVMChainTransactionHistoryBase for dynamic transaction history file naming based on the current chain ID. - Update EVMChainTransactionInfo to include chainId, ensuring accurate transaction data representation. - Refactor EVMChainWalletBase to support automatic node connection and transaction history loading upon chain selection. - Implement saveBackup and restoreWalletFilesFromBackup methods in EVMChainWalletService to manage wallet backups based on wallet type. - Update various methods across the codebase to accommodate the new chain handling logic, improving overall functionality and user experience. * chore: Cleanups and updates * feat: add WalletType.evm and deprecate individual EVM chain types Add unified WalletType.evm enum value for all EVM-compatible chains. Deprecate old individual EVM types (ethereum, polygon, base, arbitrum) with guidance to use WalletType.evm for new code. Update serialization/deserialization functions to support WalletType.evm: - serializeToInt() returns 18 for WalletType.evm - deserializeFromInt() handles case 18 - walletTypeToString() and walletTypeToDisplayName() return 'EVM' - cryptoCurrencyToWalletType() maps all EVM currencies to WalletType.evm Old EVM types remain functional for backward compatibility with existing wallets. This is the foundation for unifying EVM chain management. * feat: update EVM detection functions to support WalletType.evm Update all EVM detection and chain helper functions to support the unified WalletType.evm while maintaining backward compatibility with old EVM types. All functions require chainId parameter when used with WalletType.evm, ensuring proper chain-specific behavior. Old EVM types continue to work without changes for backward compatibility. * feat: add currency mapping support for unified EVM wallet type - Update currency mapping to support WalletType.evm with chainId-based currency resolution. - Override EVMChainWallet.currency getter to use selectedChainId from registry. - Replace walletTypeToCryptoCurrency calls with wallet.currency in view models. - Add WalletType.evm cases to switch statements and consolidate EVM transaction list item methods. - Add explorer URL method to EVM interface * feat: Update wallet operations with unified WalletType.evm - Update wallet service to always create new wallets and restores with WalletType.evm, defaulting to Ethereum. - Preserve old wallet types when opening existing wallets. Update wallet instance creation and opening logic to handle unified EVM type with chainId-based config lookup. * Add WalletType.evm support across all switch statements - Update all switch statements and conditionals to include WalletType.evm alongside existing EVM types. - Add chainId-based node selection support for unified EVM wallets. - Fix redundant code in exchange view model. - Ensure backward compatibility with old EVM wallet types. * feat(evm): Implement chainId-based node management for WalletType.evm - Add chainId-based node storage for unified EVM wallets, enabling separate node preferences per chain. - Replace unsafe dynamic casts with type-safe getSelectedChainId method. Update all node retrieval and switching logic to support chainId-based selection. * feat: Update UI components to show unified EVM wallet option - Filter out old EVM wallet types (ethereum, polygon, base, arbitrum) from wallet creation and restore UI, showing only unified WalletType.evm option. - Update wallet type generation to include WalletType.evm instead of individual chain types. - Update configure.dart to generate WalletType.evm in availableWalletTypes - Add filtering checks to prevent old EVM types from appearing * fix: Fix WalletType.evm support and complete remaining edge cases Add missing WalletType.evm cases to switch statements to resolve compilation errors. Complete remaining unification items: - Add WalletType.evm support to node, wallet utils, and view models - Update EVMChainDefaultTokens and EVMChainUtils to support chainId - Fix token initialization and priority fee calculations for WalletType.evm - Update payment view model to handle WalletType.evm with chainId All identified edge cases from the unification plan are now complete. * fix: Fix network switching issues for EVM wallets - Fix null check errors when creating and restoring an EVM wallet - Fix null check errors for balance getter when switching networks - Fix fiat amount display for native currencies in transaction history * refactor: Simplify EVM wallet chain selection in CryptoBalanceWidget - Removed the old dropdown for chain selection and replaced it with a more integrated EvmSwitcher dialog. - Updated EvmSwitcher to accept a list of available chains and handle chain selection more efficiently. - Enhanced EvmSwitcherDataItem to include chainId for better identification of chains. - Improved the overall UI flow for EVM wallet interactions. * refactor: Enhance EVM wallet handling and chainId integration - Simplified chainId checks across various components to ensure consistent handling for WalletType.evm. - Updated EVMChainDefaultTokens and EVMChainUtils checks to allow all EVM wallets for chainId operations - Handled backward compatibility for older wallet types in transaction info * refactor: More enhancement and backward compatibility fixes for EVM Wallet - Introduced a new method to retrieve cryptocurrency by chainId, improving the handling of WalletType.evm. - Updated transaction history management to filter transactions based on the current chainId - Refactored EVMChainTransactionInfo to accept chainId directly - Modified utility functions in EVMChainUtils and TokenUtilities for better chainId handling * refactor: Streamline EVM token retrieval and enhance chainId handling * feat: Introduce comprehensive guide for adding new EVM L2 networks - Added a detailed documentation file outlining the steps to integrate new EVM-compatible L2 networks into Cake Wallet. - Included prerequisites, architecture overview, and a step-by-step guide covering chain configuration, native currency addition, default tokens, and node setup. - Emphasized the unified architecture for EVM chains and backward compatibility considerations. - Removed the outdated guide on adding new L2 networks to streamline documentation. * feat: Reposition EVM switcher button on dashboard page * just adding reminders for the refactoring [skip ci] * more todos [skip ci] * feat: enhance wallet functionality with chainId support - Added chainId to `WalletBase` and updated currency method to use it. - Modified EvmChainRegistry to initialize registry on call. - Updated various components to pass chainId where necessary for currency conversions. - Improved handling of chain IDs in wallet-related view models and UI components for better compatibility with EVM networks. * fix: remove default chainId fallback in payment confirmation widget * feat: Adapt anypay to new unified flow WIP * fix: Issues with handling non evm swaps for anypay * fixes to flow structure * Add persistence to hidden chains on switcher * refactor: simplify equality operator in Erc20Token class * feat: add excluded arbutrum tokens for chainflip * chore: Remove print statement [skip ci] * refactor: dispaly QR image display for evm based on selected chain ID * feat: enhance EVM wallet compatibility check for raw address input * chore: Update doc to reflect changes [skip ci] * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Integrate Blink for EVM wallets for anti-sandwich attacks. - Add toggle in privacy settings to enable/disable - When enabled, route all evm broadcast rpc calls via Blink * feat: Handle same chainId scenario for pasted evm addresses * feat: Add Blink Setting to Advanced Settings Page * fix: Handle Blink supported chains * fix: Fixes to evm swap * fix: Add fallback for currency fetch * fix: Add evm to supported list * fix: Better handle balance errors * fix: Update token check to use contract address comparison, resolves the usdt/usdt0 issue * refactor: Simplify checks by cearing all tokens on chain switch, removing unnecessary currency checks * fix: Simplify checks based from feedback on review * fix: Exclude arbitrum from tx priority using chainId * refactor: Handle chains not having transaction priority within refactor * fix: Unified evm wallet fixes - Handle switching networks while on tx history page - Update NFTs when network is switching on listing page - Fix arbitrum tx priority error on swap screen - Switch check to display deuro from wallet type to chainId - Add fallback check for getting token info for evm - Better error message for max fee per gas less than block base fee error * refactor: Update wallet checks from type to chainId for EVM compatibility checks * refactor: Update eth chain utils and remove verified contract criteria in scam detection checks * refactor: Enhance validation for deuro calls * fix : Remove unneeded validation check for savings edit * feat: Hide EVM chain switcher behind feature flag * refactor: Remove EVM wallet type references and update related logic * chore: Clean up wallet type references and remove repeated imports * feat: Add fallback for derivation path and add arbitrum support to app scripts * fix: NFT popup error * fix: Transaction details fee regression * fix: Fixes and improvements - Add nonce parameter to transaction methods in EVM clients - Remove brackets for create swaps - Display contacts for selected wallettype alone - Remove tx priorty switching for arb * feat: Enhance Arbitrum support across swap providers * fix: Add missing secret keys * fix: display sending currency icon in send from external page * refactor: Handle arbitrum edgecase for wallet list display image * fix: Handle more cases of arbitrum image display for contact book * exploring possible issues with tx history * chore: switch to printV * fix: Add an extra check for keys * fix: Filter out eth spam transaction and modify parsing format * fix: Handle contract decimals for sending tokens * fix: Display proper balance error * Update cw_evm/lib/utils/evm_chain_formatter.dart [skip ci] --------- Co-authored-by: Robert Malikowski Co-authored-by: Omar Hatem --- .../workflows/automated_integration_test.yml | 1 - .github/workflows/pr_test_build_linux.yml | 2 +- .github/workflows/reusable-build.yml | 2 +- .gitignore | 1 + assets/images/evm_switcher.svg | 4 + assets/images/evm_switcher_arrow_left.svg | 3 + assets/images/evm_switcher_arrow_right.svg | 3 + assets/images/evm_switcher_checkmark.svg | 10 + assets/images/evm_switcher_icons/arbitrum.svg | 20 + assets/images/evm_switcher_icons/base.svg | 10 + assets/images/evm_switcher_icons/ethereum.svg | 3 + assets/images/evm_switcher_icons/gnosis.svg | 18 + assets/images/evm_switcher_icons/polygon.svg | 3 + cakewallet.bat | 2 +- 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 | 183 --- 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/.gitignore | 31 - cw_base/.metadata | 10 - cw_base/CHANGELOG.md | 3 - cw_base/LICENSE | 1 - cw_base/README.md | 39 - cw_base/analysis_options.yaml | 4 - cw_base/devtools_options.yaml | 3 - cw_base/lib/base_client.dart | 103 -- cw_base/lib/base_mnemonics_exception.dart | 5 - cw_base/lib/base_transaction_history.dart | 20 - cw_base/lib/base_transaction_info.dart | 41 - cw_base/lib/base_wallet.dart | 180 --- cw_base/lib/base_wallet_service.dart | 172 --- cw_base/lib/cw_base.dart | 5 - cw_base/lib/default_base_erc20_tokens.dart | 71 -- cw_base/pubspec.yaml | 73 -- cw_base/test/cw_base_test.dart | 12 - cw_core/lib/crypto_currency.dart | 4 +- cw_core/lib/currency_for_wallet_type.dart | 46 +- cw_core/lib/erc20_token.dart | 4 +- .../lib/hardware/device_connection_type.dart | 4 +- cw_core/lib/wallet_base.dart | 8 +- cw_core/lib/wallet_type.dart | 12 +- cw_ethereum/.gitignore | 30 - cw_ethereum/.metadata | 10 - cw_ethereum/CHANGELOG.md | 3 - cw_ethereum/LICENSE | 1 - cw_ethereum/README.md | 64 - cw_ethereum/analysis_options.yaml | 4 - cw_ethereum/lib/cw_ethereum.dart | 7 - .../lib/default_ethereum_erc20_tokens.dart | 863 -------------- cw_ethereum/lib/ethereum_client.dart | 76 -- .../lib/ethereum_mnemonics_exception.dart | 5 - .../lib/ethereum_transaction_history.dart | 19 - .../lib/ethereum_transaction_info.dart | 43 - cw_ethereum/lib/ethereum_wallet.dart | 199 ---- cw_ethereum/lib/ethereum_wallet_service.dart | 170 --- cw_ethereum/pubspec.yaml | 43 - cw_ethereum/test/cw_ethereum_test.dart | 12 - cw_evm/lib/clients/arbitrum_client.dart | 46 + cw_evm/lib/clients/base_client.dart | 46 + cw_evm/lib/clients/ethereum_client.dart | 13 + .../lib/{ => clients}/evm_chain_client.dart | 287 +++-- cw_evm/lib/clients/polygon_client.dart | 46 + .../lib/deuro/deuro_savings.dart | 9 +- .../lib/deuro/deuro_savings_contract.dart | 0 cw_evm/lib/evm_chain_client_factory.dart | 36 + cw_evm/lib/evm_chain_default_tokens.dart | 22 + cw_evm/lib/evm_chain_exceptions.dart | 5 +- cw_evm/lib/evm_chain_formatter.dart | 43 - cw_evm/lib/evm_chain_registry.dart | 203 ++++ .../evm_chain_transaction_credentials.dart | 2 + cw_evm/lib/evm_chain_transaction_history.dart | 64 +- cw_evm/lib/evm_chain_transaction_info.dart | 35 +- cw_evm/lib/evm_chain_transaction_model.dart | 5 +- cw_evm/lib/evm_chain_wallet.dart | 552 +++++++-- cw_evm/lib/evm_chain_wallet_service.dart | 381 +++++- cw_evm/lib/tokens/arbitrum_tokens.dart | 83 ++ cw_evm/lib/tokens/base_tokens.dart | 76 ++ cw_evm/lib/tokens/ethereum_tokens.dart | 97 ++ cw_evm/lib/tokens/polygon_tokens.dart | 90 ++ cw_evm/lib/utils/evm_chain_formatter.dart | 57 + cw_evm/lib/utils/evm_chain_utils.dart | 115 ++ cw_evm/lib/utils/network_chain_utils.dart | 63 + cw_evm/test/cw_evm_test.dart | 2 +- cw_polygon/.gitignore | 30 - cw_polygon/.metadata | 10 - cw_polygon/CHANGELOG.md | 3 - cw_polygon/LICENSE | 1 - cw_polygon/README.md | 63 - cw_polygon/analysis_options.yaml | 4 - cw_polygon/devtools_options.yaml | 3 - cw_polygon/lib/cw_polygon.dart | 7 - .../lib/default_polygon_erc20_tokens.dart | 93 -- cw_polygon/lib/polygon_client.dart | 103 -- .../lib/polygon_mnemonics_exception.dart | 5 - .../lib/polygon_transaction_history.dart | 20 - cw_polygon/lib/polygon_transaction_info.dart | 41 - cw_polygon/lib/polygon_wallet.dart | 198 ---- cw_polygon/lib/polygon_wallet_service.dart | 169 --- cw_polygon/pubspec.yaml | 75 -- cw_polygon/test/cw_polygon_test.dart | 12 - docs/ADDING_EVM_L2_WALLET_NETWORK_TYPES.md | 589 ++++++++++ docs/ADDING_NEW_WALLET_TYPES.md | 1033 +++++++++++++++++ docs/NEW_WALLET_TYPES.md | 301 ----- lib/arbitrum/cw_arbitrum.dart | 244 ---- lib/base/cw_base.dart | 258 ---- lib/buy/moonpay/moonpay_provider.dart | 2 +- lib/buy/wyre/wyre_buy_provider.dart | 4 +- lib/core/address_validator.dart | 1 + lib/core/background_sync.dart | 67 +- lib/core/node_switching_service.dart | 39 +- lib/core/seed_validator.dart | 16 +- lib/core/trade_monitor.dart | 2 - lib/core/universal_address_detector.dart | 14 +- lib/di.dart | 16 +- lib/entities/ens_record.dart | 24 +- lib/entities/node_list.dart | 1 + lib/entities/preferences_key.dart | 2 + lib/entities/priority_for_wallet_type.dart | 13 +- lib/entities/wallet_contact.dart | 8 +- lib/ethereum/cw_ethereum.dart | 283 ----- lib/evm/cw_evm.dart | 516 ++++++++ .../provider/chainflip_exchange_provider.dart | 2 +- .../provider/changenow_exchange_provider.dart | 2 + .../provider/exolix_exchange_provider.dart | 2 + .../letsexchange_exchange_provider.dart | 2 + .../stealth_ex_exchange_provider.dart | 19 +- .../provider/swapsxyz_exchange_provider.dart | 1 + .../provider/trocador_exchange_provider.dart | 54 +- .../provider/xoswap_exchange_provider.dart | 2 + lib/exchange/trade.dart | 31 +- lib/polygon/cw_polygon.dart | 263 ----- lib/reactions/check_connection.dart | 14 +- lib/reactions/fiat_rate_update.dart | 24 +- lib/reactions/on_current_wallet_change.dart | 32 +- lib/reactions/wallet_connect.dart | 92 +- .../select_hardware_wallet_account_page.dart | 4 +- lib/src/screens/dashboard/dashboard_page.dart | 33 +- .../screens/dashboard/pages/address_page.dart | 69 +- .../dashboard/pages/balance/balance_page.dart | 2 +- .../pages/balance/crypto_balance_widget.dart | 15 + .../dashboard/pages/cake_features_page.dart | 2 +- lib/src/screens/dashboard/sign_page.dart | 2 + .../screens/dashboard/widgets/sign_form.dart | 6 +- .../screens/dashboard/widgets/trade_row.dart | 63 +- .../dashboard/widgets/verify_form.dart | 6 +- .../exchange_trade_external_send_page.dart | 4 +- .../advanced_privacy_settings_page.dart | 8 + .../new_wallet/new_wallet_type_page.dart | 2 +- .../new_wallet/wallet_group_display_page.dart | 2 +- .../grouped_wallet_expansion_tile.dart | 2 +- lib/src/screens/send/widgets/send_card.dart | 178 ++- lib/src/screens/settings/privacy_page.dart | 8 + .../chain_service/eth/evm_chain_id.dart | 40 +- .../chain_service/eth/evm_chain_service.dart | 18 +- .../wallet_connect_key_service.dart | 22 +- .../services/walletkit_service.dart | 61 +- .../bottom_sheet_listener_widget.dart | 128 +- .../screens/wallet_list/wallet_list_page.dart | 31 +- .../evm_payment_flow_bottom_sheet.dart | 116 +- .../payment_confirmation_bottom_sheet.dart | 82 +- .../swap_confirmation_bottom_sheet.dart | 23 +- .../swap_details_bottom_sheet.dart | 81 +- .../wallet_switcher_bottom_sheet.dart | 10 +- lib/src/widgets/evm_switcher.dart | 376 ++++++ lib/src/widgets/evm_switcher_row.dart | 71 ++ lib/store/dashboard/trade_filter_store.dart | 5 +- lib/store/settings_store.dart | 260 +++-- lib/utils/feature_flag.dart | 1 + lib/utils/qr_util.dart | 47 +- lib/utils/token_utilities.dart | 175 +-- .../advanced_privacy_settings_view_model.dart | 17 + .../anon_invoice_page_view_model.dart | 17 +- lib/view_model/buy/buy_view_model.dart | 24 +- .../contact_list/contact_list_view_model.dart | 29 +- .../dashboard/balance_view_model.dart | 30 +- .../dashboard/dashboard_view_model.dart | 119 +- .../dashboard/home_settings_view_model.dart | 192 +-- lib/view_model/dashboard/nft_view_model.dart | 41 +- .../dashboard/transaction_list_item.dart | 55 +- .../exchange/exchange_trade_view_model.dart | 7 +- .../exchange/exchange_view_model.dart | 24 +- .../hardware_wallet/bitbox_view_model.dart | 9 +- .../hardware_wallet/ledger_view_model.dart | 9 +- .../hardware_wallet/trezor_view_model.dart | 9 +- .../integrations/deuro_view_model.dart | 74 +- .../node_list/node_list_view_model.dart | 93 +- .../payment/payment_view_model.dart | 221 +++- .../restore/wallet_restore_from_qr_code.dart | 23 +- lib/view_model/send/fees_view_model.dart | 61 +- lib/view_model/send/output.dart | 54 +- .../send/send_template_view_model.dart | 19 +- lib/view_model/send/send_view_model.dart | 250 ++-- .../settings/other_settings_view_model.dart | 43 +- .../settings/privacy_settings_view_model.dart | 30 +- .../transaction_details_view_model.dart | 204 +--- .../wallet_address_list_view_model.dart | 142 ++- .../wallet_hardware_restore_view_model.dart | 11 +- lib/view_model/wallet_keys_view_model.dart | 23 +- lib/view_model/wallet_new_vm.dart | 34 +- lib/view_model/wallet_restore_view_model.dart | 100 +- lib/view_model/wallet_seed_view_model.dart | 13 +- .../wallet_switcher_view_model.dart | 9 +- model_generator.sh | 4 +- pubspec_base.yaml | 1 + res/values/strings_ar.arb | 2 + res/values/strings_bg.arb | 2 + res/values/strings_cs.arb | 2 + res/values/strings_de.arb | 4 +- res/values/strings_en.arb | 2 + res/values/strings_es.arb | 2 + res/values/strings_fr.arb | 2 + res/values/strings_gn.arb | 2 + res/values/strings_ha.arb | 2 + res/values/strings_hi.arb | 2 + res/values/strings_hr.arb | 2 + res/values/strings_hy.arb | 2 + res/values/strings_id.arb | 2 + res/values/strings_it.arb | 2 + res/values/strings_ja.arb | 2 + res/values/strings_ko.arb | 2 + res/values/strings_my.arb | 2 + res/values/strings_nl.arb | 2 + res/values/strings_pl.arb | 2 + res/values/strings_pt.arb | 2 + res/values/strings_ru.arb | 2 + res/values/strings_th.arb | 2 + res/values/strings_tl.arb | 2 + res/values/strings_tr.arb | 2 + res/values/strings_uk.arb | 2 + res/values/strings_ur.arb | 2 + res/values/strings_vi.arb | 2 + res/values/strings_yo.arb | 2 + res/values/strings_zh.arb | 2 + 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 | 659 +++-------- tool/utils/secret_key.dart | 1 + 251 files changed, 7333 insertions(+), 7788 deletions(-) create mode 100644 assets/images/evm_switcher.svg create mode 100644 assets/images/evm_switcher_arrow_left.svg create mode 100644 assets/images/evm_switcher_arrow_right.svg create mode 100644 assets/images/evm_switcher_checkmark.svg create mode 100644 assets/images/evm_switcher_icons/arbitrum.svg create mode 100644 assets/images/evm_switcher_icons/base.svg create mode 100644 assets/images/evm_switcher_icons/ethereum.svg create mode 100644 assets/images/evm_switcher_icons/gnosis.svg create mode 100644 assets/images/evm_switcher_icons/polygon.svg delete mode 100644 cw_arbitrum/.gitignore delete mode 100644 cw_arbitrum/.metadata delete mode 100644 cw_arbitrum/CHANGELOG.md delete mode 100644 cw_arbitrum/LICENSE delete mode 100644 cw_arbitrum/README.md delete mode 100644 cw_arbitrum/analysis_options.yaml delete mode 100644 cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart delete mode 100644 cw_arbitrum/lib/arbitrum_client.dart delete mode 100644 cw_arbitrum/lib/arbitrum_transaction_history.dart delete mode 100644 cw_arbitrum/lib/arbitrum_transaction_info.dart delete mode 100644 cw_arbitrum/lib/arbitrum_wallet.dart delete mode 100644 cw_arbitrum/lib/arbitrum_wallet_service.dart delete mode 100644 cw_arbitrum/lib/cw_arbitrum.dart delete mode 100644 cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart delete mode 100644 cw_arbitrum/pubspec.yaml delete mode 100644 cw_arbitrum/test/cw_arbitrum_test.dart delete mode 100644 cw_base/.gitignore delete mode 100644 cw_base/.metadata delete mode 100644 cw_base/CHANGELOG.md delete mode 100644 cw_base/LICENSE delete mode 100644 cw_base/README.md delete mode 100644 cw_base/analysis_options.yaml delete mode 100644 cw_base/devtools_options.yaml delete mode 100644 cw_base/lib/base_client.dart delete mode 100644 cw_base/lib/base_mnemonics_exception.dart delete mode 100644 cw_base/lib/base_transaction_history.dart delete mode 100644 cw_base/lib/base_transaction_info.dart delete mode 100644 cw_base/lib/base_wallet.dart delete mode 100644 cw_base/lib/base_wallet_service.dart delete mode 100644 cw_base/lib/cw_base.dart delete mode 100644 cw_base/lib/default_base_erc20_tokens.dart delete mode 100644 cw_base/pubspec.yaml delete mode 100644 cw_base/test/cw_base_test.dart delete mode 100644 cw_ethereum/.gitignore delete mode 100644 cw_ethereum/.metadata delete mode 100644 cw_ethereum/CHANGELOG.md delete mode 100644 cw_ethereum/LICENSE delete mode 100644 cw_ethereum/README.md delete mode 100644 cw_ethereum/analysis_options.yaml delete mode 100644 cw_ethereum/lib/cw_ethereum.dart delete mode 100644 cw_ethereum/lib/default_ethereum_erc20_tokens.dart delete mode 100644 cw_ethereum/lib/ethereum_client.dart delete mode 100644 cw_ethereum/lib/ethereum_mnemonics_exception.dart delete mode 100644 cw_ethereum/lib/ethereum_transaction_history.dart delete mode 100644 cw_ethereum/lib/ethereum_transaction_info.dart delete mode 100644 cw_ethereum/lib/ethereum_wallet.dart delete mode 100644 cw_ethereum/lib/ethereum_wallet_service.dart delete mode 100644 cw_ethereum/pubspec.yaml delete mode 100644 cw_ethereum/test/cw_ethereum_test.dart create mode 100644 cw_evm/lib/clients/arbitrum_client.dart create mode 100644 cw_evm/lib/clients/base_client.dart create mode 100644 cw_evm/lib/clients/ethereum_client.dart rename cw_evm/lib/{ => clients}/evm_chain_client.dart (60%) create mode 100644 cw_evm/lib/clients/polygon_client.dart rename {cw_ethereum => cw_evm}/lib/deuro/deuro_savings.dart (97%) rename {cw_ethereum => cw_evm}/lib/deuro/deuro_savings_contract.dart (100%) create mode 100644 cw_evm/lib/evm_chain_client_factory.dart create mode 100644 cw_evm/lib/evm_chain_default_tokens.dart delete mode 100644 cw_evm/lib/evm_chain_formatter.dart create mode 100644 cw_evm/lib/evm_chain_registry.dart create mode 100644 cw_evm/lib/tokens/arbitrum_tokens.dart create mode 100644 cw_evm/lib/tokens/base_tokens.dart create mode 100644 cw_evm/lib/tokens/ethereum_tokens.dart create mode 100644 cw_evm/lib/tokens/polygon_tokens.dart create mode 100644 cw_evm/lib/utils/evm_chain_formatter.dart create mode 100644 cw_evm/lib/utils/evm_chain_utils.dart create mode 100644 cw_evm/lib/utils/network_chain_utils.dart delete mode 100644 cw_polygon/.gitignore delete mode 100644 cw_polygon/.metadata delete mode 100644 cw_polygon/CHANGELOG.md delete mode 100644 cw_polygon/LICENSE delete mode 100644 cw_polygon/README.md delete mode 100644 cw_polygon/analysis_options.yaml delete mode 100644 cw_polygon/devtools_options.yaml delete mode 100644 cw_polygon/lib/cw_polygon.dart delete mode 100644 cw_polygon/lib/default_polygon_erc20_tokens.dart delete mode 100644 cw_polygon/lib/polygon_client.dart delete mode 100644 cw_polygon/lib/polygon_mnemonics_exception.dart delete mode 100644 cw_polygon/lib/polygon_transaction_history.dart delete mode 100644 cw_polygon/lib/polygon_transaction_info.dart delete mode 100644 cw_polygon/lib/polygon_wallet.dart delete mode 100644 cw_polygon/lib/polygon_wallet_service.dart delete mode 100644 cw_polygon/pubspec.yaml delete mode 100644 cw_polygon/test/cw_polygon_test.dart create mode 100644 docs/ADDING_EVM_L2_WALLET_NETWORK_TYPES.md create mode 100644 docs/ADDING_NEW_WALLET_TYPES.md delete mode 100644 docs/NEW_WALLET_TYPES.md delete mode 100644 lib/arbitrum/cw_arbitrum.dart delete mode 100644 lib/base/cw_base.dart delete mode 100644 lib/ethereum/cw_ethereum.dart create mode 100644 lib/evm/cw_evm.dart delete mode 100644 lib/polygon/cw_polygon.dart create mode 100644 lib/src/widgets/evm_switcher.dart create mode 100644 lib/src/widgets/evm_switcher_row.dart diff --git a/.github/workflows/automated_integration_test.yml b/.github/workflows/automated_integration_test.yml index ac0fa6f2b..96d6ef153 100644 --- a/.github/workflows/automated_integration_test.yml +++ b/.github/workflows/automated_integration_test.yml @@ -66,7 +66,6 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart - echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 7444be1cc..c3e7ecd88 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -53,8 +53,8 @@ jobs: echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart + echo "const blinkApiKey = '${{ secrets.BLINK_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart - echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index b18495263..08bf24d67 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -68,7 +68,7 @@ jobs: echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const nowNodesApiKey = '${{ secrets.EVM_NOWNODES_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart - + echo "const blinkApiKey = '${{ secrets.BLINK_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const chainStackApiKey = '${{ secrets.CHAIN_STACK_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart diff --git a/.gitignore b/.gitignore index cbe622744..5b17c0122 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,7 @@ lib/decred/decred.dart lib/dogecoin/dogecoin.dart lib/base/base.dart lib/arbitrum/arbitrum.dart +lib/evm/evm.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ios/Runner/Assets.xcassets/AppIcon.appiconset/*.png diff --git a/assets/images/evm_switcher.svg b/assets/images/evm_switcher.svg new file mode 100644 index 000000000..18ed0cf03 --- /dev/null +++ b/assets/images/evm_switcher.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/evm_switcher_arrow_left.svg b/assets/images/evm_switcher_arrow_left.svg new file mode 100644 index 000000000..dd12ab3ca --- /dev/null +++ b/assets/images/evm_switcher_arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/evm_switcher_arrow_right.svg b/assets/images/evm_switcher_arrow_right.svg new file mode 100644 index 000000000..e73ba2a6b --- /dev/null +++ b/assets/images/evm_switcher_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/evm_switcher_checkmark.svg b/assets/images/evm_switcher_checkmark.svg new file mode 100644 index 000000000..a634d871a --- /dev/null +++ b/assets/images/evm_switcher_checkmark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/evm_switcher_icons/arbitrum.svg b/assets/images/evm_switcher_icons/arbitrum.svg new file mode 100644 index 000000000..c0720257f --- /dev/null +++ b/assets/images/evm_switcher_icons/arbitrum.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/evm_switcher_icons/base.svg b/assets/images/evm_switcher_icons/base.svg new file mode 100644 index 000000000..6d51c92f9 --- /dev/null +++ b/assets/images/evm_switcher_icons/base.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/images/evm_switcher_icons/ethereum.svg b/assets/images/evm_switcher_icons/ethereum.svg new file mode 100644 index 000000000..b011052de --- /dev/null +++ b/assets/images/evm_switcher_icons/ethereum.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/evm_switcher_icons/gnosis.svg b/assets/images/evm_switcher_icons/gnosis.svg new file mode 100644 index 000000000..d9347d66a --- /dev/null +++ b/assets/images/evm_switcher_icons/gnosis.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/assets/images/evm_switcher_icons/polygon.svg b/assets/images/evm_switcher_icons/polygon.svg new file mode 100644 index 000000000..65335cf6a --- /dev/null +++ b/assets/images/evm_switcher_icons/polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/cakewallet.bat b/cakewallet.bat index 5870452b8..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% diff --git a/cw_arbitrum/.gitignore b/cw_arbitrum/.gitignore deleted file mode 100644 index eb6c05cd3..000000000 --- a/cw_arbitrum/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# 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 deleted file mode 100644 index 6f8d1aa9c..000000000 --- a/cw_arbitrum/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index 41cc7d819..000000000 --- a/cw_arbitrum/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/cw_arbitrum/LICENSE b/cw_arbitrum/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/cw_arbitrum/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/cw_arbitrum/README.md b/cw_arbitrum/README.md deleted file mode 100644 index 4a260d8d2..000000000 --- a/cw_arbitrum/README.md +++ /dev/null @@ -1,39 +0,0 @@ - - -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 deleted file mode 100644 index a5744c1cf..000000000 --- a/cw_arbitrum/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 8af5612db..000000000 --- a/cw_arbitrum/lib/arbirtrum_mnemonics_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index f9fb36d34..000000000 --- a/cw_arbitrum/lib/arbitrum_client.dart +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index 1319c7aea..000000000 --- a/cw_arbitrum/lib/arbitrum_transaction_history.dart +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index d12499991..000000000 --- a/cw_arbitrum/lib/arbitrum_transaction_info.dart +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 39680566f..000000000 --- a/cw_arbitrum/lib/arbitrum_wallet.dart +++ /dev/null @@ -1,183 +0,0 @@ -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_transaction_priority.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 - int getTotalPriorityFee(EVMChainTransactionPriority priority) => 0; - - @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 deleted file mode 100644 index 91e0b86ee..000000000 --- a/cw_arbitrum/lib/arbitrum_wallet_service.dart +++ /dev/null @@ -1,174 +0,0 @@ -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 deleted file mode 100644 index 298576d81..000000000 --- a/cw_arbitrum/lib/cw_arbitrum.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// 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 deleted file mode 100644 index 25cc5739b..000000000 --- a/cw_arbitrum/lib/default_arbitrum_erc20_tokens.dart +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index f6cb36577..000000000 --- a/cw_arbitrum/pubspec.yaml +++ /dev/null @@ -1,73 +0,0 @@ -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 deleted file mode 100644 index 5cf35628f..000000000 --- a/cw_arbitrum/test/cw_arbitrum_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -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/.gitignore b/cw_base/.gitignore deleted file mode 100644 index eb6c05cd3..000000000 --- a/cw_base/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# 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_base/.metadata b/cw_base/.metadata deleted file mode 100644 index 6f8d1aa9c..000000000 --- a/cw_base/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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_base/CHANGELOG.md b/cw_base/CHANGELOG.md deleted file mode 100644 index 41cc7d819..000000000 --- a/cw_base/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/cw_base/LICENSE b/cw_base/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/cw_base/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/cw_base/README.md b/cw_base/README.md deleted file mode 100644 index 4a260d8d2..000000000 --- a/cw_base/README.md +++ /dev/null @@ -1,39 +0,0 @@ - - -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_base/analysis_options.yaml b/cw_base/analysis_options.yaml deleted file mode 100644 index a5744c1cf..000000000 --- a/cw_base/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -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_base/devtools_options.yaml b/cw_base/devtools_options.yaml deleted file mode 100644 index fa0b357c4..000000000 --- a/cw_base/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/cw_base/lib/base_client.dart b/cw_base/lib/base_client.dart deleted file mode 100644 index 4cf7bc910..000000000 --- a/cw_base/lib/base_client.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_evm/evm_chain_client.dart'; -import 'package:cw_evm/.secrets.g.dart' as secrets; -import 'package:cw_evm/evm_chain_transaction_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:web3dart/web3dart.dart'; - -class BaseClient 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 => 8453; - - @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_base/lib/base_mnemonics_exception.dart b/cw_base/lib/base_mnemonics_exception.dart deleted file mode 100644 index 83f8403d2..000000000 --- a/cw_base/lib/base_mnemonics_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class BaseMnemonicIsIncorrectException implements Exception { - @override - String toString() => - 'Base mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; -} diff --git a/cw_base/lib/base_transaction_history.dart b/cw_base/lib/base_transaction_history.dart deleted file mode 100644 index 42d93a1e0..000000000 --- a/cw_base/lib/base_transaction_history.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:core'; - -import 'package:cw_evm/evm_chain_transaction_history.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; -import 'package:cw_base/base_transaction_info.dart'; - -class BaseTransactionHistory extends EVMChainTransactionHistory { - BaseTransactionHistory({ - required super.walletInfo, - required super.password, - required super.encryptionFileUtils, - }); - - @override - String getTransactionHistoryFileName() => 'base_transactions.json'; - - @override - EVMChainTransactionInfo getTransactionInfo(Map val) => - BaseTransactionInfo.fromJson(val); -} diff --git a/cw_base/lib/base_transaction_info.dart b/cw_base/lib/base_transaction_info.dart deleted file mode 100644 index 858c4fce7..000000000 --- a/cw_base/lib/base_transaction_info.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; - -class BaseTransactionInfo extends EVMChainTransactionInfo { - BaseTransactionInfo({ - 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 BaseTransactionInfo.fromJson(Map data) { - return BaseTransactionInfo( - 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_base/lib/base_wallet.dart b/cw_base/lib/base_wallet.dart deleted file mode 100644 index a54ace643..000000000 --- a/cw_base/lib/base_wallet.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_base/base_client.dart'; -import 'package:cw_base/base_transaction_history.dart'; -import 'package:cw_base/base_transaction_info.dart'; -import 'package:cw_base/default_base_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_transaction_priority.dart'; -import 'package:cw_evm/evm_chain_wallet.dart'; -import 'package:cw_evm/evm_erc20_balance.dart'; -import 'package:web3dart/web3dart.dart'; - -class BaseWallet extends EVMChainWallet { - BaseWallet({ - 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.baseEth); - - @override - int getTotalPriorityFee(EVMChainTransactionPriority priority) { - return switch (priority) { - EVMChainTransactionPriority.fast => EtherAmount.fromInt(EtherUnit.mwei, 5).getInWei.toInt(), - EVMChainTransactionPriority.medium => EtherAmount.fromInt(EtherUnit.mwei, 3).getInWei.toInt(), - EVMChainTransactionPriority.slow => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), - _ => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), - }; - } - - @override - Future initErc20TokensBox() async { - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.baseBoxName}"; - - evmChainErc20TokensBox = await CakeHive.openBox(boxName); - } - - @override - void addInitialTokens() { - final initialErc20Tokens = DefaultBaseErc20Tokens().initialBaseErc20Tokens; - - 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 => - DefaultBaseErc20Tokens().initialBaseErc20Tokens.map((e) => e.contractAddress).toList(); - - @override - Future checkIfScanProviderIsEnabled() async { - return (await sharedPrefs.future).getBool("use_basescan") ?? true; - } - - @override - String getTransactionHistoryFileName() => 'base_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 = BaseTransactionInfo( - 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 BaseTransactionHistory( - 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 BaseWallet( - walletInfo: walletInfo, - derivationInfo: await walletInfo.getDerivationInfo(), - password: password, - mnemonic: keysData.mnemonic, - privateKey: keysData.privateKey, - passphrase: keysData.passphrase, - initialBalance: balance, - client: BaseClient(), - encryptionFileUtils: encryptionFileUtils, - ); - } -} diff --git a/cw_base/lib/base_wallet_service.dart b/cw_base/lib/base_wallet_service.dart deleted file mode 100644 index 7457cbd58..000000000 --- a/cw_base/lib/base_wallet_service.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:bip39/bip39.dart' as bip39; -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_base/base_wallet.dart'; -import 'package:cw_base/base_client.dart'; -import 'package:cw_base/base_mnemonics_exception.dart'; - -class BaseWalletService extends EVMChainWalletService { - BaseWalletService( - super.isDirect, { - required this.client, - }); - - late BaseClient client; - - @override - WalletType getType() => WalletType.base; - - @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 = BaseWallet( - 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 BaseWallet.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 BaseWallet.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 = BaseWallet( - 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 = BaseWallet( - 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 BaseMnemonicIsIncorrectException(); - } - - final wallet = BaseWallet( - 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 BaseWallet.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_base/lib/cw_base.dart b/cw_base/lib/cw_base.dart deleted file mode 100644 index 298576d81..000000000 --- a/cw_base/lib/cw_base.dart +++ /dev/null @@ -1,5 +0,0 @@ -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} diff --git a/cw_base/lib/default_base_erc20_tokens.dart b/cw_base/lib/default_base_erc20_tokens.dart deleted file mode 100644 index 967718373..000000000 --- a/cw_base/lib/default_base_erc20_tokens.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; - -class DefaultBaseErc20Tokens { - final List _defaultTokens = [ - Erc20Token( - name: "USD Coin", - symbol: "USDC", - contractAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "USDe", - symbol: "USDe", - contractAddress: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "Dai", - symbol: "DAI", - contractAddress: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "Bridged Tether USD", - symbol: "USDT", - contractAddress: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "Wrapped Ether", - symbol: "WETH", - contractAddress: "0x4200000000000000000000000000000000000006", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Wrapped BTC", - symbol: "WBTC", - contractAddress: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "SPX6900", - symbol: "SPX", - contractAddress: "0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C", - decimal: 8, - enabled: false, - ), - ]; - - List get initialBaseErc20Tokens => _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: 'BASE'); - }).toList(); -} diff --git a/cw_base/pubspec.yaml b/cw_base/pubspec.yaml deleted file mode 100644 index f94468bce..000000000 --- a/cw_base/pubspec.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: cw_base -description: "Base package for Cake Wallet" -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_base/test/cw_base_test.dart b/cw_base/test/cw_base_test.dart deleted file mode 100644 index 31f598069..000000000 --- a/cw_base/test/cw_base_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:cw_base/cw_base.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_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index d3f9faf22..b581e803f 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -250,7 +250,8 @@ 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 const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Ethereum (Arbitrum One)', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); + static const usdcArb = CryptoCurrency(title: 'USDC', tag: 'ARB', fullName: 'USDC Coin', raw: 107, name: 'usdcarb', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static final Map _rawCurrencyMap = [...all, ...havenCurrencies].fold>({}, (acc, item) { @@ -289,6 +290,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen 'shibainu': shib, 'zcash': zec, 'base': baseEth, + 'arbitrum': arbEth, }; static CryptoCurrency deserialize({required int raw}) { diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index fe6899c4b..e09abedf3 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -1,7 +1,11 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; -CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = false}) { +CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = false, int? chainId}) { + if (chainId != null) { + return getCryptoCurrencyByChainId(chainId); + } + switch (type) { case WalletType.monero: return CryptoCurrency.xmr; @@ -45,3 +49,43 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } } + +CryptoCurrency getCryptoCurrencyByChainId(int chainId) { + switch (chainId) { + case 1: + return CryptoCurrency.eth; + case 137: + return CryptoCurrency.maticpoly; + case 8453: + return CryptoCurrency.baseEth; + case 42161: + return CryptoCurrency.arbEth; + default: + return CryptoCurrency.eth; + } +} + +/// Get chainId from CryptoCurrency for EVM chains +/// Returns null if currency is not an EVM chain +int? getChainIdByCryptoCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.eth: + return 1; + case CryptoCurrency.maticpoly: + return 137; + case CryptoCurrency.baseEth: + return 8453; + case CryptoCurrency.arbEth: + return 42161; + default: + return null; + } +} + +CryptoCurrency getCryptoCurrencyForWalletListItem(WalletType type, {bool isTestnet = false, int? chainId}) { + if (type == WalletType.arbitrum) { + return CryptoCurrency.arb; + } + + return walletTypeToCryptoCurrency(type, isTestnet: isTestnet, chainId: chainId); +} diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart index cbc42adcb..841be33c2 100644 --- a/cw_core/lib/erc20_token.dart +++ b/cw_core/lib/erc20_token.dart @@ -74,9 +74,7 @@ class Erc20Token extends CryptoCurrency with HiveObjectMixin { static const arbitrumBoxName = 'ArbitrumErc20Tokens'; @override - bool operator ==(other) => - (other is Erc20Token && other.contractAddress == contractAddress) || - (other is CryptoCurrency && other.title == title); + bool operator ==(Object other) => other is Erc20Token && other.contractAddress == contractAddress; @override int get hashCode => contractAddress.hashCode; diff --git a/cw_core/lib/hardware/device_connection_type.dart b/cw_core/lib/hardware/device_connection_type.dart index 76f07edf1..74f043a81 100644 --- a/cw_core/lib/hardware/device_connection_type.dart +++ b/cw_core/lib/hardware/device_connection_type.dart @@ -15,7 +15,7 @@ enum DeviceConnectionType { WalletType.bitcoin, // WalletType.litecoin, WalletType.ethereum, - WalletType.polygon + WalletType.polygon, ].contains(walletType); break; case HardwareWalletType.ledger: @@ -24,7 +24,7 @@ enum DeviceConnectionType { WalletType.bitcoin, WalletType.litecoin, WalletType.ethereum, - WalletType.polygon + WalletType.polygon, ].contains(walletType); break; case HardwareWalletType.trezor: diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 0a8a00bd8..e47760846 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -25,7 +25,13 @@ abstract class WalletBase walletInfo.type; - CryptoCurrency get currency => walletTypeToCryptoCurrency(type, isTestnet: isTestnet); + int? get chainId => null; + + CryptoCurrency get currency => walletTypeToCryptoCurrency( + type, + chainId: chainId, + isTestnet: isTestnet, + ); String get id => walletInfo.id; diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 5ef50ee78..1f8af52ac 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -257,14 +257,18 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.haven; case CryptoCurrency.eth: return WalletType.ethereum; + case CryptoCurrency.maticpoly: + return WalletType.polygon; + case CryptoCurrency.baseEth: + return WalletType.base; + case CryptoCurrency.arbEth: + return WalletType.arbitrum; case CryptoCurrency.bch: return WalletType.bitcoinCash; case CryptoCurrency.nano: return WalletType.nano; case CryptoCurrency.banano: return WalletType.banano; - case CryptoCurrency.maticpoly: - return WalletType.polygon; case CryptoCurrency.sol: return WalletType.solana; case CryptoCurrency.trx: @@ -277,10 +281,6 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.decred; case CryptoCurrency.doge: return WalletType.dogecoin; - case CryptoCurrency.baseEth: - return WalletType.base; - case CryptoCurrency.arbEth: - return WalletType.arbitrum; default: return null; } diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore deleted file mode 100644 index 96486fd93..000000000 --- a/cw_ethereum/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.packages -build/ diff --git a/cw_ethereum/.metadata b/cw_ethereum/.metadata deleted file mode 100644 index 1e05dac7f..000000000 --- a/cw_ethereum/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: eb6d86ee27deecba4a83536aa20f366a6044895c - channel: stable - -project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md deleted file mode 100644 index 41cc7d819..000000000 --- a/cw_ethereum/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/cw_ethereum/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md deleted file mode 100644 index 70a350bbf..000000000 --- a/cw_ethereum/README.md +++ /dev/null @@ -1,64 +0,0 @@ -## cw_ethereum - -Ethereum wallet module built on `cw_evm`. Supports native ETH and ERC‑20 tokens, with history fetched via Etherscan. - -### Features - -- EVM client specialized for Ethereum mainnet (chainId 1). -- Default ERC‑20 token list and wallet‑scoped token storage/migration. -- History via Etherscan v2 API (external, internal, and token transfers). -- EIP‑1559 fee support; gas estimation per transaction intent. -- Create/sign native and ERC‑20 transfers; approvals; broadcast. -- Manage ERC‑20 tokens and balances; metadata lookup when needed. -- Message signing and verification. -- Node health checks for native and USDC token balance. - -### Getting started - -Add shared EVM secrets (see `cw_evm` README): - -```dart -// cw_evm/lib/.secrets.g.dart (DO NOT COMMIT) -const String etherScanApiKey = 'YOUR_ETHERSCAN_KEY'; -const String nowNodesApiKey = 'YOUR_NOWNODES_KEY'; // if using eth.nownodes.io -const String moralisApiKey = 'YOUR_MORALIS_KEY'; // optional -``` - -Connect and sync: - -```dart -final service = EthereumWalletService(walletInfoBox, true, client: EthereumClient()); -final wallet = await service.create(EVMChainNewWalletCredentials(name: 'My ETH', password: 'secret')); -await wallet.connectToNode(node: Node(uriRaw: 'eth.llamarpc.com', isSSL: true)); -await wallet.startSync(); -``` - -### Usage - -Send ETH: - -```dart -final pending = await wallet.createTransaction( - EVMChainTransactionCredentials.single( - address: '0x...', - cryptoAmount: '0.05', - currency: CryptoCurrency.eth, - priority: EVMChainTransactionPriority.medium, - ), -); -final hash = await pending.commit(); -``` - -Add an ERC‑20 token and refresh balance: - -```dart -final token = await wallet.getErc20Token('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 'eth'); // USDC -if (token != null) { - await wallet.addErc20Token(token); -} -``` - -### Additional information - -- Toggle Etherscan usage via shared preferences key `use_etherscan`. -- See `lib/` for APIs: `EthereumClient`, `EthereumWallet`, `EthereumWalletService`. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml deleted file mode 100644 index a5744c1cf..000000000 --- a/cw_ethereum/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/cw_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart deleted file mode 100644 index af9ea7ee0..000000000 --- a/cw_ethereum/lib/cw_ethereum.dart +++ /dev/null @@ -1,7 +0,0 @@ -library cw_ethereum; - -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart deleted file mode 100644 index 4bb9033c0..000000000 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ /dev/null @@ -1,863 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; - -class DefaultEthereumErc20Tokens { - final List _defaultTokens = [ - Erc20Token( - name: "USD Coin", - symbol: "USDC", - contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "USDT Tether", - symbol: "USDT", - contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "Decentralized Euro", - symbol: "DEURO", - contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "Dai", - symbol: "DAI", - contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "Wrapped Ether", - symbol: "WETH", - contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Pepe", - symbol: "PEPE", - contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "SHIBA INU", - symbol: "SHIB", - contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "ApeCoin", - symbol: "APE", - contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Matic Token", - symbol: "MATIC", - contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Wrapped BTC", - symbol: "WBTC", - contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Gitcoin", - symbol: "GTC", - contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Compound", - symbol: "COMP", - contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Aave Token", - symbol: "AAVE", - contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Uniswap", - symbol: "UNI", - contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Decentraland", - symbol: "MANA", - contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Storj", - symbol: "STORJ", - contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Maker", - symbol: "MKR", - contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Orchid", - symbol: "OXT", - contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Paxos Gold", - symbol: "PAXG", - contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Binance Coin", - symbol: "BNB", - contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "stETH", - symbol: "stETH", - contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Lido DAO", - symbol: "LDO", - contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Arbitrum", - symbol: "ARB", - contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Graph Token", - symbol: "GRT", - contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Frax", - symbol: "FRAX", - contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Gemini dollar", - symbol: "GUSD", - contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", - decimal: 2, - enabled: false, - ), - Erc20Token( - name: "Compound Ether", - symbol: "cETH", - contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Binance USD", - symbol: "BUSD", - contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "TrueUSD", - symbol: "TUSD", - contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Cronos Coin", - symbol: "CRO", - contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Pax Dollar", - symbol: "USDP", - contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Fantom Token", - symbol: "FTM", - contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "BitTorrent", - symbol: "BTT", - contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Nexo", - symbol: "NEXO", - contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "dYdX", - symbol: "DYDX", - contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "PancakeSwap Token", - symbol: "Cake", - contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "BAT", - symbol: "BAT", - contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "1INCH Token", - symbol: "1INCH", - contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Ethereum Name Service", - symbol: "ENS", - contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "ZRX", - symbol: "ZRX", - contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Verse", - symbol: "VERSE", - contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "PayPal USD", - symbol: "PYUSD", - contractAddress: "0x6c3ea9036406852006290770bedfcaba0e23a0e8", - decimal: 6, - enabled: false, - ), - Erc20Token( - name: "Chainflip", - symbol: "FLIP", - contractAddress: "0x826180541412D574cf1336d22c0C0a287822678A", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Native Decentralized Euro Protocol Share", - symbol: "NDEPS", - contractAddress: "0xc71104001A3CCDA1BEf1177d765831Bd1bfE8eE6", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Decentralized Euro Protocol Share", - symbol: "DEPS", - contractAddress: "0x103747924e74708139a9400e4ab4bea79fffa380", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Kraken Wrapped Bitcoin", - symbol: "KBTC", - contractAddress: "0x73E0C0d45E048D25Fc26Fa3159b0aA04BfA4Db98", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Coinbase Wrapped BTC", - symbol: "CBBTC", - contractAddress: "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: 'Abbott xStock', - symbol: 'ABTx', - contractAddress: '0x89233399708c18ac6887f90a2b4cd8ba5fedd06e', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/abtx.webp', - ), - Erc20Token( - name: 'AbbVie xStock', - symbol: 'ABBVx', - contractAddress: '0xfbf2398df672cee4afcc2a4a733222331c742a6a', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/abbv.webp', - ), - Erc20Token( - name: 'Accenture xStock', - symbol: 'ACNx', - contractAddress: '0x03183ce31b1656b72a55fa6056e287f50c35bbeb', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/acnx.webp', - ), - Erc20Token( - name: 'Alphabet xStock', - symbol: 'GOOGLx', - contractAddress: '0xe92f673ca36c5e2efd2de7628f815f84807e803f', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/googlx.webp', - ), - Erc20Token( - name: 'Amazon xStock', - symbol: 'AMZNx', - contractAddress: '0x3557ba345b01efa20a1bddc61f573bfd87195081', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/amznx.webp', - ), - Erc20Token( - name: 'Amber xStock', - symbol: 'AMBRx', - contractAddress: '0x2f9a35ab5ddfbc49927bfdeab98a86c53dc6e763', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/ambrx.webp', - ), - Erc20Token( - name: 'Apple xStock', - symbol: 'AAPLx', - contractAddress: '0x9d275685dc284c8eb1c79f6aba7a63dc75ec890a', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/apple.webp', - ), - Erc20Token( - name: 'AppLovin xStock', - symbol: 'APPx', - contractAddress: '0x50a1291f69d9d3853def8209cfb1af0b46927be1', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/appx.webp', - ), - Erc20Token( - name: 'AstraZeneca xStock', - symbol: 'AZNx', - contractAddress: '0x5d642505fe1a28897eb3baba665f454755d8daa2', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/aznx.webp', - ), - Erc20Token( - name: 'Bank of America xStock', - symbol: 'BACx', - contractAddress: '0x314938c596f5ce31c3f75307d2979338c346d7f2', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/bacx.webp', - ), - Erc20Token( - name: 'Berkshire Hathaway xStock', - symbol: 'BRK.Bx', - contractAddress: '0x12992613fdd35abe95dec5a4964331b1ee23b50d', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/brkbx.webp', - ), - Erc20Token( - name: 'Broadcom xStock', - symbol: 'AVGOx', - contractAddress: '0x38bac69cbbd28156796e4163b2b6dcb81e336565', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/avgox.webp', - ), - Erc20Token( - name: 'Chevron xStock', - symbol: 'CVXx', - contractAddress: '0xad5cdc3340904285b8159089974a99a1a09eb4c0', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/cvxx.webp', - ), - Erc20Token( - name: 'Circle xStock', - symbol: 'CRCLx', - contractAddress: '0xfebded1b0986a8ee107f5ab1a1c5a813491deceb', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/crclx.webp', - ), - Erc20Token( - name: 'Cisco xStock', - symbol: 'CSCOx', - contractAddress: '0x053c784cd87b74f42e0c089f98643e79c1a3ff16', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/cscox.webp', - ), - Erc20Token( - name: 'Coca-Cola xStock', - symbol: 'KOx', - contractAddress: '0xdcc1a2699441079da889b1f49e12b69cc791129b', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/kox.webp', - ), - Erc20Token( - name: 'Coinbase xStock', - symbol: 'COINx', - contractAddress: '0x364f210f430ec2448fc68a49203040f6124096f0', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/coinx.webp', - ), - Erc20Token( - name: 'Comcast xStock', - symbol: 'CMCSAx', - contractAddress: '0xbc7170a1280be28513b4e940c681537eb25e39f4', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/cmcsax.webp', - ), - Erc20Token( - name: 'CrowdStrike xStock', - symbol: 'CRWDx', - contractAddress: '0x214151022c2a5e380ab80cdac31f23ae554a7345', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/crwdx.webp', - ), - Erc20Token( - name: 'Danaher xStock', - symbol: 'DHRx', - contractAddress: '0xdba228936f4079daf9aa906fd48a87f2300405f4', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/dhrx.webp', - ), - Erc20Token( - name: 'DFDV xStock', - symbol: 'DFDVx', - contractAddress: '0x521860bb5df5468358875266b89bfe90d990c6e7', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/dfdvx.webp', - ), - Erc20Token( - name: 'Eli Lilly xStock', - symbol: 'LLYx', - contractAddress: '0x19c41ea77b34bbdee61c3a87a75d1abda2ed0be4', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/llyx.webp', - ), - Erc20Token( - name: 'Exxon Mobil xStock', - symbol: 'XOMx', - contractAddress: '0xeedb0273c5af792745180e9ff568cd01550ffa13', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/xomx.webp', - ), - Erc20Token( - name: 'Gamestop xStock', - symbol: 'GMEx', - contractAddress: '0xe5f6d3b2405abdfe6f660e63202b25d23763160d', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/gmex.webp', - ), - Erc20Token( - name: 'Gold xStock', - symbol: 'GLDx', - contractAddress: '0x2380f2673c640fb67e2d6b55b44c62f0e0e69da9', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/gldx.webp', - ), - Erc20Token( - name: 'Goldman Sachs xStock', - symbol: 'GSx', - contractAddress: '0x3ee7e9b3a992fd23cd1c363b0e296856b04ab149', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/gsx.webp', - ), - Erc20Token( - name: 'Home Depot xStock', - symbol: 'HDx', - contractAddress: '0x766b0cd6ed6d90b5d49d2c36a3761e9728501ba9', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/hdx.webp', - ), - Erc20Token( - name: 'Honeywell xStock', - symbol: 'HONx', - contractAddress: '0x62a48560861b0b451654bfffdb5be6e47aa8ff1b', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/honx.webp', - ), - Erc20Token( - name: 'Intel xStock', - symbol: 'INTCx', - contractAddress: '0xf8a80d1cb9cfd70d03d655d9df42339846f3b3c8', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/intcx.webp', - ), - Erc20Token( - name: 'International Business Machines xStock', - symbol: 'IBMx', - contractAddress: '0xd9913208647671fe0f48f7f260076b2c6f310aac', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/ibmx.webp', - ), - Erc20Token( - name: 'Johnson & Johnson xStock', - symbol: 'JNJx', - contractAddress: '0xdb0482cfad4789798623e64b15eeba01b16e917c', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/jnjx.webp', - ), - Erc20Token( - name: 'JPMorgan Chase xStock', - symbol: 'JPMx', - contractAddress: '0xd9fc3e075d45254a1d834fea18af8041207dea0a', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/jpmx.webp', - ), - Erc20Token( - name: 'Linde xStock', - symbol: 'LINx', - contractAddress: '0x15059c599c16fd8f70b633ade165502d6402cd49', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/linx.webp', - ), - Erc20Token( - name: 'Marvell xStock', - symbol: 'MRVLx', - contractAddress: '0xeaad46f4146ded5a47b55aa7f6c48c191deaec88', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mrvlx.webp', - ), - Erc20Token( - name: 'Mastercard xStock', - symbol: 'MAx', - contractAddress: '0xb365cd2588065f522d379ad19e903304f6b622c6', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mastercard.webp', - ), - Erc20Token( - name: 'McDonald\'s xStock', - symbol: 'MCDx', - contractAddress: '0x80a77a372c1e12accda84299492f404902e2da67', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mcdonalds.webp', - ), - Erc20Token( - name: 'Medtronic xStock', - symbol: 'MDTx', - contractAddress: '0x0588e851ec0418d660bee81230d6c678daf21d46', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mdtx.webp', - ), - Erc20Token( - name: 'Merck xStock', - symbol: 'MRKx', - contractAddress: '0x17d8186ed8f68059124190d147174d0f6697dc40', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mrkx.webp', - ), - Erc20Token( - name: 'Meta xStock', - symbol: 'METAx', - contractAddress: '0x96702be57cd9777f835117a809c7124fe4ec989a', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/metax.webp', - ), - Erc20Token( - name: 'Microsoft xStock', - symbol: 'MSFTx', - contractAddress: '0x5621737f42dae558b81269fcb9e9e70c19aa6b35', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/msftx.webp', - ), - Erc20Token( - name: 'MicroStrategy xStock', - symbol: 'MSTRx', - contractAddress: '0xae2f842ef90c0d5213259ab82639d5bbf649b08e', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/mstrx.webp', - ), - Erc20Token( - name: 'Nasdaq xStock', - symbol: 'QQQx', - contractAddress: '0xa753a7395cae905cd615da0b82a53e0560f250af', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/qqqx.webp', - ), - Erc20Token( - name: 'Netflix xStock', - symbol: 'NFLXx', - contractAddress: '0xa6a65ac27e76cd53cb790473e4345c46e5ebf961', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/nflxx.webp', - ), - Erc20Token( - name: 'Novo Nordisk xStock', - symbol: 'NVOx', - contractAddress: '0xf9523e369c5f55ad72dbaa75b0a9b92b3d8b147e', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/nvox.webp', - ), - Erc20Token( - name: 'NVIDIA xStock', - symbol: 'NVDAx', - contractAddress: '0xc845b2894dbddd03858fd2d643b4ef725fe0849d', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/nvdax.webp', - ), - Erc20Token( - name: 'OPEN xStock', - symbol: 'OPENx', - contractAddress: '0xbee6b69345f376598fe16abd5592c6f844825e66', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/openx.webp', - ), - Erc20Token( - name: 'Oracle xStock', - symbol: 'ORCLx', - contractAddress: '0x548308e91ec9f285c7bff05295badbd56a6e4971', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/orclx.webp', - ), - Erc20Token( - name: 'Palantir xStock', - symbol: 'PLTRx', - contractAddress: '0x6d482cec5f9dd1f05ccee9fd3ff79b246170f8e2', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/pltrx.webp', - ), - Erc20Token( - name: 'PepsiCo xStock', - symbol: 'PEPx', - contractAddress: '0x36c424a6ec0e264b1616102ad63ed2ad7857413e', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/pepx.webp', - ), - Erc20Token( - name: 'Pfizer xStock', - symbol: 'PFEx', - contractAddress: '0x1ac765b5bea23184802c7d2d497f7c33f1444a9e', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/pfex.webp', - ), - Erc20Token( - name: 'Philip Morris xStock', - symbol: 'PMx', - contractAddress: '0x02a6c1789c3b4fdb1a7a3dfa39f90e5d3c94f4f9', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/pmx.webp', - ), - Erc20Token( - name: 'Procter & Gamble xStock', - symbol: 'PGx', - contractAddress: '0xa90424d5d3e770e8644103ab503ed775dd1318fd', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/pgx.webp', - ), - Erc20Token( - name: 'Robinhood xStock', - symbol: 'HOODx', - contractAddress: '0xe1385fdd5ffb10081cd52c56584f25efa9084015', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/hoodx.webp', - ), - Erc20Token( - name: 'Salesforce xStock', - symbol: 'CRMx', - contractAddress: '0x4a4073f2eaf299a1be22254dcd2c41727f6f54a2', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/crmx.webp', - ), - Erc20Token( - name: 'SP500 xStock', - symbol: 'SPYx', - contractAddress: '0x90a2a4c76b5d8c0bc892a69ea28aa775a8f2dd48', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/spyx.webp', - ), - Erc20Token( - name: 'TBLL xStock', - symbol: 'TBLLx', - contractAddress: '0x4cbf89ed7bb30b8a860fa86d3c96e9c72931299b', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/tbllx.webp', - ), - Erc20Token( - name: 'Tesla xStock', - symbol: 'TSLAx', - contractAddress: '0x8ad3c73f833d3f9a523ab01476625f269aeb7cf0', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/tslax.webp', - ), - Erc20Token( - name: 'Thermo Fisher xStock', - symbol: 'TMOx', - contractAddress: '0xaf072f109a2c173d822a4fe9af311a1b18f83d19', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/tmox.webp', - ), - Erc20Token( - name: 'TON xStock', - symbol: 'TONXx', - contractAddress: '0xe95ab205e333443d7970336d5fd827ef9ed97608', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/tonxx.webp', - ), - Erc20Token( - name: 'TQQQ xStock', - symbol: 'TQQQx', - contractAddress: '0xfdddb57878ef9d6f681ec4381dcb626b9e69ac86', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/tqqqx.webp', - ), - Erc20Token( - name: 'UnitedHealth xStock', - symbol: 'UNHx', - contractAddress: '0x167a6375da1efc4a5be0f470e73ecefd66245048', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/unhx.webp', - ), - Erc20Token( - name: 'Vanguard xStock', - symbol: 'VTIx', - contractAddress: '0xbd730e618bcd88c82ddee52e10275cf2f88a4777', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/vtix.webp', - ), - Erc20Token( - name: 'Visa xStock', - symbol: 'Vx', - contractAddress: '0x2363fd1235c1b6d3a5088ddf8df3a0b3a30c5293', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/vx.webp', - ), - Erc20Token( - name: 'Walmart xStock', - symbol: 'WMTx', - contractAddress: '0x7aefc9965699fbea943e03264d96e50cd4a97b21', - decimal: 8, - enabled: false, - iconPath: 'assets/images/stocks/wmtx.webp', - ), - ]; - - List get initialErc20Tokens => _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: 'ETH'); - }).toList(); -} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart deleted file mode 100644 index 259e7d11d..000000000 --- a/cw_ethereum/lib/ethereum_client.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:convert'; -import 'dart:developer'; -import 'dart:typed_data'; - -import 'package:cw_evm/evm_chain_client.dart'; -import 'package:cw_evm/.secrets.g.dart' as secrets; -import 'package:cw_evm/evm_chain_transaction_model.dart'; -import 'package:web3dart/web3dart.dart'; - -class EthereumClient extends EVMChainClient { - @override - int get chainId => 1; - - @override - Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => - prependTransactionType(0x02, signedTransaction); - - @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 (jsonResponse['result'] is String) { - log(jsonResponse['result']); - return []; - } - - if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { - return (jsonResponse['result'] as List) - .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) - .toList(); - } - - return []; - } catch (e) { - log(e.toString()); - 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 (e) { - log(e.toString()); - return []; - } - } -} diff --git a/cw_ethereum/lib/ethereum_mnemonics_exception.dart b/cw_ethereum/lib/ethereum_mnemonics_exception.dart deleted file mode 100644 index b91a15c94..000000000 --- a/cw_ethereum/lib/ethereum_mnemonics_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class EthereumMnemonicIsIncorrectException implements Exception { - @override - String toString() => - 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; -} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart deleted file mode 100644 index fbb8ab79d..000000000 --- a/cw_ethereum/lib/ethereum_transaction_history.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:core'; -import 'package:cw_ethereum/ethereum_transaction_info.dart'; -import 'package:cw_evm/evm_chain_transaction_history.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; - -class EthereumTransactionHistory extends EVMChainTransactionHistory { - EthereumTransactionHistory({ - required super.walletInfo, - required super.password, - required super.encryptionFileUtils, - }); - - @override - String getTransactionHistoryFileName() => 'transactions.json'; - - @override - EVMChainTransactionInfo getTransactionInfo(Map val) => - EthereumTransactionInfo.fromJson(val); -} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart deleted file mode 100644 index 610f04315..000000000 --- a/cw_ethereum/lib/ethereum_transaction_info.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; - -class EthereumTransactionInfo extends EVMChainTransactionInfo { - EthereumTransactionInfo({ - 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.evmSignatureName, - super.exponent, - }); - - factory EthereumTransactionInfo.fromJson(Map data) { - return EthereumTransactionInfo( - id: data['id'] as String, - height: data['height'] as int, - ethAmount: BigInt.parse(data['amount']), - exponent: data['exponent'] as int, - ethFee: BigInt.parse(data['fee']), - direction: parseTransactionDirectionFromInt(data['direction'] as int), - date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), - isPending: data['isPending'] as bool, - confirmations: data['confirmations'] as int, - tokenSymbol: data['tokenSymbol'] as String, - to: data['to'], - from: data['from'], - evmSignatureName: data['evmSignatureName'], - contractAddress: data['contractAddress'], - ); - } - - @override - String get feeCurrency => 'ETH'; -} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart deleted file mode 100644 index a77bff6a3..000000000 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'dart:convert'; - -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_ethereum/default_ethereum_erc20_tokens.dart'; -import 'package:cw_ethereum/ethereum_client.dart'; -import 'package:cw_ethereum/ethereum_transaction_history.dart'; -import 'package:cw_ethereum/ethereum_transaction_info.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_transaction_priority.dart'; -import 'package:cw_evm/evm_chain_wallet.dart'; -import 'package:cw_evm/evm_erc20_balance.dart'; -import 'package:web3dart/web3dart.dart'; - -class EthereumWallet extends EVMChainWallet { - EthereumWallet({ - required super.client, - required super.password, - required super.walletInfo, - required super.derivationInfo, - super.mnemonic, - super.initialBalance, - super.privateKey, - required super.encryptionFileUtils, - super.passphrase, - }) : super(nativeCurrency: CryptoCurrency.eth); - - @override - int getTotalPriorityFee(EVMChainTransactionPriority priority) { - return EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); - } - - @override - void addInitialTokens() { - final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens; - - 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 - Future checkIfScanProviderIsEnabled() async { - bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true; - return isEtherscanEnabled; - } - - @override - Future initErc20TokensBox() async { - // This is for ethereum wallets, - // Other wallets would override and initialize their respective boxes with their boxNames. - await movePreviousErc20BoxConfigsToNewBox(); - } - - /// Majorly for backward compatibility for previous configs that have been set. - Future movePreviousErc20BoxConfigsToNewBox() async { - // Opens a box specific to this wallet - evmChainErc20TokensBox = await CakeHive.openBox( - "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}"); - - //Open the previous token configs box - erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); - - // Check if it's empty, if it is, we stop the flow and return. - if (erc20TokensBox.isEmpty) { - // If it's empty, but the new wallet specific box is also empty, - // we load the initial tokens to the new box. - if (evmChainErc20TokensBox.isEmpty) addInitialTokens(); - return; - } - - final allValues = erc20TokensBox.values.toList(); - - // Clear and delete the old token box - await erc20TokensBox.clear(); - await erc20TokensBox.deleteFromDisk(); - - // Add all the previous tokens with configs to the new box - await evmChainErc20TokensBox.addAll(allValues); - } - - @override - List get getDefaultTokenContractAddresses => - DefaultEthereumErc20Tokens().initialErc20Tokens.map((e) => e.contractAddress).toList(); - - @override - EVMChainTransactionInfo getTransactionInfo( - EVMChainTransactionModel transactionModel, String address) { - final model = EthereumTransactionInfo( - id: transactionModel.hash, - height: transactionModel.blockNumber, - ethAmount: transactionModel.amount, - direction: transactionModel.from == address - ? TransactionDirection.outgoing - : TransactionDirection.incoming, - isPending: false, - date: transactionModel.date, - confirmations: transactionModel.confirmations, - ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, - exponent: transactionModel.tokenDecimal ?? 18, - tokenSymbol: transactionModel.tokenSymbol ?? "ETH", - to: transactionModel.to, - from: transactionModel.from, - evmSignatureName: transactionModel.evmSignatureName, - contractAddress: transactionModel.contractAddress, - ); - return model; - } - - @override - String getTransactionHistoryFileName() => '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 - EVMChainTransactionHistory setUpTransactionHistory( - WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) { - return EthereumTransactionHistory( - 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 EthereumWallet( - walletInfo: walletInfo, - derivationInfo: await walletInfo.getDerivationInfo(), - password: password, - mnemonic: keysData.mnemonic, - privateKey: keysData.privateKey, - passphrase: keysData.passphrase, - initialBalance: balance, - client: EthereumClient(), - encryptionFileUtils: encryptionFileUtils, - ); - } -} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart deleted file mode 100644 index 1976c277d..000000000 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:bip39/bip39.dart' as bip39; -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_ethereum/ethereum_client.dart'; -import 'package:cw_ethereum/ethereum_mnemonics_exception.dart'; -import 'package:cw_ethereum/ethereum_wallet.dart'; -import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; -import 'package:cw_evm/evm_chain_wallet_service.dart'; - -class EthereumWalletService extends EVMChainWalletService { - EthereumWalletService(super.isDirect, {required this.client}); - - late EthereumClient client; - - @override - WalletType getType() => WalletType.ethereum; - - @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 = EthereumWallet( - 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 EthereumWallet.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 EthereumWallet.open( - name: name, - password: password, - walletInfo: walletInfo, - 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 EthereumWallet.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; - - await newWalletInfo.save(); - } - - @override - Future restoreFromHardwareWallet( - EVMChainRestoreWalletFromHardware credentials) async { - final di = await credentials.walletInfo!.getDerivationInfo(); - di.derivationType = DerivationType.bip39; - di.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"; - await di.save(); - credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; - credentials.walletInfo!.address = credentials.hwAccountData.address; - credentials.walletInfo!.save(); - - final wallet = EthereumWallet( - walletInfo: credentials.walletInfo!, - derivationInfo: di, - password: credentials.password!, - client: client, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), - ); - - await wallet.init(); - wallet.addInitialTokens(); - await wallet.save(); - - return wallet; - } - - @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, - {bool? isTestnet}) async { - final wallet = EthereumWallet( - 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 restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, - {bool? isTestnet}) async { - if (!bip39.validateMnemonic(credentials.mnemonic)) { - throw EthereumMnemonicIsIncorrectException(); - } - - final wallet = EthereumWallet( - 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; - } -} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml deleted file mode 100644 index 1da1e3b3b..000000000 --- a/cw_ethereum/pubspec.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: cw_ethereum -description: A new Flutter package project. -version: 0.0.1 -publish_to: none -author: Cake Wallet -homepage: https://cakewallet.com - -environment: - sdk: ^3.5.0 - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - web3dart: ^2.7.1 - cw_core: - path: ../cw_core - cw_evm: - path: ../cw_evm - hive: ^2.2.3 - -dependency_overrides: - web3dart: - git: - url: https://github.com/cake-tech/web3dart.git - ref: cake - watcher: ^1.1.0 - -dev_dependencies: - flutter_test: - sdk: flutter - build_runner: ^2.4.15 - -flutter: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic diff --git a/cw_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart deleted file mode 100644 index 72026a4c0..000000000 --- a/cw_ethereum/test/cw_ethereum_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:cw_ethereum/cw_ethereum.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -} diff --git a/cw_evm/lib/clients/arbitrum_client.dart b/cw_evm/lib/clients/arbitrum_client.dart new file mode 100644 index 000000000..5d25aae60 --- /dev/null +++ b/cw_evm/lib/clients/arbitrum_client.dart @@ -0,0 +1,46 @@ +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class ArbitrumClient extends EVMChainClient { + ArbitrumClient() : super(chainId: 42161); + + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + int? nonce, + }) { + 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, + nonce: nonce, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 42161; +} + diff --git a/cw_evm/lib/clients/base_client.dart b/cw_evm/lib/clients/base_client.dart new file mode 100644 index 000000000..859cc7f89 --- /dev/null +++ b/cw_evm/lib/clients/base_client.dart @@ -0,0 +1,46 @@ +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class BaseClient extends EVMChainClient { + BaseClient() : super(chainId: 8453); + + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + int? nonce, + }) { + 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, + nonce: nonce, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 8453; +} + diff --git a/cw_evm/lib/clients/ethereum_client.dart b/cw_evm/lib/clients/ethereum_client.dart new file mode 100644 index 000000000..eb0f5ddbe --- /dev/null +++ b/cw_evm/lib/clients/ethereum_client.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; + +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:web3dart/web3dart.dart'; + +class EthereumClient extends EVMChainClient { + EthereumClient() : super(chainId: 1); + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => + prependTransactionType(0x02, signedTransaction); +} + diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/clients/evm_chain_client.dart similarity index 60% rename from cw_evm/lib/evm_chain_client.dart rename to cw_evm/lib/clients/evm_chain_client.dart index 574c2b4f9..861a59d10 100644 --- a/cw_evm/lib/evm_chain_client.dart +++ b/cw_evm/lib/clients/evm_chain_client.dart @@ -5,32 +5,128 @@ import 'dart:developer'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_evm/pending_evm_chain_transaction.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/utils/evm_chain_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:hex/hex.dart' as hex; import 'package:web3dart/web3dart.dart'; -import 'contract/erc20.dart'; +import '../contract/erc20.dart'; -abstract class EVMChainClient { +class EVMChainClient { late final client = ProxyWrapper().getHttpIOClient(); Web3Client? _client; + final int _chainId; - //! To be overridden by all child classes + EVMChainClient({required int chainId}) : _chainId = chainId; - int get chainId; + //! Can be overridden by child classes + + int get chainId => _chainId; Future> fetchTransactions(String address, - {String? contractAddress}); + {String? contractAddress}) async { + try { + if (secrets.etherScanApiKey.isEmpty) { + printV('Etherscan API key is empty, cannot fetch transactions'); + return []; + } - Future> fetchInternalTransactions(String address); + /// when adding new chains, make sure they are supported by the same api through https://docs.etherscan.io/supported-chains + 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, + })); - Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction); + final jsonResponse = json.decode(response.body) as Map; + + if (jsonResponse['result'] is String) { + log(jsonResponse['result']); + return []; + } + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + final res = (jsonResponse['result'] as List); + res.removeWhere((e) => e['value'] == '0'); + + // Filter out spam native transactions below 0.00001 ETH (10000000000000 wei) + if (contractAddress == null) { + final spamThresholdWei = BigInt.from(10000000000000); + res.removeWhere((e) { + try { + final value = BigInt.parse(e['value'] ?? '0'); + final isIncoming = e['to']?.toLowerCase() == address.toLowerCase() && + e['from']?.toLowerCase() != address.toLowerCase(); + return isIncoming && value < spamThresholdWei; + } catch (_) { + return false; + } + }); + } + + final symbol = EVMChainUtils.getFeeCurrency(chainId); + + return res + .map( + (e) => EVMChainTransactionModel.fromJson(e as Map, symbol, chainId), + ) + .toList(); + } + + return []; + } catch (e) { + log(e.toString()); + return []; + } + } + + Future> fetchInternalTransactions(String address) async { + try { + if (secrets.etherScanApiKey.isEmpty) { + printV('Etherscan API key is empty, cannot fetch internal transactions'); + return []; + } + + 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) { + final symbol = EVMChainUtils.getFeeCurrency(chainId); + + return (jsonResponse['result'] as List) + .map((e) => + EVMChainTransactionModel.fromJson(e as Map, symbol, chainId)) + .toList(); + } + + printV( + 'Etherscan API returned invalid response for internal transactions: status=${jsonResponse['status']}, statusCode=${response.statusCode}'); + return []; + } catch (e, stackTrace) { + printV('Error fetching internal transactions: ${e.toString()}'); + printV('Stack trace: ${stackTrace.toString()}'); + return []; + } + } + + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; //! Common methods across all child classes @@ -43,6 +139,11 @@ abstract class EVMChainClient { isModifiedNodeUri = true; String nowNodeApiKey = secrets.nowNodesApiKey; + if (nowNodeApiKey.isEmpty) { + printV('NowNodes API key is empty, cannot connect to ${node.uriRaw}'); + return false; + } + rpcUri = Uri.https(node.uriRaw, '/$nowNodeApiKey'); } @@ -50,6 +151,7 @@ abstract class EVMChainClient { return true; } catch (e) { + printV('Error connecting to node ${node.uriRaw}: ${e.toString()}'); return false; } } @@ -64,14 +166,11 @@ abstract class EVMChainClient { // }); } - Future getBalance(EthereumAddress address, {bool throwOnError = false}) async { + Future getBalance(EthereumAddress address) async { try { return await _client!.getBalance(address); } catch (_) { - if (throwOnError) { - rethrow; - } - return EtherAmount.zero(); + rethrow; } } @@ -80,8 +179,9 @@ abstract class EVMChainClient { final gasPrice = await _client!.getGasPrice(); return gasPrice.getInWei.toInt(); - } catch (_) { - return 0; + } catch (e) { + printV('Error getting gas unit price: ${e.toString()}'); + rethrow; } } @@ -91,8 +191,9 @@ abstract class EVMChainClient { final baseFee = blockInfo.baseFeePerGas; return baseFee?.getInWei.toInt(); - } catch (_) { - return 0; + } catch (e) { + printV('Error getting gas base fee: ${e.toString()}'); + return null; } } @@ -170,6 +271,7 @@ abstract class EVMChainClient { String? contractAddress, String? data, int? gasPrice, + bool useBlinkProtection = true, }) async { assert(currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly || @@ -182,15 +284,24 @@ abstract class EVMChainClient { currency == CryptoCurrency.baseEth || currency == CryptoCurrency.arbEth; + // Get nonce with "pending" block tag to include pending transactions + // This prevents "Nonce too low" errors when sending multiple transactions quickly + final nonce = await _client!.getTransactionCount( + privateKey.address, + atBlock: const BlockNum.pending(), + ); + final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(toAddress), - maxPriorityFeePerGas: priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, + 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, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, + nonce: nonce, ); Uint8List signedTransaction; @@ -214,7 +325,8 @@ abstract class EVMChainClient { ); } - _sendTransaction = () async => await sendTransaction(signedTransaction); + _sendTransaction = () async => + await sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection); return PendingEVMChainTransaction( signedTransaction: prepareSignedTransactionForSending(signedTransaction), @@ -238,15 +350,23 @@ abstract class EVMChainClient { required int exponent, required String contractAddress, int? gasPrice, + bool useBlinkProtection = true, }) async { + final nonce = await _client!.getTransactionCount( + privateKey.address, + atBlock: const BlockNum.pending(), + ); + final Transaction transaction = createTransaction( from: privateKey.address, to: EthereumAddress.fromHex(contractAddress), - maxPriorityFeePerGas:priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, + maxPriorityFeePerGas: + priority != null ? EtherAmount.fromInt(EtherUnit.gwei, priority.tip) : null, amount: EtherAmount.zero(), maxGas: estimatedGasUnits, maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas), gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null, + nonce: nonce, ); final erc20 = ERC20( @@ -267,7 +387,8 @@ abstract class EVMChainClient { amount: amount.toString(), fee: gasFee, feeCurrency: feeCurrency, - sendTransaction: () => sendTransaction(signedTransaction), + sendTransaction: () => + sendTransaction(signedTransaction, useBlinkProtection: useBlinkProtection), exponent: exponent, isInfiniteApproval: amount.toRadixString(16) == 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', @@ -283,6 +404,7 @@ abstract class EVMChainClient { EtherAmount? maxFeePerGas, Uint8List? data, int? maxGas, + int? nonce, }) { return Transaction( from: from, @@ -293,11 +415,28 @@ abstract class EVMChainClient { maxGas: maxGas, gasPrice: gasPrice, maxFeePerGas: maxFeePerGas, + nonce: nonce, ); } - Future sendTransaction(Uint8List signedTransaction) async { - return await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); + String _blinkUrl(String apiKey) => 'https://eth.blinklabs.xyz/v1/$apiKey'; + + Future sendTransaction( + Uint8List signedTransaction, { + bool useBlinkProtection = false, + }) async { + final prepared = prepareSignedTransactionForSending(signedTransaction); + + if (useBlinkProtection && secrets.blinkApiKey.isNotEmpty) { + final blinkClient = Web3Client(_blinkUrl(secrets.blinkApiKey), client); + try { + return await blinkClient.sendRawTransaction(prepared); + } finally { + await blinkClient.dispose(); + } + } + + return await _client!.sendRawTransaction(prepared); } Future getTransactionDetails(String transactionHash) async { @@ -360,58 +499,72 @@ abstract class EVMChainClient { Future getErc20Token(String contractAddress, String chainName) async { try { - final uri = Uri.https( - 'deep-index.moralis.io', - '/api/v2.2/erc20/metadata', - { - "chain": chainName, - "addresses": contractAddress, - }, - ); + final token = await getErcTokenInfoFromNode(contractAddress, chainName); - final response = await client.get( - uri, - headers: { - "Accept": "application/json", - "X-API-Key": secrets.moralisApiKey, - }, - ); + if (token == null || token.name.isEmpty || token.symbol.isEmpty) { + return await getErc20TokenFromMoralis(contractAddress, chainName); + } - final decodedResponse = jsonDecode(response.body)[0] as Map; - - final symbol = (decodedResponse['symbol'] ?? '') as String; - String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); - - final name = decodedResponse['name'] ?? ''; - final decimal = decodedResponse['decimals'] ?? '0'; - final iconPath = decodedResponse['logo'] ?? ''; - - return Erc20Token( - name: name, - symbol: filteredSymbol, - contractAddress: contractAddress, - decimal: int.tryParse(decimal) ?? 0, - iconPath: iconPath, - ); + return token; } catch (e) { try { - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); - final name = await erc20.name(); - final symbol = await erc20.symbol(); - final decimal = await erc20.decimals(); - - return Erc20Token( - name: name, - symbol: symbol, - contractAddress: contractAddress, - decimal: decimal.toInt(), - ); - } catch (_) {} - - return null; + return await getErc20TokenFromMoralis(contractAddress, chainName); + } catch (e) { + return null; + } } } + Future getErc20TokenFromMoralis(String contractAddress, String chainName) async { + final uri = Uri.https( + 'deep-index.moralis.io', + '/api/v2.2/erc20/metadata', + { + "chain": chainName, + "addresses": contractAddress, + }, + ); + + final response = await client.get( + uri, + headers: { + "Accept": "application/json", + "X-API-Key": secrets.moralisApiKey, + }, + ); + + final decodedResponse = jsonDecode(response.body)[0] as Map; + + final symbol = (decodedResponse['symbol'] ?? '') as String; + String filteredSymbol = symbol.replaceFirst(RegExp('^\\\$'), ''); + + final name = (decodedResponse['name'] ?? '').toString(); + final decimal = decodedResponse['decimals'] ?? '0'; + final iconPath = decodedResponse['logo'] ?? ''; + + return Erc20Token( + name: name, + symbol: filteredSymbol, + contractAddress: contractAddress, + decimal: int.tryParse(decimal) ?? 0, + iconPath: iconPath, + ); + } + + Future getErcTokenInfoFromNode(String contractAddress, String chainName) async { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } + Uint8List hexToBytes(String hexString) { return Uint8List.fromList( hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString)); diff --git a/cw_evm/lib/clients/polygon_client.dart b/cw_evm/lib/clients/polygon_client.dart new file mode 100644 index 000000000..56ee9fb8f --- /dev/null +++ b/cw_evm/lib/clients/polygon_client.dart @@ -0,0 +1,46 @@ +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:flutter/foundation.dart'; +import 'package:web3dart/web3dart.dart'; + +class PolygonClient extends EVMChainClient { + PolygonClient() : super(chainId: 137); + + @override + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + Uint8List? data, + int? maxGas, + EtherAmount? gasPrice, + EtherAmount? maxFeePerGas, + int? nonce, + }) { + 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, + nonce: nonce, + // maxFeePerGas: maxFeePerGas, + // maxPriorityFeePerGas: maxPriorityFeePerGas, + ); + } + + @override + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => signedTransaction; + + @override + int get chainId => 137; +} + diff --git a/cw_ethereum/lib/deuro/deuro_savings.dart b/cw_evm/lib/deuro/deuro_savings.dart similarity index 97% rename from cw_ethereum/lib/deuro/deuro_savings.dart rename to cw_evm/lib/deuro/deuro_savings.dart index 2fa93b4ad..e35020c72 100644 --- a/cw_ethereum/lib/deuro/deuro_savings.dart +++ b/cw_evm/lib/deuro/deuro_savings.dart @@ -1,6 +1,6 @@ import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_ethereum/deuro/deuro_savings_contract.dart'; -import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_evm/deuro/deuro_savings_contract.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_evm/contract/erc20.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; @@ -16,9 +16,9 @@ const String frontendCode = "0x00000000000000000000000000000000000000000043616b6 class DEuro { final SavingsGateway _savingsGateway; final ERC20 _dEuro; - final EthereumWallet _wallet; + final EVMChainWallet _wallet; - DEuro(EthereumWallet wallet) + DEuro(EVMChainWallet wallet) : _wallet = wallet, _savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!), _dEuro = _getDEuroToken(wallet.getWeb3Client()!); @@ -200,3 +200,4 @@ class DEuro { } } } + diff --git a/cw_ethereum/lib/deuro/deuro_savings_contract.dart b/cw_evm/lib/deuro/deuro_savings_contract.dart similarity index 100% rename from cw_ethereum/lib/deuro/deuro_savings_contract.dart rename to cw_evm/lib/deuro/deuro_savings_contract.dart diff --git a/cw_evm/lib/evm_chain_client_factory.dart b/cw_evm/lib/evm_chain_client_factory.dart new file mode 100644 index 000000000..61c73fdb8 --- /dev/null +++ b/cw_evm/lib/evm_chain_client_factory.dart @@ -0,0 +1,36 @@ +import 'package:cw_evm/clients/arbitrum_client.dart'; +import 'package:cw_evm/clients/base_client.dart'; +import 'package:cw_evm/clients/ethereum_client.dart'; +import 'package:cw_evm/clients/polygon_client.dart'; +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_registry.dart'; + +/// Factory to create appropriate EVMChainClient based on chainId +class EVMChainClientFactory { + static final EvmChainRegistry _registry = EvmChainRegistry(); + + /// Create an EVMChainClient for the given chainId + /// + /// Throws an exception if chainId is not registered + static EVMChainClient createClient(int chainId) { + final config = _registry.getChainConfig(chainId); + + if (config == null) { + throw Exception('Chain config not found for chainId: $chainId'); + } + + // Check if chain needs custom client + switch (chainId) { + case 1: + return EthereumClient(); + case 137: + return PolygonClient(); + case 8453: + return BaseClient(); + case 42161: + return ArbitrumClient(); + default: + return EVMChainClient(chainId: chainId); + } + } +} diff --git a/cw_evm/lib/evm_chain_default_tokens.dart b/cw_evm/lib/evm_chain_default_tokens.dart new file mode 100644 index 000000000..bc1496f94 --- /dev/null +++ b/cw_evm/lib/evm_chain_default_tokens.dart @@ -0,0 +1,22 @@ +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_evm/tokens/arbitrum_tokens.dart'; +import 'package:cw_evm/tokens/base_tokens.dart'; +import 'package:cw_evm/tokens/ethereum_tokens.dart'; +import 'package:cw_evm/tokens/polygon_tokens.dart'; + +/// Default ERC20 tokens for each EVM chain +class EVMChainDefaultTokens { + static List getDefaultTokensByChainId(int chainId) { + return switch (chainId) { + 1 => EthereumTokens.tokens, + 137 => PolygonTokens.tokens, + 8453 => BaseTokens.tokens, + 42161 => ArbitrumTokens.tokens, + _ => [], + }; + } + + static List getDefaultTokenAddresses(int chainId) { + return getDefaultTokensByChainId(chainId).map((token) => token.contractAddress).toList(); + } +} diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart index 8d4e94d13..ff3545a46 100644 --- a/cw_evm/lib/evm_chain_exceptions.dart +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -16,7 +16,10 @@ class EVMChainTransactionCreationException implements Exception { class EVMChainTransactionFeesException implements Exception { final String exceptionMessage; - EVMChainTransactionFeesException(String currency) + EVMChainTransactionFeesException(String message) + : exceptionMessage = message; + + EVMChainTransactionFeesException.fromCurrency(String currency) : exceptionMessage = 'Transaction failed due to insufficient $currency balance to cover the fees.'; @override diff --git a/cw_evm/lib/evm_chain_formatter.dart b/cw_evm/lib/evm_chain_formatter.dart deleted file mode 100644 index e53737170..000000000 --- a/cw_evm/lib/evm_chain_formatter.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'dart:math'; - -class EVMChainFormatter { - static int _divider = 0; - - static int parseEVMChainAmount(String amount) { - try { - final decimalLength = _getDividerForInput(amount); - _divider = decimalLength; - return (double.parse(amount) * pow(10, decimalLength)).round(); - } catch (_) { - return 0; - } - } - - static double parseEVMChainAmountToDouble(int amount) { - try { - return amount / pow(10, _divider); - } catch (_) { - return 0; - } - } - - static String truncateDecimals(String amount, int decimals) { - final parts = amount.split("."); - - if (parts.length == 2) { - parts[1] = parts[1].substring(0, parts[1].length > decimals ? decimals : parts[1].length); - } - - return parts.join("."); - } - - static int _getDividerForInput(String amount) { - final result = amount.split('.'); - if (result.length > 1) { - final decimalLength = result[1].length; - return decimalLength; - } else { - return 0; - } - } -} diff --git a/cw_evm/lib/evm_chain_registry.dart b/cw_evm/lib/evm_chain_registry.dart new file mode 100644 index 000000000..4ea89a7a6 --- /dev/null +++ b/cw_evm/lib/evm_chain_registry.dart @@ -0,0 +1,203 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/utils/network_chain_utils.dart'; + +/// Centralized registry for all EVM chain configurations +class EvmChainRegistry { + static final EvmChainRegistry _instance = EvmChainRegistry._internal(); + factory EvmChainRegistry() => _instance; + EvmChainRegistry._internal() { + initialize(); + } + + final Map _chains = {}; + final Map _walletTypeToChainId = {}; + final Map _chainIdToWalletType = {}; + final Map _tagToChainId = {}; + final Map _caip2ToChainId = {}; + + bool _initialized = false; + + /// Initialize registry with all supported EVM chains + void initialize() { + if (_initialized) return; + _initialized = true; + + // Ethereum Mainnet + _registerChain( + const ChainConfig( + chainId: 1, + name: 'Ethereum', + shortCode: 'eth', + caip2: 'eip155:1', + nativeCurrency: CryptoCurrency.eth, + capabilities: ChainCapabilities( + supportsERC20: true, + supportsEIP1559: true, + supportsInternalTx: true, + supportsSubscriptions: false, + supportsENS: true, + ), + defaultRpcEndpoints: [ + 'ethereum-rpc.publicnode.com', + 'eth.llamarpc.com', + 'rpc.flashbots.net', + 'eth-mainnet.public.blastapi.io', + 'eth.nownodes.io', + 'ethereum.publicnode.com', + ], + explorerUrls: [ + 'https://etherscan.io', + ], + feeModel: FeeModel( + type: FeeType.eip1559, + defaultGasLimit: 21000, + ), + ), + WalletType.ethereum, + 'ETH', + ); + + // Polygon + _registerChain( + const ChainConfig( + chainId: 137, + name: 'Polygon', + shortCode: 'polygon', + caip2: 'eip155:137', + nativeCurrency: CryptoCurrency.maticpoly, + capabilities: ChainCapabilities( + supportsERC20: true, + supportsEIP1559: true, + supportsInternalTx: true, + supportsSubscriptions: false, + supportsENS: false, + ), + defaultRpcEndpoints: [ + 'polygon-rpc.com', + 'polygon-bor-rpc.publicnode.com', + 'polygon.llamarpc.com', + 'matic.nownodes.io', + ], + explorerUrls: [ + 'https://polygonscan.com', + ], + feeModel: FeeModel( + type: FeeType.eip1559, + defaultGasLimit: 21000, + ), + ), + WalletType.polygon, + 'POL', + ); + + // Base + _registerChain( + const ChainConfig( + chainId: 8453, + name: 'Base', + shortCode: 'base', + caip2: 'eip155:8453', + nativeCurrency: CryptoCurrency.baseEth, + capabilities: ChainCapabilities( + supportsERC20: true, + supportsEIP1559: true, + supportsInternalTx: true, + supportsSubscriptions: false, + supportsENS: false, + ), + defaultRpcEndpoints: [ + 'base.nownodes.io', + 'base.llamarpc.com', + 'base-rpc.publicnode.com', + '1rpc.io/base', + ], + explorerUrls: [ + 'https://basescan.org', + ], + feeModel: FeeModel( + type: FeeType.eip1559, + defaultGasLimit: 21000, + ), + ), + WalletType.base, + 'BASE', + ); + + // Arbitrum + _registerChain( + const ChainConfig( + chainId: 42161, + name: 'Arbitrum', + shortCode: 'arbitrum', + caip2: 'eip155:42161', + nativeCurrency: CryptoCurrency.arbEth, + capabilities: ChainCapabilities( + supportsERC20: true, + supportsEIP1559: true, + supportsInternalTx: true, + supportsSubscriptions: false, + supportsENS: false, + ), + defaultRpcEndpoints: [ + 'arbitrum.nownodes.io', + 'arbitrum.drpc.org', + 'arbitrum-one-rpc.publicnode.com', + ], + explorerUrls: [ + 'https://arbiscan.io', + ], + feeModel: FeeModel( + type: FeeType.eip1559, + defaultGasLimit: 21000, + ), + ), + WalletType.arbitrum, + 'ARB', + ); + } + + void _registerChain( + ChainConfig config, + WalletType walletType, + String tag, + ) { + _chains[config.chainId] = config; + _walletTypeToChainId[walletType] = config.chainId; + _chainIdToWalletType[config.chainId] = walletType; + _tagToChainId[tag.toUpperCase()] = config.chainId; + _caip2ToChainId[config.caip2] = config.chainId; + } + + ChainConfig? getChainConfig(int chainId) => _chains[chainId]; + + ChainConfig? getChainConfigByWalletType(WalletType walletType) { + final chainId = _walletTypeToChainId[walletType]; + return chainId != null ? _chains[chainId] : null; + } + + /// Get chain configuration by tag (e.g., 'ETH', 'POL', 'BASE', 'ARB') + ChainConfig? getChainConfigByTag(String tag) { + final chainId = _tagToChainId[tag.toUpperCase()]; + return chainId != null ? _chains[chainId] : null; + } + + /// Get chain configuration by CAIP-2 identifier (e.g. 'eip155:1') + ChainConfig? getChainConfigByCaip2(String caip2) { + final chainId = _caip2ToChainId[caip2]; + return chainId != null ? _chains[chainId] : null; + } + + WalletType? getWalletTypeByChainId(int chainId) => _chainIdToWalletType[chainId]; + + int? getChainIdByWalletType(WalletType walletType) => _walletTypeToChainId[walletType]; + + bool isChainRegistered(int chainId) => _chains.containsKey(chainId); + + List getRegisteredChainIds() => _chains.keys.toList(); + + List getAllChains() => _chains.values.toList(); + + List getRegisteredWalletTypes() => + _walletTypeToChainId.keys.toList(); +} diff --git a/cw_evm/lib/evm_chain_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart index 5b5bdf170..327608afe 100644 --- a/cw_evm/lib/evm_chain_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -8,10 +8,12 @@ class EVMChainTransactionCredentials { required this.priority, required this.currency, this.feeRate, + this.useBlinkProtection = true, }); final List outputs; final EVMChainTransactionPriority? priority; final int? feeRate; final CryptoCurrency currency; + final bool useBlinkProtection; } diff --git a/cw_evm/lib/evm_chain_transaction_history.dart b/cw_evm/lib/evm_chain_transaction_history.dart index 7c46ebecc..8c8d72fdc 100644 --- a/cw_evm/lib/evm_chain_transaction_history.dart +++ b/cw_evm/lib/evm_chain_transaction_history.dart @@ -3,21 +3,25 @@ import 'dart:core'; import 'dart:developer'; import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/utils/evm_chain_utils.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; part 'evm_chain_transaction_history.g.dart'; -abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase - with _$EVMChainTransactionHistory; +class EVMChainTransactionHistory = EVMChainTransactionHistoryBase with _$EVMChainTransactionHistory; abstract class EVMChainTransactionHistoryBase extends TransactionHistoryBase with Store { - EVMChainTransactionHistoryBase( - {required this.walletInfo, required String password, required this.encryptionFileUtils}) - : _password = password { + EVMChainTransactionHistoryBase({ + required this.walletInfo, + required String password, + required this.encryptionFileUtils, + required this.getCurrentChainId, + }) : _password = password { transactions = ObservableMap(); } @@ -26,13 +30,17 @@ abstract class EVMChainTransactionHistoryBase final WalletInfo walletInfo; final EncryptionFileUtils encryptionFileUtils; - //! Method to be overridden by all child classes + /// Function to get the current chain ID (allows transaction history to use correct file) + final int Function() getCurrentChainId; - String getTransactionHistoryFileName(); + /// Get transaction history file name based on current chain ID + String getTransactionHistoryFileName() { + return EVMChainUtils.getTransactionHistoryFileName(getCurrentChainId()); + } - EVMChainTransactionInfo getTransactionInfo(Map val); - - //! Common methods across all child classes + EVMChainTransactionInfo getTransactionInfo(Map val) { + return EVMChainTransactionInfo.fromJson(val, getCurrentChainId()); + } Future init() async { clear(); @@ -45,7 +53,19 @@ abstract class EVMChainTransactionHistoryBase try { final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); String path = '$dirPath/$transactionsHistoryFileNameForWallet'; - final data = json.encode({'transactions': transactions}); + + // Filter transactions by current chainId before saving + // This ensures we only save transactions for the current chain, preventing + // transactions from other chains from being saved to the wrong file + final currentChainId = getCurrentChainId(); + final filteredTransactions = {}; + for (final entry in transactions.entries) { + if (entry.value.chainId == currentChainId) { + filteredTransactions[entry.key] = entry.value; + } + } + + final data = json.encode({'transactions': filteredTransactions}); await encryptionFileUtils.write(path: path, password: _password, data: data); } catch (e, s) { log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); @@ -54,11 +74,27 @@ abstract class EVMChainTransactionHistoryBase } @override - void addOne(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction; + void addOne(EVMChainTransactionInfo transaction) { + if (transaction.chainId == getCurrentChainId()) { + transactions[transaction.id] = transaction; + } + } @override - void addMany(Map transactions) => - this.transactions.addAll(transactions); + void addMany(Map transactionsToAdd) { + final currentChainId = getCurrentChainId(); + + // First, remove any transactions that don't match the current chainId + // This prevents transactions from other chains from persisting in the map + transactions.removeWhere((key, value) => value.chainId != currentChainId); + + // Then add/update transactions for the current chain + for (final entry in transactionsToAdd.entries) { + if (entry.value.chainId == currentChainId) { + transactions[entry.key] = entry.value; + } + } + } Future> _read() async { final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart index 781dd5738..9f4bc63ec 100644 --- a/cw_evm/lib/evm_chain_transaction_info.dart +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -3,10 +3,12 @@ import 'dart:math'; import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/format_fixed.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; +import 'package:cw_evm/utils/evm_chain_utils.dart'; -abstract class EVMChainTransactionInfo extends TransactionInfo { +class EVMChainTransactionInfo extends TransactionInfo { EVMChainTransactionInfo({ required this.id, required this.height, @@ -22,6 +24,7 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { required this.from, this.evmSignatureName, this.contractAddress, + required this.chainId, }) : amount = ethAmount.toInt(), fee = ethFee.toInt(); @@ -42,9 +45,10 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { final String? from; final String? evmSignatureName; final String? contractAddress; + final int chainId; - //! Getter to be overridden in child classes - String get feeCurrency; + /// Get fee currency symbol based on wallet type + String get feeCurrency => EVMChainUtils.getFeeCurrency(chainId); @override String amountFormatted() { @@ -60,8 +64,28 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { @override String feeFormatted() { - final amount = (ethFee / BigInt.from(10).pow(18)).toString(); - return '${amount.substring(0, min(18, amount.length))} $feeCurrency'; + final amount = formatFixed(ethFee, 18); + return '$amount $feeCurrency'; + } + + factory EVMChainTransactionInfo.fromJson(Map data, int chainId) { + return EVMChainTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount'] as String), + exponent: data['exponent'] as int? ?? 18, + ethFee: BigInt.parse(data['fee'] as String), + direction: TransactionDirection.values[data['direction'] as int], + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool? ?? false, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'] as String?, + from: data['from'] as String?, + evmSignatureName: data['evmSignatureName'] as String?, + contractAddress: data['contractAddress'] as String?, + chainId: chainId, + ); } Map toJson() => { @@ -79,5 +103,6 @@ abstract class EVMChainTransactionInfo extends TransactionInfo { 'from': from, 'evmSignatureName': evmSignatureName, 'contractAddress': contractAddress, + 'chainId': chainId, }; } diff --git a/cw_evm/lib/evm_chain_transaction_model.dart b/cw_evm/lib/evm_chain_transaction_model.dart index ca4fb0864..766c7b969 100644 --- a/cw_evm/lib/evm_chain_transaction_model.dart +++ b/cw_evm/lib/evm_chain_transaction_model.dart @@ -13,6 +13,7 @@ class EVMChainTransactionModel { final int? tokenDecimal; final bool isError; final String input; + final int chainId; String? evmSignatureName; EVMChainTransactionModel({ @@ -30,10 +31,11 @@ class EVMChainTransactionModel { required this.tokenDecimal, required this.isError, required this.input, + required this.chainId, this.evmSignatureName, }); - factory EVMChainTransactionModel.fromJson(Map json, String defaultSymbol) => + factory EVMChainTransactionModel.fromJson(Map json, String defaultSymbol, int chainId) => EVMChainTransactionModel( date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), hash: json["hash"] ?? "", @@ -50,5 +52,6 @@ class EVMChainTransactionModel { isError: json["isError"] == "1", input: json["input"] ?? "", evmSignatureName: json["evmSignatureName"], + chainId: chainId, ); } diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart index 3821d27d2..8e86f821a 100644 --- a/cw_evm/lib/evm_chain_wallet.dart +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -10,10 +10,10 @@ 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/node.dart'; -import 'package:cw_core/parse_fixed.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_addresses.dart'; @@ -21,13 +21,18 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_keys_file.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_client_factory.dart'; +import 'package:cw_evm/evm_chain_default_tokens.dart'; import 'package:cw_evm/evm_chain_exceptions.dart'; -import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_registry.dart'; import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/utils/evm_chain_formatter.dart'; import 'package:cw_evm/evm_chain_transaction_history.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/utils/evm_chain_utils.dart'; +import 'package:cw_evm/utils/network_chain_utils.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/hardware/evm_chain_bitbox_credentials.dart'; import 'package:cw_evm/hardware/evm_chain_ledger_credentials.dart'; @@ -60,7 +65,7 @@ const Map methodSignatureToType = { '0xd505accf': 'permit', }; -abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; +class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; abstract class EVMChainWalletBase extends WalletBase @@ -76,16 +81,17 @@ abstract class EVMChainWalletBase EVMChainERC20Balance? initialBalance, required this.encryptionFileUtils, this.passphrase, + int? initialChainId, }) : syncStatus = const NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = client, + selectedChainId = initialChainId ?? _getInitialChainId(walletInfo.type), walletAddresses = EVMChainWalletAddresses(walletInfo), balance = ObservableMap.of( { - // Not sure of this yet, will it work? will it not? nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero), }, ), @@ -107,15 +113,47 @@ abstract class EVMChainWalletBase late final Box erc20TokensBox; - late final Box evmChainErc20TokensBox; + late Box evmChainErc20TokensBox; late final Credentials _evmChainPrivateKey; Credentials get evmChainPrivateKey => _evmChainPrivateKey; - late final EVMChainClient _client; + late EVMChainClient _client; - bool hasPriorityFee = true; + @override + int? get chainId => selectedChainId; + + /// Currently selected chain ID for this wallet + @observable + int selectedChainId; + + /// Get chain configuration for currently selected chain + @computed + ChainConfig? get selectedChainConfig { + final registry = EvmChainRegistry(); + return registry.getChainConfig(selectedChainId); + } + + @override + @computed + CryptoCurrency get currency { + final config = selectedChainConfig; + if (config != null) { + return config.nativeCurrency; + } + + return super.currency; + } + + bool get hasPriorityFee => EVMChainUtils.hasPriorityFee(selectedChainId); + + /// Get initial chain ID from registry based on wallet type + static int _getInitialChainId(WalletType walletType) { + final registry = EvmChainRegistry(); + final chainConfig = registry.getChainConfigByWalletType(walletType); + return chainConfig?.chainId ?? 1; // Default to Ethereum if not found + } @observable String? nativeTxEstimatedFee; @@ -125,7 +163,6 @@ abstract class EVMChainWalletBase bool _isTransactionUpdating; - // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter Timer? _transactionsUpdateTimer; @override @@ -141,42 +178,197 @@ abstract class EVMChainWalletBase Completer sharedPrefs = Completer(); - //! Methods to be overridden by every child + //! Chain selection methods - void addInitialTokens(); + /// Select a different EVM network chain for this wallet + /// + /// This allows switching between EVM networks (Ethereum, Polygon, Base, Arbitrum, etc.) + /// without creating a new wallet. The selected chain ID is stored, the client is + /// immediately updated, and the wallet automatically connects to the node, updates + /// balance, and refreshes transactions for the selected network. + /// + /// Transactions are stored in separate files per network (based on chainId), so switching + /// networks automatically loads transactions from the correct file. + @action + Future selectChain(int chainId, {required Node node}) async { + if (EvmChainRegistry().getChainConfig(chainId) == null) { + throw Exception('Chain config not found for chainId: $chainId'); + } - // Future open({ - // required String name, - // required String password, - // required WalletInfo walletInfo, - // }); + if (selectedChainId == chainId) return; - List get getDefaultTokenContractAddresses; + _client.stop(); - Future initErc20TokensBox(); + balance.clear(); - String getTransactionHistoryFileName(); + selectedChainId = chainId; + _client = EVMChainClientFactory.createClient(selectedChainId); - Future checkIfScanProviderIsEnabled(); + // Automatically connect to node for the selected chain + await connectToNode(node: node); + + // Reload ERC20 tokens box for the new chain + await initErc20TokensBox(); + + // Reload transaction history from the new chain's file + await transactionHistory.init(); + + await save(); + + await startSync(); + } + + void addInitialTokens() { + final initialErc20Tokens = EVMChainDefaultTokens.getDefaultTokensByChainId(selectedChainId); + + 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), + ); + } + } + } + + List get getDefaultTokenContractAddresses => + EVMChainDefaultTokens.getDefaultTokenAddresses(selectedChainId); + + Future initErc20TokensBox() async { + // Migration for old WalletType.ethereum wallets: + // Old wallets used a global erc20TokensBox (shared across all wallets). + // New system uses wallet-specific, chain-specific boxes. + // This checks if migration is needed and runs it once. + if (walletInfo.type == WalletType.ethereum) { + try { + // Try to access erc20TokensBox - if it exists, migration already ran + final _ = erc20TokensBox; + // Migration done, proceed with normal chain-specific logic below + } catch (_) { + // erc20TokensBox doesn't exist yet, run migration from global box + await _initEthereumErc20TokensBox(); + return; + } + } + + final chainId = selectedChainId; + + final boxName = EVMChainUtils.getErc20TokensBoxName(walletInfo.name, chainId); + + // Close existing box if it's already open (for chain switching) + try { + if (evmChainErc20TokensBox.isOpen) { + await evmChainErc20TokensBox.close(); + } + } catch (_) { + // Box might not be initialized yet, ignore + } + + // Check if box is already open, if so use it, otherwise open it + if (CakeHive.isBoxOpen(boxName)) { + evmChainErc20TokensBox = CakeHive.box(boxName); + } else { + evmChainErc20TokensBox = await CakeHive.openBox(boxName); + } + + addInitialTokens(); + } + + /// Ethereum-specific initialization with backward compatibility + Future _initEthereumErc20TokensBox() async { + // Opens a box specific to this wallet + evmChainErc20TokensBox = await CakeHive.openBox( + "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}", + ); + + erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); + + if (erc20TokensBox.isEmpty) { + if (evmChainErc20TokensBox.isEmpty) addInitialTokens(); + return; + } + + final allValues = erc20TokensBox.values.toList(); + + // Clear and delete the old token box + await erc20TokensBox.clear(); + await erc20TokensBox.deleteFromDisk(); + + // Add all the previous tokens with configs to the new box + await evmChainErc20TokensBox.addAll(allValues); + } + + String getTransactionHistoryFileName() => + EVMChainUtils.getTransactionHistoryFileName(selectedChainId); + + Future checkIfScanProviderIsEnabled() async { + final key = EVMChainUtils.getScanProviderPreferenceKey(selectedChainId); + return (await sharedPrefs.future).getBool(key) ?? true; + } EVMChainTransactionInfo getTransactionInfo( - EVMChainTransactionModel transactionModel, String address); + EVMChainTransactionModel transactionModel, + String address, + ) { + return EVMChainTransactionInfo( + 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 ?? + EVMChainUtils.getDefaultTokenSymbol(transactionModel.chainId), + to: transactionModel.to, + from: transactionModel.from, + evmSignatureName: transactionModel.evmSignatureName, + contractAddress: transactionModel.contractAddress, + chainId: transactionModel.chainId, + ); + } - Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath); + 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 ?? EVMChainUtils.getDefaultTokenTag(selectedChainId), + iconPath: iconPath, + isPotentialScam: token.isPotentialScam, + ); + } EVMChainTransactionHistory setUpTransactionHistory( WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils, - ); + ) { + return EVMChainTransactionHistory( + walletInfo: walletInfo, + password: password, + encryptionFileUtils: encryptionFileUtils, + getCurrentChainId: () => selectedChainId, + ); + } String _getUSDCContractAddress() { - return switch (_client.chainId) { + return switch (selectedChainId) { 1 => "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 137 => "0x2791bca1f2de4661ed88a30c99a7a9449aa84174", 8453 => "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 42161 => "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - _ => throw Exception("Unsupported chain ID: ${_client.chainId}"), + _ => throw Exception("Unsupported chain ID: $selectedChainId"), }; } @@ -184,7 +376,7 @@ abstract class EVMChainWalletBase Future checkNodeHealth() async { try { // Check native balance - await _client.getBalance(_evmChainPrivateKey.address, throwOnError: true); + await _client.getBalance(_evmChainPrivateKey.address); // Check USDC token balance String usdcContractAddress = _getUSDCContractAddress(); @@ -210,7 +402,7 @@ abstract class EVMChainWalletBase // check for Already existing scam tokens, cuz users can get scammed twice ¯\_(ツ)_/¯ await _checkForExistingScamTokens(); - switch(walletInfo.hardwareWalletType) { + switch (walletInfo.hardwareWalletType) { case HardwareWalletType.ledger: _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address); walletAddresses.address = walletInfo.address; @@ -238,6 +430,12 @@ abstract class EVMChainWalletBase walletAddresses.address = _evmChainPrivateKey.address.hexEip55; break; } + + // Ensure balance is initialized for current currency (in case currency changed) + if (!balance.containsKey(currency)) { + balance[currency] = EVMChainERC20Balance(BigInt.zero); + } + await save(); } @@ -296,9 +494,6 @@ abstract class EVMChainWalletBase final erc20Fee = await _getErc20TxFee(priority); erc20TxEstimatedFee = erc20Fee.toString(); - - printV('Native Estimated Fee: $nativeTxEstimatedFee'); - printV('ERC20 Estimated Fee: $erc20TxEstimatedFee'); } Future _getNativeTxFee(TransactionPriority? priority) async { @@ -358,7 +553,8 @@ abstract class EVMChainWalletBase } } - int getTotalPriorityFee(EVMChainTransactionPriority priority); + int getTotalPriorityFee(EVMChainTransactionPriority priority) => + EVMChainUtils.getTotalPriorityFee(priority, selectedChainId); /// Allows more customization to the fetch estimatedFees flow. /// @@ -384,10 +580,31 @@ abstract class EVMChainWalletBase final gasBaseFee = await _client.getGasBaseFee(); final gasPrice = await _client.getGasUnitPrice(); + // Validate that we got valid gas price + if (gasPrice <= 0) { + printV('Invalid gas price received: $gasPrice'); + throw EVMChainTransactionFeesException('Failed to retrieve gas price from node'); + } + int maxFeePerGas; int adjustedGasPrice; - maxFeePerGas = gasBaseFee != null ? (gasBaseFee + priorityFee) : (gasPrice + priorityFee); + if (gasBaseFee != null && gasBaseFee > 0) { + // For chains with base fee, add priority fee (if supported) and a buffer to account for base fee increases + // Base fee can increase between estimation and transaction submission + final baseFeeWithPriority = gasBaseFee + priorityFee; + + // For chains without priority fees (e.g., Arbitrum), use a 5% buffer + // For chains with priority fees (e.g., Ethereum), use a 15% buffer to account for base fee volatility + // Base fee can increase significantly during high network activity + final bufferMultiplier = hasPriorityFee ? 115 : 105; + final bufferPercent = (baseFeeWithPriority * bufferMultiplier) ~/ 100; + final bufferMin = baseFeeWithPriority + (baseFeeWithPriority ~/ 100); + maxFeePerGas = bufferPercent > bufferMin ? bufferPercent : bufferMin; + } else { + // Fallback to gasPrice if baseFee is not available + maxFeePerGas = gasPrice + priorityFee; + } adjustedGasPrice = maxFeePerGas; @@ -410,7 +627,12 @@ abstract class EVMChainWalletBase gasPrice: adjustedGasPrice, ); } catch (e) { - return GasParamsHandler.zero(); + printV('Error calculating estimated fee: ${e.toString()}'); + if (e is EVMChainTransactionFeesException) { + rethrow; + } + throw EVMChainTransactionFeesException( + 'Failed to calculate transaction fees: ${e.toString()}'); } } @@ -459,12 +681,14 @@ abstract class EVMChainWalletBase syncStatus = FailedSyncStatus(); return; } - await _updateBalance(); - await _updateTransactions(); - await _getEstimatedFees( - hasPriorityFee ? EVMChainTransactionPriority.medium : null, - ); // We're using medium priority for default estimation + + await Future.wait([ + _updateTransactions(), + _getEstimatedFees( + hasPriorityFee ? EVMChainTransactionPriority.medium : null, + ), // We're using medium priority for default estimation + ]); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -487,9 +711,10 @@ abstract class EVMChainWalletBase } final transactionCurrency = balance.keys.firstWhere( - (currency) => - currency.title == _credentials.currency.title && - currency.tag == _credentials.currency.tag, + (currency) => + currency.title == _credentials.currency.title && + (currency.tag == _credentials.currency.tag || + currency.tag == _credentials.currency.title), orElse: () => throw Exception( 'Currency ${_credentials.currency.title} ${_credentials.currency.tag} is not accessible in the wallet, try to enable it first.')); @@ -514,11 +739,19 @@ abstract class EVMChainWalletBase throw EVMChainTransactionCreationException(transactionCurrency); } - final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble( - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); - - totalAmount = parseFixed( - EVMChainFormatter.truncateDecimals(totalOriginalAmount.toString(), exponent), exponent); + totalAmount = outputs.fold( + BigInt.zero, + (acc, output) { + if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { + return acc + + EVMChainFormatter.parseEVMChainAmountToBigInt( + output.cryptoAmount!, + decimals: exponent, + ); + } + return acc; + }, + ); final gasFeesModel = await calculateActualEstimatedFeeForCreateTransaction( amount: totalAmount, @@ -537,13 +770,14 @@ abstract class EVMChainWalletBase } else { final output = outputs.first; if (!output.sendAll) { - final totalOriginalAmount = - EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); - - totalAmount = parseFixed( - EVMChainFormatter.truncateDecimals(totalOriginalAmount.toString(), exponent), - exponent, - ); + if (output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { + totalAmount = EVMChainFormatter.parseEVMChainAmountToBigInt( + output.cryptoAmount!, + decimals: exponent, + ); + } else { + totalAmount = BigInt.zero; + } } if (output.sendAll && transactionCurrency is Erc20Token) { @@ -562,7 +796,7 @@ abstract class EVMChainWalletBase maxFeePerGasForTransaction = gasFeesModel.maxFeePerGas; if (output.sendAll && transactionCurrency is! Erc20Token) { - if (_client.chainId == 8453) { + if (selectedChainId == 8453) { // Applying a small buffer to account for gas price fluctuations // 10% or minimum 10,000 wei, whichever is higher final refinedGasFee = estimatedFeesForTransaction; @@ -581,7 +815,7 @@ abstract class EVMChainWalletBase // check the fees on the base currency if (estimatedFeesForTransaction > balance[currency]!.balance) { - throw EVMChainTransactionFeesException(currency.title); + throw EVMChainTransactionFeesException.fromCurrency(currency.title); } if (currencyBalance.balance < totalAmount) { @@ -592,7 +826,7 @@ abstract class EVMChainWalletBase if (transactionCurrency is Erc20Token && walletInfo.hardwareWalletType == HardwareWalletType.ledger) { await (_evmChainPrivateKey as EvmLedgerCredentials) - .provideERC20Info(transactionCurrency.contractAddress, _client.chainId); + .provideERC20Info(transactionCurrency.contractAddress, selectedChainId); } final pendingEVMChainTransaction = await _client.signTransaction( @@ -603,13 +837,14 @@ abstract class EVMChainWalletBase gasFee: estimatedFeesForTransaction, priority: _credentials.priority, currency: transactionCurrency, - feeCurrency: switch (_client.chainId) { 137 => "POL", _ => "ETH" }, + feeCurrency: switch (selectedChainId) { 137 => "POL", _ => "ETH" }, maxFeePerGas: maxFeePerGasForTransaction, exponent: exponent, contractAddress: transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, data: hexOpReturnMemo, gasPrice: maxFeePerGasForTransaction, + useBlinkProtection: _credentials.useBlinkProtection, ); return pendingEVMChainTransaction; @@ -619,8 +854,9 @@ abstract class EVMChainWalletBase String to, String dataHex, BigInt valueWei, - EVMChainTransactionPriority? priority, - ) async { + EVMChainTransactionPriority? priority, { + bool useBlinkProtection = true, + }) async { // Estimate gas with the SAME call (sender, to, value, data) final gas = await calculateActualEstimatedFeeForCreateTransaction( amount: valueWei, // native value (usually 0 for ERC20 transfer) @@ -630,7 +866,7 @@ abstract class EVMChainWalletBase data: _client.hexToBytes(dataHex), ); - final nativeCurrency = switch (_client.chainId) { + final nativeCurrency = switch (selectedChainId) { 137 => CryptoCurrency.maticpoly, 8453 => CryptoCurrency.baseEth, 42161 => CryptoCurrency.arbEth, @@ -655,16 +891,13 @@ abstract class EVMChainWalletBase contractAddress: null, data: dataHex, gasPrice: gas.gasPrice, + useBlinkProtection: useBlinkProtection, ); } - 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, + {bool useBlinkProtection = true}) async { final CryptoCurrency transactionCurrency = balance.keys.firstWhere((element) => element.title == token.title); assert(transactionCurrency is Erc20Token); @@ -695,6 +928,7 @@ abstract class EVMChainWalletBase exponent: transactionCurrency.decimal, contractAddress: transactionCurrency.contractAddress, gasPrice: gasFeesModel.gasPrice, + useBlinkProtection: useBlinkProtection, ); } @@ -814,8 +1048,9 @@ abstract class EVMChainWalletBase String toJSON() => json.encode({ 'mnemonic': _mnemonic, 'private_key': privateKey, - 'balance': balance[currency]!.toJSON(), + 'balance': balance[currency]?.toJSON() ?? EVMChainERC20Balance(BigInt.zero).toJSON(), 'passphrase': passphrase, + 'selected_chain_id': selectedChainId, }); Future _updateBalance() async { @@ -826,12 +1061,66 @@ abstract class EVMChainWalletBase } Future _fetchEVMChainBalance() async { - final balance = await _client.getBalance(_evmChainPrivateKey.address); - return EVMChainERC20Balance(balance.getInWei); + try { + final balance = await _client.getBalance(_evmChainPrivateKey.address); + + return EVMChainERC20Balance(balance.getInWei); + } catch (_) { + return balance[currency] ?? EVMChainERC20Balance(BigInt.zero); + } + } + + bool _isTokenMatchingChain(Erc20Token token) { + final registry = EvmChainRegistry(); + + if (token.tag != null) { + final chainConfig = registry.getChainConfigByTag(token.tag!); + if (chainConfig != null) return chainConfig.chainId == selectedChainId; + } + + if (currency.tag == null) return token.tag == currency.title; + + return token.tag?.toLowerCase() == currency.tag?.toLowerCase(); } Future _fetchErc20Balances() async { - for (var token in evmChainErc20TokensBox.values) { + // Check if box is open before accessing it + if (!evmChainErc20TokensBox.isOpen) { + return; + } + + // First, clean up any tokens in balance map that don't belong to current chain + // This handles tokens from previous chains that might still be in the balance map + final tokensInBalance = balance.keys.whereType().toList(); + final tokensInBox = evmChainErc20TokensBox.values.toList(); + final boxTokenAddresses = tokensInBox.map((t) => t.contractAddress.toLowerCase()).toSet(); + + for (var token in tokensInBalance) { + // Remove token if it's not in the current box or doesn't match current chain + if (!boxTokenAddresses.contains(token.contractAddress.toLowerCase()) || + !_isTokenMatchingChain(token)) { + balance.remove(token); + } + } + + // Get a snapshot of tokens from current box to avoid issues if box is closed during iteration + final tokens = tokensInBox; + + for (var token in tokens) { + // Check if box is still open before operating on tokens + if (!evmChainErc20TokensBox.isOpen) break; + + if (!_isTokenMatchingChain(token)) { + printV('NOTEE!!!: Token ${token.title} is not matching the currency ${currency.title}'); + try { + await deleteErc20Token(token, shouldUpdateBalance: false); + } catch (e) { + balance.remove(token); + printV('Error deleting token ${token.title}: $e'); + } + continue; + } + try { if (token.enabled) { balance[token] = await _client.fetchERC20Balances( @@ -859,7 +1148,7 @@ abstract class EVMChainWalletBase final erc20 = ERC20( client: _client.getWeb3Client()!, address: EthereumAddress.fromHex(tokenContract), - chainId: _client.chainId, + chainId: selectedChainId, ); final allowance = await erc20.allowance(owner, EthereumAddress.fromHex(spender)); @@ -899,7 +1188,15 @@ abstract class EVMChainWalletBase @override Future updateTransactionsHistory() async => await _updateTransactions(); - List get erc20Currencies => evmChainErc20TokensBox.values.toList(); + List get erc20Currencies { + try { + if (!evmChainErc20TokensBox.isOpen) return []; + + return evmChainErc20TokensBox.values.toList(); + } catch (_) { + return []; + } + } Future addErc20Token(Erc20Token token) async { String? iconPath; @@ -935,12 +1232,25 @@ abstract class EVMChainWalletBase } } - Future deleteErc20Token(Erc20Token token) async { - await token.delete(); + Future deleteErc20Token(Erc20Token token, {bool shouldUpdateBalance = true}) async { + // Check if box is open before trying to delete + if (!evmChainErc20TokensBox.isOpen) { + balance.remove(token); + return; + } + + try { + await token.delete(); + } catch (e) { + // Token might be from a closed box, just remove from balance + printV('Error deleting token from box: $e'); + } balance.remove(token); await removeTokenTransactionsInHistory(token); - _updateBalance(); + if (shouldUpdateBalance) { + _updateBalance(); + } } Future removeTokenTransactionsInHistory(Erc20Token token) async { @@ -948,14 +1258,100 @@ abstract class EVMChainWalletBase await transactionHistory.save(); } - Future getErc20Token(String contractAddress, String chainName) async => - await _client.getErc20Token(contractAddress, chainName); + Future getErc20Token(String contractAddress, String chainName) async { + try { + return await _client.getErc20Token(contractAddress, chainName); + } catch (e) { + printV('Error getting ERC20 token: $e'); + rethrow; + } + } void _onNewTransaction() { _updateBalance(); _updateTransactions(); } + /// Static method to open an existing wallet + 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 the 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, + ); + } + + final savedChainId = data?['selected_chain_id'] as int?; + + final registry = EvmChainRegistry(); + + // Get chainId from wallet type, use saved chainId if available (for chain switching) + final defaultChainId = registry.getChainConfigByWalletType(walletInfo.type)?.chainId; + if (defaultChainId == null) { + throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + } + + // Use saved chainId if available, otherwise default to wallet type's chainId + final chainId = savedChainId ?? defaultChainId; + + final chainConfig = registry.getChainConfig(chainId); + if (chainConfig == null) { + throw Exception('Chain config not found for chainId: $chainId'); + } + + final client = EVMChainClientFactory.createClient(chainId); + + // Use saved chainId if available, otherwise use the computed chainId + final initialChainIdForWallet = savedChainId ?? chainId; + + return EVMChainWallet( + walletInfo: walletInfo, + derivationInfo: await walletInfo.getDerivationInfo(), + password: password, + mnemonic: keysData.mnemonic, + privateKey: keysData.privateKey, + passphrase: keysData.passphrase, + initialBalance: balance, + client: client, + nativeCurrency: chainConfig.nativeCurrency, + encryptionFileUtils: encryptionFileUtils, + initialChainId: initialChainIdForWallet, + ); + } + @override Future renameWalletFiles(String newWalletName) async { final transactionHistoryFileNameForWallet = getTransactionHistoryFileName(); diff --git a/cw_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart index ea6e68a23..d63686225 100644 --- a/cw_evm/lib/evm_chain_wallet_service.dart +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -1,16 +1,24 @@ import 'dart:io'; -import 'package:collection/collection.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/encryption_file_utils.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/clients/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_client_factory.dart'; +import 'package:cw_evm/evm_chain_registry.dart'; import 'package:cw_evm/evm_chain_wallet.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; -import 'package:hive/hive.dart'; -abstract class EVMChainWalletService extends WalletService< +/// Unified service for all EVM chains (Ethereum, Polygon, Base, Arbitrum, etc.) +/// +/// This service dynamically determines which chain to use based on WalletType +/// from credentials or walletInfo, eliminating the need for separate service +/// classes per chain. +class EVMChainWalletService extends WalletService< EVMChainNewWalletCredentials, EVMChainRestoreWalletFromSeedCredentials, EVMChainRestoreWalletFromPrivateKey, @@ -18,39 +26,354 @@ abstract class EVMChainWalletService extends WalletSer EVMChainWalletService(this.isDirect); final bool isDirect; + final EvmChainRegistry _registry = EvmChainRegistry(); + + List get _evmWalletTypes { + return _registry.getRegisteredWalletTypes(); + } + + Future _findWalletByName(String name) async { + for (final type in _evmWalletTypes) { + final walletInfo = await WalletInfo.get(name, type); + if (walletInfo != null) { + return walletInfo; + } + } + return null; + } + + /// getType() is not meaningful for this unified service, it throws to prevent misuse + @override + WalletType getType() { + throw UnsupportedError( + 'EVMChainWalletService is unified and does not have a single type. ' + 'Use walletInfo.type instead.', + ); + } + + /// Override saveBackup to look up walletType from wallet name + /// Optionally accepts walletInfo to avoid lookup (useful during rename) + @override + Future saveBackup(String name, {WalletInfo? walletInfo}) async { + final info = walletInfo ?? await _findWalletByName(name); + if (info == null) { + throw Exception('Wallet not found: $name'); + } + + final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: info.type); + final walletDirPath = await pathForWalletDir(name: name, type: info.type); + + if (File(walletDirPath).existsSync()) { + await File(walletDirPath).copy(backupWalletDirPath); + } + } + + /// Override restoreWalletFilesFromBackup to look up walletType from wallet name + @override + Future restoreWalletFilesFromBackup(String name) async { + final walletInfo = await _findWalletByName(name); + if (walletInfo == null) { + throw Exception('Wallet not found: $name'); + } + + final backupWalletDirPath = await pathForWalletDir(name: "$name.backup", type: walletInfo.type); + final walletDirPath = await pathForWalletDir(name: name, type: walletInfo.type); + + if (File(backupWalletDirPath).existsSync()) { + await File(backupWalletDirPath).copy(walletDirPath); + } + } @override - WalletType getType(); + Future create( + EVMChainNewWalletCredentials credentials, { + bool? isTestnet, + }) async { + final walletInfo = credentials.walletInfo!; + + // Get chainId from wallet type + final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); + if (chainConfig == null) { + throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + } + final initialChainId = chainConfig.chainId; + + final client = EVMChainClientFactory.createClient(initialChainId); + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + final mnemonic = credentials.mnemonic ?? bip39.generateMnemonic(strength: strength); + + final derivationInfo = await walletInfo.getDerivationInfo(); + if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) { + derivationInfo.derivationPath = "m/44'/60'/0'/0"; + derivationInfo.derivationType = DerivationType.bip39; + await derivationInfo.save(); + } + + final wallet = _createWalletInstance( + walletType: walletInfo.type, + walletInfo: walletInfo, + derivationInfo: derivationInfo, + mnemonic: mnemonic, + password: credentials.password!, + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + initialChainId: initialChainId, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } @override - Future create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}); - - @override - Future restoreFromHardwareWallet(EVMChainRestoreWalletFromHardware credentials); - - @override - Future openWallet(String name, String password); - - @override - Future rename(String currentName, String password, String newName); - - @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet}); - - @override - Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}); - - @override - Future isWalletExit(String name) async => - File(await pathForWallet(name: name, type: getType())).existsSync(); - - @override - Future remove(String wallet) async { - File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); - final walletInfo = await WalletInfo.get(wallet, getType()); + Future openWallet(String name, String password) async { + final walletInfo = await _findWalletByName(name); if (walletInfo == null) { throw Exception('Wallet not found'); } + + try { + final wallet = await _openWalletInstance( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + await saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await _openWalletInstance( + name: name, + password: password, + walletInfo: walletInfo, + 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 _findWalletByName(currentName); + if (currentWalletInfo == null) { + throw Exception('Wallet not found'); + } + + final currentWallet = await _openWalletInstance( + password: password, + name: currentName, + walletInfo: currentWalletInfo, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + ); + + await currentWallet.renameWalletFiles(newName); + + // Update walletInfo with new name before saving backup + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, currentWalletInfo.type); + newWalletInfo.name = newName; + + // Pass walletInfo to saveBackup to avoid lookup (since WalletInfo not saved yet) + await saveBackup(newName, walletInfo: newWalletInfo); + + await newWalletInfo.save(); + } + + @override + Future restoreFromSeed( + EVMChainRestoreWalletFromSeedCredentials credentials, { + bool? isTestnet, + }) async { + final walletInfo = credentials.walletInfo!; + + // Get chainId from wallet type + final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); + if (chainConfig == null) { + throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + } + final initialChainId = chainConfig.chainId; + + final client = EVMChainClientFactory.createClient(initialChainId); + + final derivationInfo = await walletInfo.getDerivationInfo(); + if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) { + derivationInfo.derivationPath = "m/44'/60'/0'/0"; + derivationInfo.derivationType = DerivationType.bip39; + await derivationInfo.save(); + } + + final wallet = _createWalletInstance( + walletType: walletInfo.type, + walletInfo: walletInfo, + derivationInfo: derivationInfo, + mnemonic: credentials.mnemonic, + password: credentials.password!, + passphrase: credentials.passphrase, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + initialChainId: initialChainId, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromKeys( + EVMChainRestoreWalletFromPrivateKey credentials, { + bool? isTestnet, + }) async { + final walletInfo = credentials.walletInfo!; + + // Get chainId from wallet type + final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); + if (chainConfig == null) { + throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + } + final initialChainId = chainConfig.chainId; + + final client = EVMChainClientFactory.createClient(initialChainId); + + final derivationInfo = await walletInfo.getDerivationInfo(); + if (derivationInfo.derivationPath == null || derivationInfo.derivationPath!.isEmpty) { + derivationInfo.derivationPath = "m/44'/60'/0'/0"; + derivationInfo.derivationType = DerivationType.bip39; + await derivationInfo.save(); + } + + final wallet = _createWalletInstance( + walletType: walletInfo.type, + walletInfo: walletInfo, + derivationInfo: derivationInfo, + privateKey: credentials.privateKey, + password: credentials.password!, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + initialChainId: initialChainId, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + EVMChainRestoreWalletFromHardware credentials, + ) async { + final walletInfo = credentials.walletInfo!; + + // Get chainId from wallet type + final chainConfig = _registry.getChainConfigByWalletType(walletInfo.type); + if (chainConfig == null) { + throw Exception('Chain config not found for wallet type: ${walletInfo.type}'); + } + final initialChainId = chainConfig.chainId; + + final client = EVMChainClientFactory.createClient(initialChainId); + final derivationInfo = await walletInfo.getDerivationInfo(); + derivationInfo.derivationType = DerivationType.bip39; + derivationInfo.derivationPath = "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"; + await derivationInfo.save(); + walletInfo.hardwareWalletType = credentials.hardwareWalletType; + walletInfo.address = credentials.hwAccountData.address; + await walletInfo.save(); + + final wallet = _createWalletInstance( + walletType: walletInfo.type, + walletInfo: walletInfo, + derivationInfo: derivationInfo, + password: credentials.password!, + client: client, + encryptionFileUtils: encryptionFileUtilsFor(isDirect), + initialChainId: initialChainId, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + return wallet; + } + + @override + Future isWalletExit(String name) async { + for (final type in _evmWalletTypes) { + if (File(await pathForWallet(name: name, type: type)).existsSync()) { + return true; + } + } + return false; + } + + @override + Future remove(String wallet) async { + final walletInfo = await _findWalletByName(wallet); + if (walletInfo == null) { + throw Exception('Wallet not found'); + } + + File(await pathForWalletDir(name: wallet, type: walletInfo.type)).delete(recursive: true); await WalletInfo.delete(walletInfo); } + + EVMChainWallet _createWalletInstance({ + required WalletType walletType, + required WalletInfo walletInfo, + required DerivationInfo derivationInfo, + String? mnemonic, + String? privateKey, + required String password, + required EVMChainClient client, + required EncryptionFileUtils encryptionFileUtils, + String? passphrase, + int? initialChainId, + }) { + final chainConfig = _registry.getChainConfigByWalletType(walletType); + + if (chainConfig == null) { + throw Exception('Chain config not found for wallet type: $walletType'); + } + + return EVMChainWallet( + walletInfo: walletInfo, + derivationInfo: derivationInfo, + mnemonic: mnemonic, + privateKey: privateKey, + password: password, + passphrase: passphrase, + client: client, + nativeCurrency: chainConfig.nativeCurrency, + encryptionFileUtils: encryptionFileUtils, + initialChainId: initialChainId, + ); + } + + Future _openWalletInstance({ + required String name, + required String password, + required WalletInfo walletInfo, + required EncryptionFileUtils encryptionFileUtils, + }) { + return EVMChainWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + encryptionFileUtils: encryptionFileUtils, + ); + } } diff --git a/cw_evm/lib/tokens/arbitrum_tokens.dart b/cw_evm/lib/tokens/arbitrum_tokens.dart new file mode 100644 index 000000000..34470fdfc --- /dev/null +++ b/cw_evm/lib/tokens/arbitrum_tokens.dart @@ -0,0 +1,83 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +/// Default ERC20 tokens for Arbitrum +class ArbitrumTokens { + static List get tokens { + final tokens = [ + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0x912CE59144191C1204E64559FE8253a0e49E6548", + decimal: 18, + enabled: true, + ), + 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, + ), + ]; + + return tokens.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_evm/lib/tokens/base_tokens.dart b/cw_evm/lib/tokens/base_tokens.dart new file mode 100644 index 000000000..6169bc923 --- /dev/null +++ b/cw_evm/lib/tokens/base_tokens.dart @@ -0,0 +1,76 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +/// Default ERC20 tokens for Base +class BaseTokens { + static List get tokens { + final tokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDe", + symbol: "USDe", + contractAddress: "0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Bridged Tether USD", + symbol: "USDT", + contractAddress: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x4200000000000000000000000000000000000006", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "SPX6900", + symbol: "SPX", + contractAddress: "0x50dA645f148798F68EF2d7dB7C1CB22A6819bb2C", + decimal: 8, + enabled: false, + ), + ]; + + return tokens.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: 'BASE'); + }).toList(); + } +} + diff --git a/cw_evm/lib/tokens/ethereum_tokens.dart b/cw_evm/lib/tokens/ethereum_tokens.dart new file mode 100644 index 000000000..4b0df879b --- /dev/null +++ b/cw_evm/lib/tokens/ethereum_tokens.dart @@ -0,0 +1,97 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +/// Default ERC20 tokens for Ethereum Mainnet +class EthereumTokens { + static List get tokens { + final tokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Decentralized Euro", + symbol: "DEURO", + contractAddress: "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + ]; + + return tokens.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: 'ETH'); + }).toList(); + } +} + diff --git a/cw_evm/lib/tokens/polygon_tokens.dart b/cw_evm/lib/tokens/polygon_tokens.dart new file mode 100644 index 000000000..24957a17a --- /dev/null +++ b/cw_evm/lib/tokens/polygon_tokens.dart @@ -0,0 +1,90 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +/// Default ERC20 tokens for Polygon +class PolygonTokens { + static List get tokens { + final tokens = [ + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Tether USD (PoS)", + symbol: "USDT", + contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin (POS)", + symbol: "USDC.e", + contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Decentralized Euro", + symbol: "DEURO", + contractAddress: "0xC2ff25dD99e467d2589b2c26EDd270F220F14E47", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Avalanche Token", + symbol: "AVAX", + contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC (PoS)", + symbol: "WBTC", + contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Dai (PoS)", + symbol: "DAI", + contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "SHIBA INU (PoS)", + symbol: "SHIB", + contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec", + decimal: 18, + enabled: false, + ), + ]; + + return tokens.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: 'POL'); + }).toList(); + } +} + diff --git a/cw_evm/lib/utils/evm_chain_formatter.dart b/cw_evm/lib/utils/evm_chain_formatter.dart new file mode 100644 index 000000000..f9f3a73bd --- /dev/null +++ b/cw_evm/lib/utils/evm_chain_formatter.dart @@ -0,0 +1,57 @@ +import 'dart:math'; + +class EVMChainFormatter { + static const int evmDecimals = 18; + + static int parseEVMChainAmount(String amount) { + try { + return (double.parse(amount) * pow(10, evmDecimals)).round(); + } catch (_) { + return 0; + } + } + + /// Parse EVM chain amount to BigInt to avoid integer overflow for large amounts + /// [decimals] defaults to 18 for native ETH/POL, but should be set to token decimals for ERC20 tokens + static BigInt parseEVMChainAmountToBigInt(String amount, {int decimals = evmDecimals}) { + try { + String cleanAmount = amount.replaceAll(',', '.'); + + bool isNegative = cleanAmount.startsWith('-'); + if (isNegative) { + cleanAmount = cleanAmount.substring(1); + } + + final parts = cleanAmount.split('.'); + String whole = parts[0].isEmpty ? '0' : parts[0]; + String fraction = parts.length > 1 ? parts[1] : ''; + + // 3. Strict Truncation Logic + // If fraction is longer than decimals, strictly cut it off. + // If shorter, pad it with zeros. + if (fraction.length > decimals) { + fraction = fraction.substring(0, decimals); + } else { + fraction = fraction.padRight(decimals, '0'); + } + + final wholeBigInt = BigInt.parse(whole); + final fractionBigInt = BigInt.parse(fraction); + final multiplier = BigInt.from(10).pow(decimals); + + return (wholeBigInt * multiplier) + fractionBigInt; + } catch (_) { + return BigInt.zero; + } + } + + static String truncateDecimals(String amount, int decimals) { + final parts = amount.split("."); + + if (parts.length == 2) { + parts[1] = parts[1].substring(0, parts[1].length > decimals ? decimals : parts[1].length); + } + + return parts.join("."); + } +} diff --git a/cw_evm/lib/utils/evm_chain_utils.dart b/cw_evm/lib/utils/evm_chain_utils.dart new file mode 100644 index 000000000..f824bcd90 --- /dev/null +++ b/cw_evm/lib/utils/evm_chain_utils.dart @@ -0,0 +1,115 @@ +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:web3dart/web3dart.dart' show EtherAmount, EtherUnit; + +/// Utility class for chain-specific EVM chain operations +class EVMChainUtils { + static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) { + return switch (chainId) { + 1 => _ethereumPriorityFee(priority), + 137 => _polygonPriorityFee(priority), + 8453 => _basePriorityFee(priority), + 42161 => 0, // Arbitrum doesn't use priority fees + _ => _ethereumPriorityFee(priority), + }; + } + + static bool hasPriorityFee(int chainId) { + return switch (chainId) { + 42161 => false, // Arbitrum doesn't use priority fees + _ => true, + }; + } + + static String getErc20TokensBoxName(String walletName, int chainId) { + final sanitizedName = walletName.replaceAll(" ", "_"); + + return switch (chainId) { + 1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}", + 137 => "${sanitizedName}_${Erc20Token.polygonBoxName}", + 8453 => "${sanitizedName}_${Erc20Token.baseBoxName}", + 42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}", + _ => "${sanitizedName}_${Erc20Token.ethereumBoxName}", + }; + } + + static String getTransactionHistoryFileName(int chainId) { + return switch (chainId) { + 1 => 'transactions.json', // Ethereum + 137 => 'polygon_transactions.json', + 8453 => 'base_transactions.json', + 42161 => 'arbitrum_transactions.json', + _ => 'transactions_$chainId.json', // Generic format for other chains + }; + } + + /// Get scan provider preference key for a wallet type + static String getScanProviderPreferenceKey(int chainId) { + return switch (chainId) { + 1 => 'use_etherscan', + 137 => 'use_polygonscan', + 8453 => 'use_basescan', + 42161 => 'use_arbiscan', + _ => 'use_etherscan', + }; + } + + static String getDefaultTokenTag(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'POL', + 8453 => 'BASE', + 42161 => 'ARB', + _ => 'ETH', + }; + } + + static String getFeeCurrency(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'MATIC', + 8453 => 'ETH', + 42161 => 'ETH', + _ => 'ETH', + }; + } + + static String getDefaultTokenSymbol(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'MATIC', + 8453 => 'BASE', + 42161 => 'ARBITRUM', + _ => 'ETH', + }; + } + + static int _ethereumPriorityFee(EVMChainTransactionPriority priority) { + return EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + } + + // Polygon priority fee calculation (minimum 25 gwei + additional based on priority) + static int _polygonPriorityFee(EVMChainTransactionPriority priority) { + const int minPriorityFee = 25; + final minPriorityFeeWei = EtherAmount.fromInt(EtherUnit.gwei, minPriorityFee).getInWei.toInt(); + + final int additionalPriorityFee = switch (priority) { + EVMChainTransactionPriority.slow => 0, + EVMChainTransactionPriority.medium => + EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(), + EVMChainTransactionPriority.fast => EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(), + _ => 0, + }; + + return minPriorityFeeWei + additionalPriorityFee; + } + + static int _basePriorityFee(EVMChainTransactionPriority priority) { + return switch (priority) { + EVMChainTransactionPriority.fast => EtherAmount.fromInt(EtherUnit.mwei, 5).getInWei.toInt(), + EVMChainTransactionPriority.medium => EtherAmount.fromInt(EtherUnit.mwei, 3).getInWei.toInt(), + EVMChainTransactionPriority.slow => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), + _ => EtherAmount.fromInt(EtherUnit.mwei, 1).getInWei.toInt(), + }; + } +} diff --git a/cw_evm/lib/utils/network_chain_utils.dart b/cw_evm/lib/utils/network_chain_utils.dart new file mode 100644 index 000000000..21ddf6f62 --- /dev/null +++ b/cw_evm/lib/utils/network_chain_utils.dart @@ -0,0 +1,63 @@ +import 'package:cw_core/crypto_currency.dart'; + +/// Immutable configuration for an EVM chain +class ChainConfig { + final int chainId; + final String name; + final String shortCode; + final String caip2; // e.g., "eip155:1" + final CryptoCurrency nativeCurrency; + final ChainCapabilities capabilities; + final List defaultRpcEndpoints; + final List explorerUrls; + final FeeModel feeModel; + + const ChainConfig({ + required this.chainId, + required this.name, + required this.shortCode, + required this.caip2, + required this.nativeCurrency, + required this.capabilities, + required this.defaultRpcEndpoints, + required this.explorerUrls, + required this.feeModel, + }); +} + +/// Capabilities supported by an EVM chain +class ChainCapabilities { + final bool supportsERC20; + final bool supportsEIP1559; + final bool supportsInternalTx; + final bool supportsSubscriptions; + final bool supportsENS; + + const ChainCapabilities({ + required this.supportsERC20, + required this.supportsEIP1559, + required this.supportsInternalTx, + required this.supportsSubscriptions, + required this.supportsENS, + }); +} + +/// Fee model type for EVM chains +enum FeeType { + legacy, + eip1559, +} + +/// Fee model configuration for an EVM chain +class FeeModel { + final FeeType type; + final int defaultGasLimit; + final int? maxPriorityFee; + + const FeeModel({ + required this.type, + required this.defaultGasLimit, + this.maxPriorityFee, + }); +} + diff --git a/cw_evm/test/cw_evm_test.dart b/cw_evm/test/cw_evm_test.dart index 558a7349c..5a60f4934 100644 --- a/cw_evm/test/cw_evm_test.dart +++ b/cw_evm/test/cw_evm_test.dart @@ -1,6 +1,6 @@ import "dart:typed_data"; -import "package:cw_evm/evm_chain_formatter.dart"; +import "package:cw_evm/utils/evm_chain_formatter.dart"; import "package:cw_evm/utils/rlp_decode.dart"; import "package:flutter_test/flutter_test.dart"; import "package:web3dart/crypto.dart"; diff --git a/cw_polygon/.gitignore b/cw_polygon/.gitignore deleted file mode 100644 index 96486fd93..000000000 --- a/cw_polygon/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.packages -build/ diff --git a/cw_polygon/.metadata b/cw_polygon/.metadata deleted file mode 100644 index fa347fc6a..000000000 --- a/cw_polygon/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: f468f3366c26a5092eb964a230ce7892fda8f2f8 - channel: stable - -project_type: package diff --git a/cw_polygon/CHANGELOG.md b/cw_polygon/CHANGELOG.md deleted file mode 100644 index 41cc7d819..000000000 --- a/cw_polygon/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/cw_polygon/LICENSE b/cw_polygon/LICENSE deleted file mode 100644 index ba75c69f7..000000000 --- a/cw_polygon/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/cw_polygon/README.md b/cw_polygon/README.md deleted file mode 100644 index 300e5c968..000000000 --- a/cw_polygon/README.md +++ /dev/null @@ -1,63 +0,0 @@ -## cw_polygon - -Polygon (PoS) wallet module built on the shared EVM base (`cw_evm`). Supports native MATIC and ERC‑20 tokens with PolygonScan-backed history. - -### Features - -- EVM chain integration (chainId 137) with `web3dart`. -- Default ERC‑20 token list and per‑wallet token box. -- History via Etherscan v2 API (Polygon chain id) and internal tx support. -- Fee handling tuned for Polygon (priority fee floor, legacy gasPrice when needed). -- Create/sign native and ERC‑20 transfers; approvals; send/broadcast. -- Manage tokens (enable/disable, add/remove) and balances. -- Message signing and verification. - -### Getting started - -Provide secrets used by the shared EVM layer in `cw_evm/lib/.secrets.g.dart`: - -```dart -// cw_evm/lib/.secrets.g.dart (DO NOT COMMIT) -const String etherScanApiKey = 'YOUR_ETHERSCAN_KEY'; -const String nowNodesApiKey = 'YOUR_NOWNODES_KEY'; // if using matic.nownodes.io -const String moralisApiKey = 'YOUR_MORALIS_KEY'; // optional: ERC20 metadata -``` - -Connect and sync: - -```dart -final service = PolygonWalletService(walletInfoBox, true, client: PolygonClient()); -final wallet = await service.create(EVMChainNewWalletCredentials(name: 'My POL', password: 'secret')); -await wallet.connectToNode(node: Node(uriRaw: 'polygon-rpc.com', isSSL: true)); -await wallet.startSync(); -``` - -### Usage - -Send MATIC: - -```dart -final pending = await wallet.createTransaction( - EVMChainTransactionCredentials.single( - address: '0x...', - cryptoAmount: '0.2', - currency: CryptoCurrency.maticpoly, - priority: EVMChainTransactionPriority.medium, - ), -); -final hash = await pending.commit(); -``` - -Add an ERC‑20 token and refresh balance: - -```dart -final token = await wallet.getErc20Token('0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', 'polygon'); // USDC -if (token != null) { - await wallet.addErc20Token(token); -} -``` - -### Additional information - -- Toggle PolygonScan usage via shared preferences key `use_polygonscan`. -- See `lib/` for APIs: `PolygonClient`, `PolygonWallet`, `PolygonWalletService`. diff --git a/cw_polygon/analysis_options.yaml b/cw_polygon/analysis_options.yaml deleted file mode 100644 index a5744c1cf..000000000 --- a/cw_polygon/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -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_polygon/devtools_options.yaml b/cw_polygon/devtools_options.yaml deleted file mode 100644 index fa0b357c4..000000000 --- a/cw_polygon/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/cw_polygon/lib/cw_polygon.dart b/cw_polygon/lib/cw_polygon.dart deleted file mode 100644 index 5d4e447d1..000000000 --- a/cw_polygon/lib/cw_polygon.dart +++ /dev/null @@ -1,7 +0,0 @@ -library cw_polygon; - -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} diff --git a/cw_polygon/lib/default_polygon_erc20_tokens.dart b/cw_polygon/lib/default_polygon_erc20_tokens.dart deleted file mode 100644 index 01ea2d7ab..000000000 --- a/cw_polygon/lib/default_polygon_erc20_tokens.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; - -class DefaultPolygonErc20Tokens { - final List _defaultTokens = [ - Erc20Token( - name: "Wrapped Ether", - symbol: "WETH", - contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Tether USD (PoS)", - symbol: "USDT", - contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "USD Coin", - symbol: "USDC", - contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "USD Coin (POS)", - symbol: "USDC.e", - contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", - decimal: 6, - enabled: true, - ), - Erc20Token( - name: "Decentralized Euro", - symbol: "DEURO", - contractAddress: "0xC2ff25dD99e467d2589b2c26EDd270F220F14E47", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "Avalanche Token", - symbol: "AVAX", - contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Wrapped BTC (PoS)", - symbol: "WBTC", - contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", - decimal: 8, - enabled: false, - ), - Erc20Token( - name: "Dai (PoS)", - symbol: "DAI", - contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", - decimal: 18, - enabled: true, - ), - Erc20Token( - name: "SHIBA INU (PoS)", - symbol: "SHIB", - contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec", - decimal: 18, - enabled: false, - ), - Erc20Token( - name: "Uniswap (PoS)", - symbol: "UNI", - contractAddress: "0xb33EaAd8d922B1083446DC23f610c2567fB5180f", - decimal: 18, - enabled: false, - ), - ]; - - List get initialPolygonErc20Tokens => _defaultTokens.map((token) { - String? iconPath; - if (token.iconPath?.isEmpty ?? true) { - try { - iconPath = CryptoCurrency.all - .firstWhere((element) => - element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase()) - .iconPath; - } catch (_) {} - } else { - iconPath = token.iconPath; - } - - return Erc20Token.copyWith(token, icon: iconPath, tag: 'POL'); - }).toList(); -} diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart deleted file mode 100644 index 1e3d547a7..000000000 --- a/cw_polygon/lib/polygon_client.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'dart:convert'; - -import 'package:cw_evm/evm_chain_client.dart'; -import 'package:cw_evm/.secrets.g.dart' as secrets; -import 'package:cw_evm/evm_chain_transaction_model.dart'; -import 'package:flutter/foundation.dart'; -import 'package:web3dart/web3dart.dart'; - -class PolygonClient 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 => 137; - - @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, 'MATIC'), - ) - .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, 'MATIC')) - .toList(); - } - - return []; - } catch (_) { - return []; - } - } -} diff --git a/cw_polygon/lib/polygon_mnemonics_exception.dart b/cw_polygon/lib/polygon_mnemonics_exception.dart deleted file mode 100644 index c1a2fcc84..000000000 --- a/cw_polygon/lib/polygon_mnemonics_exception.dart +++ /dev/null @@ -1,5 +0,0 @@ -class PolygonMnemonicIsIncorrectException implements Exception { - @override - String toString() => - 'Polygon mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; -} diff --git a/cw_polygon/lib/polygon_transaction_history.dart b/cw_polygon/lib/polygon_transaction_history.dart deleted file mode 100644 index 27547b7ee..000000000 --- a/cw_polygon/lib/polygon_transaction_history.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:core'; - -import 'package:cw_evm/evm_chain_transaction_history.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; -import 'package:cw_polygon/polygon_transaction_info.dart'; - -class PolygonTransactionHistory extends EVMChainTransactionHistory { - PolygonTransactionHistory({ - required super.walletInfo, - required super.password, - required super.encryptionFileUtils, - }); - - @override - String getTransactionHistoryFileName() => 'polygon_transactions.json'; - - @override - EVMChainTransactionInfo getTransactionInfo(Map val) => - PolygonTransactionInfo.fromJson(val); -} diff --git a/cw_polygon/lib/polygon_transaction_info.dart b/cw_polygon/lib/polygon_transaction_info.dart deleted file mode 100644 index 51ca53cb1..000000000 --- a/cw_polygon/lib/polygon_transaction_info.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_evm/evm_chain_transaction_info.dart'; - -class PolygonTransactionInfo extends EVMChainTransactionInfo { - PolygonTransactionInfo({ - 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 PolygonTransactionInfo.fromJson(Map data) { - return PolygonTransactionInfo( - 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 => 'MATIC'; -} diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart deleted file mode 100644 index e77c112a3..000000000 --- a/cw_polygon/lib/polygon_wallet.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'dart:convert'; - -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_transaction_priority.dart'; -import 'package:cw_evm/evm_chain_wallet.dart'; -import 'package:cw_evm/evm_erc20_balance.dart'; -import 'package:cw_polygon/default_polygon_erc20_tokens.dart'; -import 'package:cw_polygon/polygon_client.dart'; -import 'package:cw_polygon/polygon_transaction_history.dart'; -import 'package:cw_polygon/polygon_transaction_info.dart'; -import 'package:web3dart/web3dart.dart'; - -class PolygonWallet extends EVMChainWallet { - PolygonWallet({ - required super.walletInfo, - required super.derivationInfo, - required super.password, - super.mnemonic, - super.initialBalance, - super.privateKey, - required super.client, - required super.encryptionFileUtils, - super.passphrase, - }) : super(nativeCurrency: CryptoCurrency.maticpoly); - - @override - int getTotalPriorityFee(EVMChainTransactionPriority priority) { - // Polygon has a minimum priority fee of 25 gwei - - 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: - additionalPriorityFee = 0; - break; - case EVMChainTransactionPriority.medium: - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 15).getInWei.toInt(); - break; - case EVMChainTransactionPriority.fast: - additionalPriorityFee = EtherAmount.fromInt(EtherUnit.gwei, 35).getInWei.toInt(); - break; - } - - return (minPriorityFeeWei + additionalPriorityFee); - } - - @override - Future initErc20TokensBox() async { - final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}"; - if (await CakeHive.boxExists(boxName)) { - evmChainErc20TokensBox = await CakeHive.openBox(boxName); - } else { - evmChainErc20TokensBox = await CakeHive.openBox(boxName.replaceAll(" ", "")); - } - } - - @override - void addInitialTokens() { - final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; - - 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 => - DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList(); - - @override - Future checkIfScanProviderIsEnabled() async { - bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true; - return isPolygonScanEnabled; - } - - @override - String getTransactionHistoryFileName() => 'polygon_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 ?? 'POL', - iconPath: iconPath, - isPotentialScam: token.isPotentialScam, - ); - } - - @override - EVMChainTransactionInfo getTransactionInfo( - EVMChainTransactionModel transactionModel, String address) { - final model = PolygonTransactionInfo( - 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 ?? "MATIC", - to: transactionModel.to, - from: transactionModel.from, - contractAddress: transactionModel.contractAddress, - ); - return model; - } - - @override - EVMChainTransactionHistory setUpTransactionHistory( - WalletInfo walletInfo, String password, EncryptionFileUtils encryptionFileUtils) { - return PolygonTransactionHistory( - 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, - ); - } - - final derivationInfo = await walletInfo.getDerivationInfo(); - - return PolygonWallet( - walletInfo: walletInfo, - derivationInfo: derivationInfo, - password: password, - mnemonic: keysData.mnemonic, - privateKey: keysData.privateKey, - passphrase: keysData.passphrase, - initialBalance: balance, - client: PolygonClient(), - encryptionFileUtils: encryptionFileUtils, - ); - } -} diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart deleted file mode 100644 index 23a31ece6..000000000 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:bip39/bip39.dart' as bip39; -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_polygon/polygon_client.dart'; -import 'package:cw_polygon/polygon_mnemonics_exception.dart'; -import 'package:cw_polygon/polygon_wallet.dart'; - -class PolygonWalletService extends EVMChainWalletService { - PolygonWalletService(super.isDirect, { - required this.client, - }); - - late PolygonClient client; - - @override - WalletType getType() => WalletType.polygon; - - @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 = PolygonWallet( - 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 PolygonWallet.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 PolygonWallet.open( - name: name, - password: password, - walletInfo: walletInfo, - encryptionFileUtils: encryptionFileUtilsFor(isDirect), - ); - - await wallet.init(); - await wallet.save(); - return wallet; - } - } - - @override - Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, - {bool? isTestnet}) async { - final wallet = PolygonWallet( - 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"; - derivationInfo.save(); - credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType; - credentials.walletInfo!.address = credentials.hwAccountData.address; - - final wallet = PolygonWallet( - 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 PolygonMnemonicIsIncorrectException(); - } - - final wallet = PolygonWallet( - 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 PolygonWallet.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; - - await newWalletInfo.save(); - } -} diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml deleted file mode 100644 index bcbd80443..000000000 --- a/cw_polygon/pubspec.yaml +++ /dev/null @@ -1,75 +0,0 @@ -name: cw_polygon -description: A new Flutter package project. -version: 0.0.1 -publish_to: none -author: Cake Wallet -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/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_polygon/test/cw_polygon_test.dart b/cw_polygon/test/cw_polygon_test.dart deleted file mode 100644 index 554e28795..000000000 --- a/cw_polygon/test/cw_polygon_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:cw_polygon/cw_polygon.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/docs/ADDING_EVM_L2_WALLET_NETWORK_TYPES.md b/docs/ADDING_EVM_L2_WALLET_NETWORK_TYPES.md new file mode 100644 index 000000000..c5ef9747c --- /dev/null +++ b/docs/ADDING_EVM_L2_WALLET_NETWORK_TYPES.md @@ -0,0 +1,589 @@ +# Guide: Adding a New L2 Network + +This guide provides step-by-step instructions for adding a new EVM-compatible L2 network to Cake Wallet. + +## Prerequisites + +- The network must be EVM-compatible +- You need the following information: + - Chain ID + - Network name + - Native currency (usually a `CryptoCurrency` instance) + - RPC endpoints + - Block explorer URLs + - Supported features (ERC20, EIP-1559, internal transactions, etc.) + - Default ERC20 tokens (optional but recommended) + +## Architecture Overview + +With the unified EVM architecture, all chains are managed through: +- **EvmChainRegistry**: Centralized registry for chain configurations +- **EVMChainWallet**: Single wallet class that handles all EVM chains via `selectedChainId` +- **ChainId-based operations**: All operations use `chainId` instead of `WalletType` +- **Backward compatibility**: Old wallet types (ethereum, polygon, base, arbitrum) still work + +**Key Principle**: New chains should use `WalletType.evm` and be identified by their `chainId`. Old chains maintain backward compatibility. + +## Step-by-Step Guide + +### Step 1: Add Chain Configuration to Registry + +**File**: `cw_evm/lib/evm_chain_registry.dart` + +Add your chain configuration in the `initialize()` method: + +```dart +// Example: Adding Optimism +_registerChain( + const ChainConfig( + chainId: 10, // Optimism mainnet + name: 'Optimism', + shortCode: 'op', + caip2: 'eip155:10', + nativeCurrency: CryptoCurrency.op, // Must exist in cw_core/lib/crypto_currency.dart + capabilities: ChainCapabilities( + supportsERC20: true, + supportsEIP1559: true, + supportsInternalTx: true, + supportsSubscriptions: false, + supportsENS: false, + ), + defaultRpcEndpoints: [ + 'mainnet.optimism.io', + 'optimism.publicnode.com', + // Add more RPC endpoints + ], + explorerUrls: [ + 'https://optimistic.etherscan.io', + ], + feeModel: FeeModel( + type: FeeType.eip1559, + defaultGasLimit: 21000, + ), + ), + WalletType.evm, // Use WalletType.evm for new chains (or old type if backward compatibility needed) + 'OP', // Native currency symbol +); +``` + +**Notes**: +- For **new chains**, use `WalletType.evm` (unified type) +- For **backward compatibility** with existing wallets, you can map to an old `WalletType` (e.g., `WalletType.optimism` if it exists) +- The registry automatically creates mappings: `chainId` → `WalletType`, `tag` → `chainId`, `caip2` → `chainId` +- If the chain uses a standard EVM client, you can use the default `EVMChainClient` (no custom client needed) + +### Step 2: Add Native Currency (If New) + +**File**: `cw_core/lib/crypto_currency.dart` + +If your chain's native currency doesn't exist, add it: + +```dart +// Example: Adding Optimism native currency +static const CryptoCurrency op = CryptoCurrency( + name: 'Optimism', + title: 'OP', + raw: 126, + iconPath: 'assets/images/op.png', // Add icon asset + tag: 'OP', + decimals: 18, +); +``` + +**Notes**: +- The `tag` should match the symbol used in the registry +- Add the currency icon to `assets/images/` +- Update currency lists if needed (e.g., `all`, `fiat`, etc.) + +### Step 2b: Wire Currency ↔ chainId Mappings + +The unified EVM and PayAnything flows rely on a **two-way mapping** between +`CryptoCurrency` and `chainId`. + +**File**: `cw_core/lib/currency_for_wallet_type.dart` + +1. **Map `chainId` → `CryptoCurrency`** in `getCryptoCurrencyByChainId`: + +```dart +CryptoCurrency getCryptoCurrencyByChainId(int chainId) { + switch (chainId) { + case 1: + return CryptoCurrency.eth; + case 137: + return CryptoCurrency.maticpoly; + case 8453: + return CryptoCurrency.baseEth; + case 42161: + return CryptoCurrency.arbEth; + case 10: + return CryptoCurrency.op; // NEW: Optimism + default: + return CryptoCurrency.eth; + } +} +``` + +2. **Map `CryptoCurrency` → `chainId`** in `getChainIdByCryptoCurrency`: + +```dart +int? getChainIdByCryptoCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.eth: + return 1; + case CryptoCurrency.maticpoly: + return 137; + case CryptoCurrency.baseEth: + return 8453; + case CryptoCurrency.arbEth: + return 42161; + case CryptoCurrency.op: // NEW: Optimism + return 10; + default: + return null; + } +} +``` + +**Why this matters**: +- `UniversalAddressDetector` and `PaymentViewModel` use these helpers to + derive `chainId` from detected currencies (QR codes, URIs, raw EVM + addresses). +- The EVM PayAnything flow and `EVMPaymentFlowBottomSheet` depend on having + the correct `chainId` for network and token selection. + +### Step 3: Create Default Tokens File (Optional but Recommended) + +**File**: `cw_evm/lib/tokens/optimism_tokens.dart` (example) + +Create a new file following the pattern of existing token files: + +```dart +import 'package:cw_core/erc20_token.dart'; + +/// Default ERC20 tokens for Optimism Mainnet +class OptimismTokens { + static List get tokens { + return [ + Erc20Token( + name: 'USD Coin', + symbol: 'USDC', + contractAddress: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + decimal: 6, + enabled: true, + ), + Erc20Token( + name: 'Tether USD', + symbol: 'USDT', + contractAddress: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', + decimal: 6, + enabled: true, + ), + // Add more default tokens + ]; + } +} +``` + +**File**: `cw_evm/lib/evm_chain_default_tokens.dart` + +Add your chain's tokens to the switch statement: + +```dart +static List getDefaultTokensByChainId(int chainId) { + return switch (chainId) { + 1 => EthereumTokens.tokens, + 137 => PolygonTokens.tokens, + 8453 => BaseTokens.tokens, + 42161 => ArbitrumTokens.tokens, + 10 => OptimismTokens.tokens, // NEW + _ => [], + }; +} +``` + +**Notes**: +- Default tokens are automatically loaded when a wallet is created or when switching to that chain +- Users can add/remove tokens later via the UI +- Only include well-known, verified tokens + +### Step 4: Update Chain Utilities (If Needed) + +**File**: `cw_evm/lib/utils/evm_chain_utils.dart` + +Add chain-specific logic if your chain has special requirements: + +#### 4.1 Priority Fees + +```dart +static int getTotalPriorityFee(EVMChainTransactionPriority priority, int chainId) { + return switch (chainId) { + 1 => _ethereumPriorityFee(priority), + 137 => _polygonPriorityFee(priority), + 8453 => _basePriorityFee(priority), + 42161 => 0, // Arbitrum doesn't use priority fees + 10 => _optimismPriorityFee(priority), // NEW - if custom logic needed + _ => _ethereumPriorityFee(priority), // Default to Ethereum logic + }; +} + +static bool hasPriorityFee(int chainId) { + return switch (chainId) { + 42161 => false, // Arbitrum doesn't use priority fees + 10 => true, // Optimism uses priority fees + _ => true, + }; +} +``` + +#### 4.2 ERC20 Tokens Box Name + +```dart +static String getErc20TokensBoxName(String walletName, int chainId) { + final sanitizedName = walletName.replaceAll(" ", "_"); + return switch (chainId) { + 1 => "${sanitizedName}_${Erc20Token.ethereumBoxName}", + 137 => "${sanitizedName}_${Erc20Token.polygonBoxName}", + 8453 => "${sanitizedName}_${Erc20Token.baseBoxName}", + 42161 => "${sanitizedName}_${Erc20Token.arbitrumBoxName}", + 10 => "${sanitizedName}_${Erc20Token.optimismBoxName}", // NEW - if custom box name needed + _ => "${sanitizedName}_${Erc20Token.ethereumBoxName}", // Default + }; +} +``` + +**Note**: If you don't add a case, it will use the default (Ethereum box name pattern). Only add if you need a specific box name. + +#### 4.3 Transaction History File Name + +```dart +static String getTransactionHistoryFileName(int chainId) { + return switch (chainId) { + 1 => 'transactions.json', + 137 => 'polygon_transactions.json', + 8453 => 'base_transactions.json', + 42161 => 'arbitrum_transactions.json', + 10 => 'optimism_transactions.json', // NEW + _ => 'transactions_$chainId.json', // Generic format for other chains + }; +} +``` + +#### 4.4 Scan Provider Preference Key + +```dart +static String getScanProviderPreferenceKey(int chainId) { + return switch (chainId) { + 1 => 'use_etherscan', + 137 => 'use_polygonscan', + 8453 => 'use_basescan', + 42161 => 'use_arbiscan', + 10 => 'use_optimismscan', // NEW + _ => 'use_etherscan', // Default + }; +} +``` + +#### 4.5 Default Token Tag + +```dart +static String getDefaultTokenTag(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'POL', + 8453 => 'ETH', + 42161 => 'ETH', + 10 => 'OP', // NEW + _ => 'ETH', // Default + }; +} +``` + +#### 4.6 Fee Currency Symbol + +```dart +static String getFeeCurrency(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'MATIC', // Polygon uses MATIC, not POL + 8453 => 'ETH', + 42161 => 'ETH', + 10 => 'ETH', // Optimism uses ETH + _ => 'ETH', // Default + }; +} +``` + +**Note**: This is used in transaction fetching APIs. Polygon uses 'MATIC' even though the currency tag is 'POL'. + +#### 4.7 Default Token Symbol + +```dart +static String getDefaultTokenSymbol(int chainId) { + return switch (chainId) { + 1 => 'ETH', + 137 => 'MATIC', + 8453 => 'ETH', + 42161 => 'ETH', + 10 => 'ETH', // Optimism uses ETH + _ => 'ETH', // Default + }; +} +``` + +### Step 5: Create Custom Client (Only If Needed) + +**Only needed if the chain requires custom transaction/balance fetching behavior** + +Most chains can use the default `EVMChainClient` which handles: +- Standard ERC20 token operations +- EIP-1559 transactions +- Internal transactions +- Balance fetching + +**File**: `cw_evm/lib/clients/optimism_client.dart` (example) + +```dart +import 'package:cw_evm/clients/evm_chain_client.dart'; + +class OptimismClient extends EVMChainClient { + OptimismClient() : super(chainId: 10); + + // Only override methods if custom behavior is needed + // For example, if Optimism has special transaction formatting: + + // @override + // Future> fetchTransactions(...) async { + // // Custom implementation + // } +} +``` + +**File**: `cw_evm/lib/clients/evm_chain_client_factory.dart` + +Add your custom client to the factory: + +```dart +static EVMChainClient createClient(int chainId) { + switch (chainId) { + case 1: // Ethereum + return EthereumClient(); + case 137: // Polygon + return PolygonClient(); + case 8453: // Base + return BaseClient(); + case 42161: // Arbitrum + return ArbitrumClient(); + case 10: // Optimism - NEW + return OptimismClient(); + default: + // Default client works for most chains + return EVMChainClient(chainId: chainId); + } +} +``` + +**Note**: If you don't create a custom client, the default `EVMChainClient(chainId: chainId)` will be used automatically. + +### Step 6: Add Node List YAML + +**File**: `assets/optimism_node_list.yml` + +Create a YAML file with default RPC endpoints: + +```yaml +- uri: mainnet.optimism.io + useSSL: true + isEnabledForAutoSwitching: true +- uri: optimism.publicnode.com + useSSL: true + isEnabledForAutoSwitching: true +- uri: 1rpc.io/op + useSSL: true + isEnabledForAutoSwitching: true +``` + +**File**: `lib/entities/node_list.dart` + +Add node loading for your chain: + +```dart +Future> loadDefaultNodes(WalletType type) async { + String path; + switch (type) { + // ... existing cases ... + case WalletType.evm: // For new chains using WalletType.evm + // Nodes are loaded based on chainId, not WalletType + // This is handled automatically by the node switching service + return []; + case WalletType.optimism: // Only if using old WalletType for backward compatibility + path = 'assets/optimism_node_list.yml'; + break; + } + // ... rest of the function ... +} +``` + +**Note**: For `WalletType.evm` wallets, nodes are managed dynamically based on `chainId`. The node switching service automatically loads the correct nodes. + +### Step 7: Update DI Registration (If Using Old WalletType) + +**Only needed if you're adding a new `WalletType` enum value for backward compatibility** + +**File**: `cw_core/lib/wallet_type.dart` + +If you need a new `WalletType` (not recommended for new chains): + +```dart +enum WalletType { + // ... existing types ... + @HiveField(19) // Next available field ID + optimism, // Only if needed for backward compatibility +} +``` + +**File**: `lib/di.dart` + +Add your wallet type to the `WalletService` factory: + +```dart +factory WalletService(WalletType type, bool isDirect) { + switch (type) { + // ... existing cases ... + case WalletType.optimism: // Only if using old WalletType + return evm!.createEVMWalletService(type, isDirect); + case WalletType.evm: // For new unified wallets + return evm!.createEVMWalletService(type, isDirect); + } +} +``` + +**Note**: **For new chains, use `WalletType.evm`** - no DI changes needed! The unified proxy already handles all EVM chains. + +### Step 8: Add Erc20Token Box Name Constant (If Needed) + +**File**: `cw_core/lib/erc20_token.dart` + +If you need a specific box name pattern: + +```dart +class Erc20Token extends CryptoCurrency { + // ... existing code ... + + static const String optimismBoxName = 'optimism_erc20_tokens'; +} +``` + +**Note**: Only needed if you want a custom box name. Otherwise, the default pattern will be used. + +## What Happens Automatically + +Once you've completed the steps above, the following will work automatically: + +✅ **Chain appears in dropdown** - The chain selection UI (`EvmSwitcher`) automatically shows your new chain from the registry +✅ **Wallet creation** - Users can create `WalletType.evm` wallets and switch to your chain +✅ **Chain switching** - Users can switch between chains seamlessly +✅ **All operations** - Balance fetching, transaction sending, etc. all work +✅ **Transaction filtering** - Transactions are automatically filtered by `chainId` +✅ **Node connection** - Automatic node connection when switching chains (uses `chainId` to find correct nodes) +✅ **Balance updates** - Automatic balance refresh when switching chains +✅ **ERC20 tokens** - Default tokens are automatically loaded +✅ **Transaction history** - Separate history files per chain +✅ **Backward compatibility** - Old wallet types continue to work + +## Testing Checklist + +- [ ] Create a new `WalletType.evm` wallet +- [ ] Switch to your new chain and verify it appears in the chain switcher +- [ ] Verify balances update correctly +- [ ] Switch between chains and verify balances update +- [ ] Send a transaction on the new chain +- [ ] Verify transactions are filtered correctly (only show transactions for current chain) +- [ ] Test node connection and switching +- [ ] Verify default tokens are loaded +- [ ] Test wallet backup/restore +- [ ] Verify transaction history is separate per chain +- [ ] Test on old wallet types (if applicable) to ensure backward compatibility + +## Common Issues + +### Issue: Chain doesn't appear in dropdown + +**Solution**: +- Verify the chain is registered in `EvmChainRegistry.initialize()` +- Check that `EvmChainRegistry().initialize()` is called during app startup +- Verify the registry is initialized before the UI tries to load chains + +### Issue: Node connection fails + +**Solution**: Check that: +- Node list YAML file exists and is properly formatted +- RPC endpoints are correct and accessible +- For `WalletType.evm` wallets, nodes are retrieved using `chainId` via `settingsStore.getCurrentNode(WalletType.evm, chainId: chainId)` +- Node switching service uses `isEVMCompatibleChain()` to handle all EVM wallets + +### Issue: Transactions not showing + +**Solution**: Verify: +- `chainId` is correctly set in transaction info +- Transaction filtering logic uses `chainId` (not `walletType`) +- Transactions are being saved with the correct `chainId` in `EVMChainTransactionHistory` +- `EVMChainTransactionInfo.fromJson()` correctly infers `chainId` for old transactions + +### Issue: Default tokens not loading + +**Solution**: +- Verify tokens are added to `EVMChainDefaultTokens.getDefaultTokensByChainId()` +- Check that `addInitialTokens()` is called during wallet initialization +- Ensure token file follows the pattern: `class OptimismTokens { static List get tokens { ... } }` + +### Issue: Balance not updating after chain switch + +**Solution**: +- Verify `selectChain()` is called with the correct `chainId` +- Check that `initErc20TokensBox()` switches to the new chain's box +- Ensure `_fetchErc20Balances()` is called after chain switch +- Verify `erc20Currencies` getter handles closed boxes gracefully + +### Issue: "Box has already been closed" error + +**Solution**: +- This can happen during chain switching if code accesses `evmChainErc20TokensBox` while it's being closed +- Ensure all access to `evmChainErc20TokensBox` checks `isOpen` first +- The `erc20Currencies` getter should return empty list if box is closed +- Use try-catch blocks when accessing the box during async operations + +## Summary + +### Minimum Steps for Standard EVM Chain + +1. **Add chain config to Registry** (Step 1) - Required +2. **Add native currency** (Step 2) - Required if currency doesn't exist +3. **Add default tokens** (Step 3) - Recommended +4. **Add node list YAML** (Step 6) - Required +5. **Update chain utilities** (Step 4) - Only if chain has special requirements + +### For Chains with Custom Behavior + +- Add **Step 5** (Custom Client) only if needed +- Add **Step 7** (DI Registration) only if using old `WalletType` (not recommended) +- Add **Step 8** (Box Name Constant) only if custom box name needed + +### Key Points + +✅ **Use `WalletType.evm` for new chains** - No need to create new `WalletType` enum values +✅ **Everything is `chainId`-based** - All operations use `chainId`, not `walletType` +✅ **Registry-driven** - Chain configuration is centralized in `EvmChainRegistry` +✅ **Backward compatible** - Old wallet types (ethereum, polygon, base, arbitrum) still work +✅ **No proxy files needed** - The unified `evm` proxy handles all chains +✅ **Automatic chain switching** - Users can switch chains without creating new wallets + +### What You DON'T Need to Do + +❌ Create a new `WalletType` enum value (use `WalletType.evm`) +❌ Create a new proxy file (unified proxy handles all chains) +❌ Create a new wallet service (unified service handles all chains) +❌ Create a new wallet class (unified `EVMChainWallet` handles all chains) +❌ Update view models (they work with any EVM chain via proxy) +❌ Update UI components (chain switcher auto-populates from registry) + +**Key Point**: With the unified EVM architecture, adding new L2 chains is now much simpler - most chains only require Registry configuration and default tokens! diff --git a/docs/ADDING_NEW_WALLET_TYPES.md b/docs/ADDING_NEW_WALLET_TYPES.md new file mode 100644 index 000000000..91a69b9f8 --- /dev/null +++ b/docs/ADDING_NEW_WALLET_TYPES.md @@ -0,0 +1,1033 @@ +# Guide to Adding a New Wallet Type in Cake Wallet + +## Important: EVM-Compatible Wallets + +**If you're adding an EVM-compatible wallet (Ethereum, Polygon, Base, Arbitrum, Optimism, etc.)**, please refer to the **[Adding New L2 Network Guide](./adding_new_l2_network_guide.md)** instead. EVM wallets use a unified architecture and don't require the full setup described in this guide. + +**For EVM wallets:** +- Use `WalletType.evm` (unified type for all EVM chains) +- Add chain configuration to `EvmChainRegistry` +- No proxy files needed (unified `evm` proxy handles all chains) +- See the L2 guide for detailed steps + +--- + +## Wallet Integration Overview + +**Note**: Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. + +This guide covers adding **non-EVM** wallet types. The process involves: +1. Core package setup (`cw_walletx`) +2. Proxy setup (communication layer) +3. Configuration files +4. Dependency injection +5. Node setup +6. UI integration +7. Feature integration (send, receive, exchange, etc.) + +--- + +## Step 1: Core Package Setup + +### 1.1 Add WalletType Enum + +**File**: `cw_core/lib/wallet_type.dart` + +Add your new wallet type to the enum: + +```dart +enum WalletType { + // ... existing types ... + @HiveField(19) // Use next available field ID + walletx, +} +``` + +**Update Required Functions**: +- `serializeToInt()`: Add `case WalletType.walletx: return 19;` +- `deserializeFromInt()`: Add `case 19: return WalletType.walletx;` +- `walletTypeToString()`: Add `case WalletType.walletx: return 'WalletX';` +- `walletTypeToDisplayName()`: Add `case WalletType.walletx: return 'WalletX';` + +### 1.2 Add Currency Mapping + +**File**: `cw_core/lib/currency_for_wallet_type.dart` + +Add a case in the `currencyForWalletType` function: + +```dart +CryptoCurrency currencyForWalletType(WalletType type) { + switch (type) { + // ... existing cases ... + case WalletType.walletx: + return CryptoCurrency.walletx; // Must exist in crypto_currency.dart + } +} +``` + +### 1.3 Add Native Currency (If New) + +**File**: `cw_core/lib/crypto_currency.dart` + +If the cryptocurrency doesn't exist, add it: + +```dart +static const CryptoCurrency walletx = CryptoCurrency( + name: 'WalletX', + title: 'WLTX', + raw: 'walletx', + iconPath: 'assets/images/walletx.png', // Add icon asset + tag: 'WLTX', // Optional tag for identification +); +``` + +**Important**: Add the currency to the `all` list in the same file: + +```dart +static List get all => [ + // ... existing currencies ... + walletx, +]; +``` + +### 1.4 Create Core Package + +Create a new package for wallet-specific integration: `cw_walletx/` + +**Required Files** (create as needed): +- `walletx_transaction_history.dart` - Transaction history management +- `walletx_transaction_info.dart` - Transaction information model +- `walletx_mnemonics_exception.dart` - Mnemonic-related exceptions +- `walletx_tokens.dart` - Token management (if applicable) +- `walletx_wallet_service.dart` - Wallet service implementation +- `walletx_wallet.dart` - Main wallet class +- `walletx_client.dart` - Network client (if applicable) +- `walletx_new_wallet_credentials.dart` - New wallet credentials +- `walletx_restore_wallet_credentials.dart` - Restore wallet credentials +- etc. + +### 1.5 Add Code Generation + +**File**: `model_generator.sh` + +Add code generation for your package: + +```bash +cd cw_walletx && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. +``` + +**Add dev_dependencies** to `cw_walletx/pubspec.yaml`: +```yaml +dev_dependencies: + build_runner: ^2.0.0 + mobx_codegen: ^2.0.0 + hive_generator: ^2.0.0 +``` + +--- + +## Step 2: Proxy Setup + +A **Proxy** class is used to communicate with the wallet package. Instead of directly importing from `cw_walletx` within the `lib` directory, we use a proxy to access these functionalities. This maintains separation of concerns and allows conditional compilation. + +### 2.1 Create Proxy Directory + +Create a proxy folder: `lib/walletx/` + +It should contain 2 files: +- `cw_walletx.dart` - Implementation class (links `cw_walletx` package to `lib`) +- `walletx.dart` - Abstract class (generated, defines the interface) + +### 2.2 Add to .gitignore + +**File**: `.gitignore` + +Add the generated abstract class file: + +``` +lib/walletx/walletx.dart +``` + +**Note**: The `walletx.dart` file is generated by `configure.dart` and should not be committed. + +### 2.3 Create Implementation File + +**File**: `lib/walletx/cw_walletx.dart` + +Create the implementation class: + +```dart +part of 'walletx.dart'; + +class CWWalletX extends WalletX { + @override + List getWalletXWordList(String language) => WalletXMnemonics.englishWordlist; + + @override + WalletService createWalletXWalletService(bool isDirect) => + WalletXWalletService(isDirect); + + @override + WalletCredentials createWalletXNewWalletCredentials({ + required String name, + String? mnemonic, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }) => + WalletXNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + + // Add other required methods from the abstract class +} +``` + +--- + +## Step 3: Configuration Files Setup + +### 3.1 Update configure.dart + +**File**: `tool/configure.dart` + +#### 3.1.1 Add Output Path Constant + +```dart +const walletxOutputPath = 'lib/walletx/walletx.dart'; +``` + +#### 3.1.2 Add Activation Variable + +In the `main()` function: + +```dart +final hasWalletX = args.contains('${prefix}walletx'); +``` + +#### 3.1.3 Create Generation Function + +Add a function to generate the abstract class: + +```dart +Future generateWalletX(bool hasImplementation) async { + final outputFile = File(walletxOutputPath); + const walletxCommonHeaders = """ +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_info.dart'; +// Add other necessary imports +"""; + + const walletxCWHeaders = """ +import 'package:cw_walletx/walletx_wallet.dart'; +import 'package:cw_walletx/walletx_wallet_service.dart'; +import 'package:cw_walletx/walletx_mnemonics.dart'; +// Add other necessary imports from cw_walletx +"""; + + const walletxCwPart = """ +part 'cw_walletx.dart'; +"""; + + const walletxContent = """ +abstract class WalletX { + List getWalletXWordList(String language); + WalletService createWalletXWalletService(bool isDirect); + WalletCredentials createWalletXNewWalletCredentials({ + required String name, + String? mnemonic, + WalletInfo? walletInfo, + String? password, + String? passphrase, + }); + // Add other abstract methods as needed +} +"""; + + const walletxEmptyDefinition = 'WalletX? walletx;\n'; + const walletxCwDefinition = 'WalletX? walletx = CWWalletX();\n'; + + final output = '$walletxCommonHeaders\n' + + (hasImplementation ? '$walletxCWHeaders\n' : '\n') + + (hasImplementation ? '$walletxCwPart\n\n' : '\n') + + (hasImplementation ? walletxCwDefinition : walletxEmptyDefinition) + + '\n' + + walletxContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} +``` + +#### 3.1.4 Call Generation Function + +In `main()`, add: + +```dart +await generateWalletX(hasWalletX); +``` + +#### 3.1.5 Update generatePubspec Function + +Add parameter and logic: + +```dart +Future generatePubspec({ + // ... existing parameters ... + required bool hasWalletX, +}) async { + // ... existing code ... + + const cwWalletX = """ + cw_walletx: + path: ./cw_walletx + """; + + // ... existing code ... + + if (hasWalletX) { + output += '\n$cwWalletX'; + } +} +``` + +#### 3.1.6 Update generateWalletTypes Function + +Add parameter and logic: + +```dart +Future generateWalletTypes({ + // ... existing parameters ... + required bool hasWalletX, +}) async { + // ... existing code ... + + if (hasWalletX) { + outputContent += '\tWalletType.walletx,\n'; + } +} +``` + +### 3.2 Update Build Scripts + +**Files**: +- `scripts/android/app_config.sh` +- `scripts/ios/app_config.sh` +- `scripts/macos/app_config.sh` + +Add `--walletx` to the `CONFIG_ARGS` under `$CAKEWALLET`: + +```bash +CONFIG_ARGS="--monero --bitcoin --walletx" +``` + +### 3.3 Run Configuration + +Open a terminal and run: + +```bash +cd scripts/android/ +source ./app_env.sh cakewallet +./app_config.sh + +cd cw_walletx && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. +dart run build_runner build --delete-conflicting-outputs +``` + +This will: +- Generate the proxy abstract class +- Add wallet type to available types +- Add `cw_walletx` to `pubspec.yaml` + +--- + +## Step 4: Dependency Injection + +### 4.1 Register Wallet Service + +**File**: `lib/di.dart` + +In the `registerWalletService` function, add your case: + +```dart +getIt.registerFactoryParam((WalletType param1, __) { + switch (param1) { + // ... existing cases ... + case WalletType.walletx: + return walletx!.createWalletXWalletService(SettingsStoreBase.walletPasswordDirectInput); + } +}); +``` + +**Note**: Import the proxy at the top of the file: + +```dart +import 'package:cake_wallet/walletx/walletx.dart'; +``` + +### 4.2 Register Wallet Creation Credentials + +**File**: `lib/view_model/wallet_new_vm.dart` + +In the `getCredentials` method, add your case: + +```dart +@override +WalletCredentials getCredentials(dynamic _options) { + // ... existing code ... + + switch (type) { + // ... existing cases ... + case WalletType.walletx: + return walletx!.createWalletXNewWalletCredentials( + name: name, + password: walletPassword, + mnemonic: newWalletArguments!.mnemonic, + passphrase: passphrase, + ); + } +} +``` + +**Note**: Import the proxy: + +```dart +import 'package:cake_wallet/walletx/walletx.dart'; +``` + +--- + +## Step 5: Node Setup + +### 5.1 Create Node List YAML + +**File**: `assets/walletx_node_list.yml` + +Create a YAML file with default RPC endpoints: + +```yaml +- uri: api.walletx.io + is_default: true + useSSL: true + isEnabledForAutoSwitching: true +- uri: walletx.publicnode.com + useSSL: true + isEnabledForAutoSwitching: true +``` + +### 5.2 Add to pubspec.yaml + +**File**: `pubspec_base.yaml` + +Add the asset path: + +```yaml +flutter: + assets: + - assets/walletx_node_list.yml +``` + +### 5.3 Load Nodes + +**File**: `lib/entities/node_list.dart` + +Add a function to load nodes: + +```dart +Future> loadDefaultWalletXNodes() async { + final nodesRaw = await rootBundle.loadString('assets/walletx_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.walletx; + nodes.add(node); + } + } + return nodes; +} +``` + +In the `resetToDefault` function, call it: + +```dart +final walletxNodes = await loadDefaultWalletXNodes(); +nodes.addAll(walletxNodes); +``` + +### 5.4 Add Default Node Migration + +**File**: `lib/entities/default_settings_migration.dart` + +#### 5.4.1 Define Default Node URI + +At the top of the file: + +```dart +const String walletXDefaultNodeUri = 'api.walletx.io'; +``` + +#### 5.4.2 Add Helper Functions + +```dart +Node? getWalletXDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == walletXDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.walletx); +} + +Future addWalletXNodeList({required Box nodes}) async { + final nodeList = await loadDefaultWalletXNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeWalletXCurrentNodeToDefault({ + required SharedPreferences sharedPreferences, + required Box nodes, +}) async { + final node = getWalletXDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, nodeId); +} +``` + +#### 5.4.3 Add Preference Key + +**File**: `lib/entities/preference_key.dart` + +```dart +static const currentWalletXNodeIdKey = 'current_walletx_node_id'; +``` + +#### 5.4.4 Add Migration Case + +In `defaultSettingsMigration`, add: + +```dart +case "next-number-increment": // Increment from current version + await addWalletXNodeList(nodes: nodes); + await changeWalletXCurrentNodeToDefault( + sharedPreferences: sharedPreferences, + nodes: nodes, + ); + break; +``` + +#### 5.4.5 Update Migration Version + +**File**: `lib/main.dart` + +Increment `initialMigrationVersion` to the new case number. + +### 5.5 Update Node List ViewModel + +**File**: `lib/view_model/node_list/node_list_view_model.dart` + +In the `reset` function, add: + +```dart +case WalletType.walletx: + node = getWalletXDefaultNode(nodes: _nodeSource)!; + break; +``` + +### 5.6 Update Node Class + +**File**: `cw_core/lib/node.dart` + +#### 5.6.1 Update URI Getter + +In the `uri` getter, add: + +```dart +case WalletType.walletx: + return useSSL ? Uri.https(uriRaw, '') : Uri.http(uriRaw, ''); +``` + +#### 5.6.2 Update requestNode Method + +Add case for `WalletType.walletx` in the `requestNode` method. + +### 5.7 Update Settings Store + +**File**: `lib/store/settings_store.dart` + +In the `load` function: + +```dart +final walletXNodeId = sharedPreferences.getInt(PreferencesKey.currentWalletXNodeIdKey); +final walletXNode = nodeSource.get(walletXNodeId); +if (walletXNode != null) { + nodes[WalletType.walletx] = walletXNode; +} +``` + +Repeat in the `reload` function. + +In `_saveCurrentNode`, add: + +```dart +case WalletType.walletx: + await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, node.key as int); + break; +``` + +### 5.8 Run Code Generation + +```bash +cd cw_core && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. +dart run build_runner build --delete-conflicting-outputs +``` + +--- + +## Step 6: UI Integration + +### 6.1 Add Wallet Icons + +**File**: `lib/src/dashboard/widgets/menu_widget.dart` + +Add icon for walletx. + +**File**: `lib/src/screens/wallet_list/wallet_list_page.dart` + +Add icon and case in `imageFor` method: + +```dart +case WalletType.walletx: + return 'assets/images/walletx_icon.png'; +``` + +**File**: `lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart` + +Add icon similarly. + +### 6.2 Update Wallet Creation + +**File**: `lib/view_model/wallet_new_vm.dart` + +In the `seedPhraseWordsLength` getter, add: + +```dart +case WalletType.walletx: + return 12; // or appropriate length +``` + +--- + +## Step 7: Wallet Restoration + +### 7.1 Update Seed Validator + +**File**: `lib/core/seed_validator.dart` + +In `getWordList`, add: + +```dart +case WalletType.walletx: + return WalletXMnemonics.englishWordlist; // or appropriate wordlist +``` + +### 7.2 Update Restore ViewModel + +**File**: `lib/view_model/wallet_restore_view_model.dart` + +#### 7.2.1 Update Available Modes + +In the constructor, add to appropriate restore mode lists: + +```dart +case WalletType.walletx: + availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; // or as appropriate + break; +``` + +#### 7.2.2 Update hasRestoreFromPrivateKey + +Add to the list if supported: + +```dart +late final bool hasRestoreFromPrivateKey = [ + // ... existing types ... + WalletType.walletx, +].contains(type); +``` + +#### 7.2.3 Update getCredentials + +In the `getCredentials` method, add cases for restore modes: + +```dart +case WalletType.walletx: + if (mode == WalletRestoreMode.seed) { + return walletx!.createWalletXRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + passphrase: passphrase, + ); + } else if (mode == WalletRestoreMode.keys) { + return walletx!.createWalletXRestoreWalletFromPrivateKey( + name: name, + privateKey: options['privateKey'] as String, + password: password, + ); + } + break; +``` + +--- + +## Step 8: Display Seeds/Keys + +**File**: `lib/view_model/wallet_keys_view_model.dart` + +In the `populateItems` function, add: + +```dart +case WalletType.walletx: + // Add items for displaying seed/keys + break; +``` + +--- + +## Step 9: Receive Functionality + +**File**: `lib/view_model/wallet_address_list/wallet_address_list_view_model.dart` + +### 9.1 Create PaymentUri Implementation + +```dart +class WalletXPaymentUri implements PaymentUri { + final String address; + final CryptoCurrency currency; + + WalletXPaymentUri(this.address, this.currency); + + @override + String get uri => 'walletx:$address'; // Adjust scheme as needed +} +``` + +### 9.2 Update URI Getter + +In the `uri` getter: + +```dart +case WalletType.walletx: + return WalletXPaymentUri(address, currency); +``` + +### 9.3 Update Address List + +In the `addressList` getter, add logic to return addresses for walletx. + +--- + +## Step 10: Balance Screen + +### 10.1 Update Balance ViewModel + +**File**: `lib/view_model/dashboard/balance_view_model.dart` + +- Update `isHomeScreenSettingsEnabled` if needed +- Add cases to `availableBalanceLabel` and `additionalBalanceLabel` getters + +### 10.2 Update Fiat Rate Updates + +**File**: `lib/reactions/fiat_rate_update.dart` + +In `startFiatRateUpdate`, add: + +```dart +if (appStore.wallet!.type == WalletType.walletx) { + currencies = walletx!.getWalletXTokenCurrencies(appStore.wallet!) + .where((element) => element.enabled); +} +``` + +**File**: `lib/reactions/on_current_wallet_change.dart` + +Similarly update `startCurrentWalletChangeReaction`. + +### 10.3 Update Transaction List Item + +**File**: `lib/view_model/dashboard/transaction_list_item.dart` + +In `formattedFiatAmount`, add: + +```dart +case WalletType.walletx: + // Handle fiat amount conversion + break; +``` + +--- + +## Step 11: Send Functionality + +**File**: `lib/view_model/send/send_view_model.dart` + +- Update `_credentials` function to handle `WalletType.walletx` +- Update `hasMultipleTokens` if walletx supports tokens + +--- + +## Step 12: Exchange Integration + +### 12.1 Update Exchange ViewModel + +**File**: `lib/view_model/exchange/exchange_view_model.dart` + +In `initialPairBasedOnWallet`, add: + +```dart +case WalletType.walletx: + depositCurrency = CryptoCurrency.walletx; + break; +``` + +### 12.2 Update Exchange Trade ViewModel + +**File**: `lib/view_model/exchange/exchange_trade_view_model.dart` + +If walletx has tokens, update `_checkIfCanSend`: + +```dart +bool _isWalletXToken(CryptoCurrency currency) { + return currency is WalletXToken; // Adjust based on your token type +} + +// In _checkIfCanSend: +if (_isWalletXToken(from)) { + return true; +} +``` + +--- + +## Step 13: Home Settings (Token Management) + +**File**: `lib/view_model/dashboard/home_settings_view_model.dart` + +- Update `_updateTokensList` to add walletx tokens if applicable +- Update `getTokenAddressBasedOnWallet` to handle walletx tokens +- Update `getToken`, `addToken`, `deleteToken`, and `changeTokenAvailability` methods + +--- + +## Step 14: Buy and Sell + +**File**: `lib/entities/provider_types.dart` + +- Add case in `getAvailableBuyProviderTypes` for `WalletType.walletx` +- Add case in `getAvailableSellProviderTypes` for `WalletType.walletx` + +--- + +## Step 15: QR Code Restoration + +### 15.1 Update QR Restore ViewModel + +**File**: `lib/view_model/restore/wallet_restore_from_qr_code.dart` + +Add scheme to `_walletTypeMap`: + +```dart +'walletx': WalletType.walletx, +``` + +Update `_determineWalletRestoreMode` if needed. + +**File**: `lib/view_model/restore/restore_from_qr_vm.dart` + +Update `getCredentialsFromRestoredWallet` method. + +### 15.2 Update Address Validator + +**File**: `lib/core/address_validator.dart` + +In `getAddressFromStringPattern`, add: + +```dart +case WalletType.walletx: + // Add address pattern matching logic + break; +``` + +If walletx has tokens, add them to the switch case as well. + +### 15.3 Update Platform Manifests + +**Android**: `AndroidManifestBase.xml` + +Add intent filter: + +```xml + + + + + + +``` + +**iOS**: `InfoBase.plist` + +Add URL scheme: + +```xml +CFBundleURLTypes + + + CFBundleURLSchemes + + walletx + + + +``` + +--- + +## Step 16: Transaction History + +**File**: `lib/view_model/transaction_details_view_model.dart` + +- Add case for `WalletType.walletx` to add items to detailed view +- Update `_explorerUrl` to return blockchain explorer link +- Update `_explorerDescription` to display explorer name + +--- + +## Step 17: Secrets Management + +### 17.1 Create Secrets Config + +**File**: `wallet-secrets-config.json` + +Create with empty object: `{}` + +### 17.2 Update Secret Key Utility + +**File**: `tool/utils/secret_key.dart` + +Add entry for walletx. + +### 17.3 Update Generate Secrets + +**File**: `tool/generate_secrets_config.dart` + +Add generation logic for walletx (don't forget to call `secrets.clear()` before adding new logic). + +### 17.4 Update Import Secrets + +**File**: `tool/import_secrets_config.dart` + +Add import logic for walletx. + +### 17.5 Update .gitignore + +Add: +``` +**/tool/.walletx-secrets-config.json +**/cw_walletx/lib/.secrets.g.dart +``` + +--- + +## Testing Checklist + +- [ ] Create a new wallet for the type +- [ ] Restore wallet from seed +- [ ] Restore wallet from private key (if supported) +- [ ] Display seed/keys correctly +- [ ] Send transaction +- [ ] Receive transaction +- [ ] View transaction history +- [ ] View transaction details +- [ ] Exchange integration (if applicable) +- [ ] Buy/Sell integration (if applicable) +- [ ] Token management (if applicable) +- [ ] QR code restoration +- [ ] Node connection and switching +- [ ] Wallet backup/restore + +--- + +## Important Notes + +1. **No Direct Imports**: Never import directly from `cw_walletx` in `lib/`. Always use the proxy layer (`lib/walletx/walletx.dart`). + +2. **Proxy Pattern**: The proxy pattern ensures: + - Conditional compilation (wallets can be excluded from builds) + - Separation of concerns + - Easier testing and mocking + +3. **Code Generation**: Always run `build_runner` after making changes to MobX/Hive classes: + ```bash + dart run build_runner build --delete-conflicting-outputs + ``` + +4. **EVM Wallets**: If adding an EVM-compatible wallet, use the [L2 Network Guide](./adding_new_l2_network_guide.md) instead. + +5. **Token Support**: If your wallet supports tokens (like ERC20, SPL, TRC20): + - Add token checks in `exchange_trade_view_model.dart` + - Add token tag checks where needed + - Implement token management in home settings + +6. **Buy/Sell Providers**: Check which providers support your wallet currency and add them in `provider_types.dart`. + +7. **Build Verification**: Try building a minimal version (e.g., Monero-only) to catch any missing imports or compilation errors. + +--- + +## Common Issues + +### Issue: Proxy not found + +**Solution**: +- Verify `configure.dart` is set up correctly +- Run the configuration scripts +- Check that `walletx.dart` is generated (even if empty) + +### Issue: Wallet type not appearing + +**Solution**: +- Verify `generateWalletTypes` includes your wallet type +- Check that configuration scripts are run +- Verify `lib/wallet_types.g.dart` includes your type + +### Issue: Node connection fails + +**Solution**: +- Verify node list YAML is properly formatted +- Check that nodes are loaded in `default_settings_migration.dart` +- Verify preference keys are set correctly + +### Issue: Compilation errors + +**Solution**: +- Ensure all proxy methods are implemented in `cw_walletx.dart` +- Check that all imports are through proxy, not direct +- Verify code generation is run + +--- + +Copyright (C) 2018-2023 Cake Labs LLC diff --git a/docs/NEW_WALLET_TYPES.md b/docs/NEW_WALLET_TYPES.md deleted file mode 100644 index 74350e5d8..000000000 --- a/docs/NEW_WALLET_TYPES.md +++ /dev/null @@ -1,301 +0,0 @@ -# Guide to adding a new wallet type in Cake Wallet - -## Wallet Integration - -**N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. - -**Core Folder/Files Setup** -- Identify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc -- Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. -- Fill out the necessary information in the various functions in the files, concerning the wallet name, the native currency type, symbol etc. -- Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. -- If the cryptocurrency for walletx is not available among the default cryptocurrencies, add a new cryptocurrency entry in `cw_core/lib/cryptocurrency.dart`. -- Add the newly created cryptocurrency name to the list named `all` in this file. -- Create a package for the wallet specific integration, name it. `cw_walletx` -- Add the following initial common files and replicate to fit the wallet - - walletx_transaction_history.dart - - walletx_transaction_info.dart - - walletx_mnemonics_exception.dart - - walletx_tokens.dart - - walletx_wallet_service.dart: - - walletx_wallet.dart - - etc. - -- Add the code to run the code generation needed for the files in the `cw_walletx` package to the `model_generator.sh` script - - cd cw_walletx && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. - -- Add the relevant dev_dependencies for generating the files also - - build_runner - - mobx_codegen - - hive_generator - -**WalletX Proxy Setup** - -A `Proxy` class is used to communicate with the specific wallet package we have. Instead of directly making use of methods and parameters in `cw_walletx` within the `lib` directory, we use a proxy to access these data. All important functions, calls and interactions we want to make with our `cw_walletx` package would be defined and done through the proxy class. The class would define the import - -- Create a proxy folder titled `walletx` to handle the wallet operations. It would contain 2 files: `cw_walletx.dart` and `walletx.dart`. -- `cw_walletx.dart` file would hold an implementation class containing major operations to be done in the lib directory. It serves as the link between the cw_walletx package and the rest of the codebase(lib directory files and folders). -- `walletx.dart` would contain the abstract class highlighting the methods that would bring the functionalities and features in the `cw_walletx` package to the rest of the `lib` directory. -- Add `walletx.dart` to `.gitignore` as we won’t be pushing it: `lib/tron/tron.dart`. -- `walletx.dart` would always be generated based on the configure files we would be setting up in the next step. - -**Configuration Files Setup** -- Before we populate the field, head over to `tool/configure.dart` to setup the necessary configurations for the `walletx` proxy. -- Define the output path, it’ll follow the format `lib/walletx/walletx.dart`. -- Add the variable to check if `walletx` is to be activated -- Define the function that would generate the abstract class for the proxy.(We will flesh out this function in the next steps). -- Add the defined variable in step 2 to the `generatePubspec` and `generateWalletTypes`. -- Next, modify the following functions: - - generatePubspec function - 1. Add the parameters to the method params (i.e required bool hasWalletX) - 2. Define a variable to hold the entry for the pubspec.yaml file - - const cwWalletX = """ - cw_tron: - path: ./cw_walletx - """; - - 3. Add an if block that takes in the passed parameter and adds the defined variable(inn the previous step) to the list of outputs - - if (hasWalletX) { - output += '\n$cwWalletX’; - } - - - generateWalletTypes function - 1. Add the parameters to the method params (i.e required bool hasWalletX) - 2. Add an if block to add the wallet type to the list of outputs this function generates - - if (hasWalletX) { - outputContent += '\tWalletType.walletx,\n’; - } - -- Head over to `scripts/android/pubspec_gen.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params. -- Repeat this in `scripts/ios/app_config.sh` and `scripts/macos/app_config.sh` -- Open a terminal and cd into `scripts/android/`. Run the following commands to run setup configuration scripts(proxy class, add walletx to list of wallet types and add cw_walletx to pubspec). - - source ./app_env.sh cakewallet - - ./app_config.sh - - cd cw_walletx && flutter pub get && dart run build_runner build - - dart run build_runner build --delete-conflicting-outputs - -Moving forward, our interactions with the cw_walletx package would be through the proxy class and its methods. - -**Pre-Wallet Creation for WalletX** -- Go to `di.dart` and locate the block to `registerWalletService`. In this, add the case to handle creating the WalletXWalletService - - case WalletType.walletx: - return walletx!.createWalletXWalletService(_walletInfoSource); - -- Go to `lib/view_model/wallet_new_vm.dart`, in the getCredentials method, which gets the new wallet credentials for walletX add the case for the new wallet - - case WalletType.walletx: - return walletx!.createWalletXNewWalletCredentials(name: name); - -**Node Setup** -- Before we can be able to successfully create a new wallet of wallet type walletx we need to setup the node that the wallet would use: -- In the assets directory, create a new file and name it `walletx_node_list.yml`. This yml file would contain the details for nodes to be used for walletX. An example structure for each node entry - - uri: "api.nodeurl.io" - is_default: true - useSSL: true - -You can add as many node entries as desired. - -- Add the path to the yml file created to the `pubspec_base.yaml` file (`“assets/walletx_node_list.yml”`) -- Go to `lib/entities/node_list.dart`, add a function to load the node entries we made in `walletx_node_list.yml` for walletx. -- Name your function `loadDefaultWalletXNodes()`. The function would handle loading the yml file as a string and parsing it into a Node Object to be used within the app. Here’s a template for the function. - - Future> loadDefaultWalletXNodes() async { - final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.tron; - nodes.add(node); - } - } - return nodes; - } - -- Inside the `resetToDefault` function, call the function you created and add the result to the nodes result variable. -- Go to `lib/entities/default_settings_migration.dart` file, we’ll be adding the following to the file. -- At the top of the file, after the imports, define the default nodeUrl for wallet-name. -- Next, write a function to fetch the node for this default uri you added above. - - Node? getWalletXDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == walletXDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.walletx); - } - -- Next, write a function that will add the list of nodes we declared in the `walletx_node_list.yml` file to the Nodes Box, to be used in the app. Here’s the format for this function - - Future addWalletXNodeList({required Box nodes}) async { - final nodeList = await loadDefaultWalletXNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } - } - -- Next, we’ll write the function to change walletX current node to default. A handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. - - Future changeWalletXCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getWalletXDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, nodeId); - } - -- Next, in the `defaultSettingsMigration` function at the top of the file, add a new case to handle both `addWalletXNodeList` and `changeWalletXCurrentNodeToDefault` - - case “next-number-increment”: - await addWalletXNodeList(nodes: nodes); - await changeWalletXCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - break; - -- Next, increase the `initialMigrationVersion` number in `main.dart` to be the new case entry number you entered in the step above for the `defaultSettingsMigration` function. -- Next, go to `lib/view_model/node_list/node_list_view_model.dart` -- In the `reset` function, add a case for walletX: - - case WalletType.tron: - node = getTronDefaultNode(nodes: _nodeSource)!; - break; - -- Lastly, go to `cw_core/lib/node.dart`, -- In the uri getter, add a case to handle the uri setup for walletX. If the node uses http, return `Uri.http`, if not, return `Uri.https` - - case WalletType.walletX: - return Uri.https(uriRaw, ‘’); - -- Also, in the `requestNode` method, add a case for `WalletType.walletx` -- Next is the modifications to `lib/store/settings_store.dart` file: -- In the `load` function, create a variable to fetch the currentWalletxNodeId using the `PreferencesKey.currentWalletXNodeIdKey` we created earlier. -- Create another variable `walletXNode` which gets the walletx node using the nodeId variable assigned in the step above. -- Add a check to see if walletXNode is not null, if it’s not null, assign the created tronNode variable to the nodeMap with a type of walletX - - final walletXNode = nodeSource.get(walletXNodeId); - final walletXNodeId = sharedPreferences.getInt(PreferencesKey.currentWalletXNodeIdKey); - if (walletXNode != null) { - nodes[WalletType.walletx] = walletXNode; - } - -- Repeat the steps above in the `reload` function -- Next, add a case for walletX in the `_saveCurrentNode` function. - -- Run the following commands after to generate modified files in cw_core and lib - - cd cw_core && flutter pub get && dart run build_runner build --delete-conflicting-outputs && cd .. - - dart run build_runner build --delete-conflicting-outputs - -- Lastly, before we run the app to test what we’ve done so far, -- Go to `lib/src/dashboard/widgets/menu_widget.dart` and add an icon for walletX to be used within the app. -- Go to `lib/src/screens/wallet_list/wallet_list_page.dart` and add an icon for walletx, add a case for walletx also in the `imageFor` method. -- Do the same thing in `lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart` - -- One last thing before we can create a wallet for walletx, go to `lib/view_model/wallet_new_vm.dart` -- Modify the `seedPhraseWordsLength` getter by adding a case for `WalletType.walletx` - -Now you can run the codebase and successfully create a wallet for type walletX successfully. - -**Display Seeds/Keys** -- Next, we want to set up our wallet to display the seeds and/or keys in the security page of the app. -- Go to `lib/view_model/wallet_keys_view_model.dart` -- Modify the `populateItems` function by adding a case for `WalletType.walletx` in it. -- Now your seeds and/or keys should display when you go to Security and Backup -> Show seed/keys page within the app. - -**Restore Wallet** -- Go to `lib/core/seed_validator.dart` -- In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds. -- Next, go to `lib/wallet_restore_view_model.dart` -- Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key -- Add a switch case to handle the various restore modes that walletX supports -- Modify the `getCredential` method to handle the restore flows for `WalletType.walletx` -- Run the build_runner code generation command - -**Receive** -- Go to `lib/view_model/wallet_address_list/wallet_address_list_view_model.dart` -- Create an implementation of `PaymentUri` for type WalletX. -- In the uri getter, add a case for `WalletType.walletx` returning the implementation class for `PaymentUri` -- Modify the `addressList` getter to return the address/addresses for walletx - -**Balance Screen** -- Go to `lib/view_model/dashboard/balance_view_model.dart` -- Modify the function to adjust the way the balance is being displayed on the app: `isHomeScreenSettingsEnabled` -- Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) -- Same for `additionalBalanceLabel` -- Next, go to `lib/reactions/fiat_rate_update.dart` -- Modify the `startFiatRateUpdate` function and add a check for `WalletType.walletx` to return all the token currencies -- Next, go to `lib/reactions/on_current_wallet_change.dart` -- Modify the `startCurrentWalletChangeReaction` function and add a check for `WalletType.walletx` to return all the token currencies -- Lastly, go to `lib/view_model/dashboard/transaction_list_item.dart` -- In the `formattedFiatAmount` getter, add a case to handle the fiat amount conversion for `WalletType.walletx` - -**Send ViewModel** -- Go to `lib/view_model/send/send_view_model.dart` -- Modify the `_credentials` function to reflect `WalletType.walletx` -- Modify `hasMultipleTokens` to reflect wallets - -**Exchange** -- Go to lib/view_model/exchange/exchange_view_model.dart -- First, add a case for WalletType.walletx in the `initialPairBasedOnWallet` method. -- If WalletX supports tokens, go to `lib/view_model/exchange/exchange_trade_view_model.dart` -- Modify the `_checkIfCanSend` method by creating a `_isWalletXToken` that checks if the from currency is WalletX and if its tag is for walletx -- Add `_isWalletXToken` to the return logic for the method. - -**Secrets** -- Create a json file named `wallet-secrets-config.json` and put an empty curly bracket “{}” in it -- Add a new entry to `tool/utils/secret_key.dart` for walletx -- Modify the `tool/generate_secrets_config.dart` file for walletx, don’t forget to call `secrets.clear()` before adding a new set of generation logic -- Modify the `tool/import_secrets_config.dart` file for walletx -- In the `.gitignore` file, add `**/tool/.walletx-secrets-config.json` and `**/cw_walletx/lib/.secrets.g.dart` - -**HomeSettings: WalletX Tokens Display and Management** -- Go to `lib/view_model/dashboard/home_settings_view_model.dart` -- Modify the `_updateTokensList` method to add all walletx tokens if the wallet type is `WalletType.walletx`. -- Modify the `getTokenAddressBasedOnWallet` method to include a case to fetch the address for a WalletX token. -- Modify the `getToken` method to return a specific walletx token -- Modify the `addToken`, `deleteToken` and `changeTokenAvailability` methods to handle cases where the walletType is walletx - -**Buy and Sell WalletX** -- Go to `lib/entities/provider_types.dart` -- Add a case for `WalletType.walletx` in the `getAvailableBuyProviderTypes` method. Return a list of providers that support buying WalletX. -- Add a case for `WalletType.walletx` in the `getAvailableSellProviderTypes` method. Return a list of providers that support selling WalletX. - -**Restore QR setup** -- Go to `lib/view_model/restore/wallet_restore_from_qr_code.dart` -- Add the scheme for walletx in `_walletTypeMap` -- Also modify `_determineWalletRestoreMode` to include a case for walletx -- Go to `lib/view_model/restore/restore_from_qr_vm.dart` -- Modify `getCredentialsFromRestoredWallet` method -- Go to `lib/core/address_validator.dart` -- Modify the `getAddressFromStringPattern` method to add a case for `WalletType.walletx` -- and if it has tokens (ex. erc20, trc20, spl tokens) then add them to the switch case as well -- Add the scheme for walletx for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` - -**Transaction History** -- Go to `lib/view_model/transaction_details_view_model.dart` -- Add a case for `WalletType.walletx` to add the items to be displayed on the detailed view -- Modify the `_explorerUrl` method to add the blockchain explorer link for WalletX in order to view the more info on a transaction -- Modify the `_explorerDescription` to display the name of the explorer - - - - -# Points to note when adding the new wallet type - -1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add that to this function `_checkIfCanSend` in `exchange_trade_view_model.dart` -1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add a check for the tags as well in the -2. Check On/Off ramp providers that support the new wallet currency and add them accordingly in `provider_types.dart` -3. Add support for wallet uri scheme to restore from QR for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` -4. Make sure no imports are using the wallet internal package files directly, instead use the proxy layers that is created in the main lib `lib/cw_ethereum.dart` for example. (i.e try building Monero.com if you get compilation errors, then you probably missed something) - - -Copyright (C) 2018-2023 Cake Labs LLC diff --git a/lib/arbitrum/cw_arbitrum.dart b/lib/arbitrum/cw_arbitrum.dart deleted file mode 100644 index e4e5b6d64..000000000 --- a/lib/arbitrum/cw_arbitrum.dart +++ /dev/null @@ -1,244 +0,0 @@ -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 deleted file mode 100644 index 5b17abd02..000000000 --- a/lib/base/cw_base.dart +++ /dev/null @@ -1,258 +0,0 @@ -part of 'base.dart'; - -class CWBase extends Base { - @override - List getBaseWordList(String language) => EVMChainMnemonics.englishWordlist; - - WalletService createBaseWalletService(bool isDirect) => - BaseWalletService(isDirect, client: BaseClient()); - - @override - WalletCredentials createBaseNewWalletCredentials({ - required String name, - String? mnemonic, - WalletInfo? walletInfo, - String? password, - String? passphrase, - }) => - EVMChainNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); - - @override - WalletCredentials createBaseRestoreWalletFromSeedCredentials({ - required String name, - required String mnemonic, - required String password, - String? passphrase, - }) => - EVMChainRestoreWalletFromSeedCredentials( - name: name, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); - - @override - WalletCredentials createBaseRestoreWalletFromPrivateKey({ - required String name, - required String privateKey, - required String password, - }) => - EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); - - @override - WalletCredentials createBaseHardwareWalletCredentials({ - required String name, - required HardwareAccountData hwAccountData, - WalletInfo? walletInfo, - }) => - EVMChainRestoreWalletFromHardware( - name: name, - hwAccountData: hwAccountData, - walletInfo: walletInfo, - ); - - @override - String getAddress(WalletBase wallet) => (wallet as BaseWallet).walletAddresses.address; - - @override - String getPrivateKey(WalletBase wallet) { - final privateKeyHolder = (wallet as BaseWallet).evmChainPrivateKey; - if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); - return ""; - } - - @override - String getPublicKey(WalletBase wallet) { - final privateKeyInUnitInt = (wallet as BaseWallet).evmChainPrivateKey; - return privateKeyInUnitInt.address.hex; - } - - @override - TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; - - @override - TransactionPriority getBaseTransactionPrioritySlow() => EVMChainTransactionPriority.slow; - - @override - List getTransactionPriorities() => EVMChainTransactionPriority.all; - - @override - TransactionPriority deserializeBaseTransactionPriority(int raw) => - EVMChainTransactionPriority.deserialize(raw: raw); - - Object createBaseTransactionCredentials( - List outputs, { - 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, - ); - - Object createBaseTransactionCredentialsRaw( - List outputs, { - TransactionPriority? priority, - required CryptoCurrency currency, - required int feeRate, - }) => - EVMChainTransactionCredentials( - outputs, - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - ); - - @override - int formatterBaseParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); - - @override - double formatterBaseAmountToDouble({ - 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 BaseWallet).erc20Currencies; - - @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as BaseWallet).addErc20Token(token as Erc20Token); - - @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as BaseWallet).deleteErc20Token(token as Erc20Token); - - @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => - (wallet as BaseWallet).removeTokenTransactionsInHistory(token as Erc20Token); - - @override - Future getErc20Token(WalletBase wallet, String contractAddress) => - (wallet as BaseWallet).getErc20Token(contractAddress, 'base'); - - @override - CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as EVMChainTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.baseEth.title || - transaction.tokenSymbol == "BASE") { - return CryptoCurrency.baseEth; - } - - wallet as BaseWallet; - - return wallet.erc20Currencies.firstWhere( - (element) => - transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), - ); - } - - @override - void updateBaseScanUsageState(WalletBase wallet, bool isEnabled) => - (wallet as BaseWallet).updateScanProviderUsageState(isEnabled); - - @override - Web3Client? getWeb3Client(WalletBase wallet) => (wallet as BaseWallet).getWeb3Client(); - - @override - String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; - - @override - Future isApprovalRequired( - WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => - (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); - - @override - Future createTokenApproval( - WalletBase wallet, - BigInt amount, - String spender, - CryptoCurrency token, - TransactionPriority priority, - ) => - (wallet as EVMChainWallet).createApprovalTransaction( - amount, - spender, - token, - priority as EVMChainTransactionPriority, - "BASE", - ); - - @override - 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 { - 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: 8453); - - @override - List getDefaultTokenContractAddresses() => - DefaultBaseErc20Tokens().initialBaseErc20Tokens.map((e) => e.contractAddress).toList(); - - @override - bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { - final baseWallet = wallet as BaseWallet; - return baseWallet.erc20Currencies.any( - (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/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 2255c2b1a..6ff720fcd 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -82,7 +82,7 @@ class MoonPayProvider extends BuyProvider { static String get _apiKey => secrets.moonPayApiKey; - String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); + String get currencyCode => walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title.toLowerCase(); String get trackUrl => baseBuyUrl + '/transaction_receipt?transactionId='; diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index a08aff170..535ed2fb9 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -70,7 +70,7 @@ class WyreBuyProvider extends BuyProvider { final body = { 'amount': amount, 'sourceCurrency': sourceCurrency, - 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title, + 'destCurrency': walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title, 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address, 'referrerAccountId': _accountId, 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest'] @@ -100,7 +100,7 @@ class WyreBuyProvider extends BuyProvider { final body = { 'amount': amount, 'sourceCurrency': sourceCurrency, - 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title, + 'destCurrency': walletTypeToCryptoCurrency(wallet.type, chainId: wallet.chainId).title, 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address, 'accountId': _accountId, 'country': _countryCode diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 059a92c44..62c62fab9 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -338,6 +338,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.maticpoly: case CryptoCurrency.baseEth: case CryptoCurrency.arbEth: + case CryptoCurrency.arb: pattern = '0x[0-9a-zA-Z]+'; case CryptoCurrency.nano: pattern = 'nano_[0-9a-zA-Z]{60}'; diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart index a503f5c1b..7a50d2b1e 100644 --- a/lib/core/background_sync.dart +++ b/lib/core/background_sync.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/tor.dart'; @@ -16,7 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/evm/evm.dart'; class BackgroundSync { final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -26,7 +27,7 @@ class BackgroundSync { if (_isInitialized) return; const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); - + const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, @@ -45,16 +46,18 @@ class BackgroundSync { Future requestPermissions() async { if (Platform.isIOS || Platform.isMacOS) { return await _notificationsPlugin - .resolvePlatformSpecificImplementation() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ) ?? false; + .resolvePlatformSpecificImplementation() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ) ?? + false; } else if (Platform.isAndroid) { return await _notificationsPlugin - .resolvePlatformSpecificImplementation() - ?.areNotificationsEnabled() ?? false; + .resolvePlatformSpecificImplementation() + ?.areNotificationsEnabled() ?? + false; } return false; } @@ -62,7 +65,7 @@ class BackgroundSync { Future showNotification(String title, String content) async { await _initializeNotifications(); final hasPermission = await requestPermissions(); - + if (!hasPermission) { printV('Notification permissions not granted'); return; @@ -112,10 +115,11 @@ class BackgroundSync { .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type)) .toList(); for (int i = 0; i < moneroWallets.length; i++) { - final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, isBackground: true); + final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, + isBackground: true); int syncedTicks = 0; final keyService = getIt.get(); - + int stuckTicks = 0; inner: @@ -123,7 +127,9 @@ class BackgroundSync { await Future.delayed(const Duration(seconds: 1)); final syncStatus = wallet.syncStatus; final progress = syncStatus.progress(); - if (syncStatus is ConnectedSyncStatus || syncStatus is AttemptingSyncStatus || syncStatus is NotConnectedSyncStatus) { + if (syncStatus is ConnectedSyncStatus || + syncStatus is AttemptingSyncStatus || + syncStatus is NotConnectedSyncStatus) { stuckTicks++; if (stuckTicks > 30) { printV("${wallet.name} STUCK SYNCING"); @@ -134,7 +140,13 @@ class BackgroundSync { } if (syncStatus is NotConnectedSyncStatus) { printV("${wallet.name} NOT CONNECTED"); - final node = settingsStore.getCurrentNode(wallet.type); + + int? chainId; + if (isEVMCompatibleChain(wallet.type)) { + chainId = evm!.getSelectedChainId(wallet); + } + + final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId); await wallet.connectToNode(node: node); await wallet.startBackgroundSync(); printV("STARTED SYNC"); @@ -147,7 +159,8 @@ class BackgroundSync { syncedTicks = 0; printV("WALLET $i SYNCED"); try { - await wallet.stopBackgroundSync((await keyService.getWalletPassword(walletName: wallet.name))); + await wallet.stopBackgroundSync( + (await keyService.getWalletPassword(walletName: wallet.name))); } catch (e) { printV("error stopping sync: $e"); } @@ -185,25 +198,29 @@ class BackgroundSync { final sortedTxs = txs.transactions.values.toList()..sort((a, b) => a.date.compareTo(b.date)); final sharedPreferences = await SharedPreferences.getInstance(); for (final tx in sortedTxs) { - final lastTriggerString = sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name)); - final lastTriggerDate = lastTriggerString != null - ? DateTime.parse(lastTriggerString) - : DateTime.now(); + final lastTriggerString = + sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name)); + final lastTriggerDate = + lastTriggerString != null ? DateTime.parse(lastTriggerString) : DateTime.now(); final keys = sharedPreferences.getKeys(); if (tx.date.isBefore(lastTriggerDate)) { - printV("w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})"); + printV( + "w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})"); continue; } - await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), tx.date.add(Duration(minutes: 1)).toIso8601String()); + await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), + tx.date.add(Duration(minutes: 1)).toIso8601String()); final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent"; if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) { - await showNotification("$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}"); + await showNotification( + "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}"); } - printV("${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}"); + printV( + "${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}"); } wallet.id; await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name)); await wallet.close(shouldCleanup: true); } } -} \ No newline at end of file +} diff --git a/lib/core/node_switching_service.dart b/lib/core/node_switching_service.dart index 6f7b36dc3..c17ec2e47 100644 --- a/lib/core/node_switching_service.dart +++ b/lib/core/node_switching_service.dart @@ -7,6 +7,8 @@ import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:hive/hive.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; class NodeSwitchingService { NodeSwitchingService({ @@ -142,12 +144,21 @@ class NodeSwitchingService { return; } - final walletType = appStore.wallet!.type; - final currentNode = settingsStore.getCurrentNode(walletType); + final wallet = appStore.wallet!; + final walletType = wallet.type; + + WalletType nodeWalletType = walletType; + + int? chainId; + if (isEVMCompatibleChain(walletType)) { + chainId = evm!.getSelectedChainId(appStore.wallet!); + } + + final currentNode = settingsStore.getCurrentNode(nodeWalletType, chainId: chainId); // Get all trusted nodes for this wallet type final trustedNodes = nodeSource.values - .where((node) => node.type == walletType && node.isEnabledForAutoSwitching) + .where((node) => node.type == nodeWalletType && node.isEnabledForAutoSwitching) .toList(); if (trustedNodes.isEmpty) { @@ -157,26 +168,26 @@ class NodeSwitchingService { } // Initialize used nodes list for this wallet type if it does not exist - _usedNodeKeys.putIfAbsent(walletType, () => []); + _usedNodeKeys.putIfAbsent(nodeWalletType, () => []); // Add current node to used list if not already there - if (!_usedNodeKeys[walletType]!.contains(currentNode.key)) { - _usedNodeKeys[walletType]!.add(currentNode.key); + if (!_usedNodeKeys[nodeWalletType]!.contains(currentNode.key)) { + _usedNodeKeys[nodeWalletType]!.add(currentNode.key); } // Try to find an active unused node - Node? nextNode = await _findActiveNode(trustedNodes, walletType); + Node? nextNode = await _findActiveNode(trustedNodes, nodeWalletType); // If all trusted nodes have been used, check if we should reset if (nextNode == null) { - printV('All trusted nodes have been tried for wallet type: $walletType'); + printV('All trusted nodes have been tried for wallet type: $nodeWalletType'); // If we've tried all nodes and still haven't reached max attempts, reset and try again if (_switchingAttempts < _maxNodeSwitchingAttempts) { printV('Resetting used nodes list and trying again'); - _usedNodeKeys[walletType]!.clear(); + _usedNodeKeys[nodeWalletType]!.clear(); // Try again with cleared used list - nextNode = await _findActiveNode(trustedNodes, walletType); + nextNode = await _findActiveNode(trustedNodes, nodeWalletType); } // If still no active node found, we give up @@ -188,16 +199,16 @@ class NodeSwitchingService { } // Ensure the selected node is marked as used - if (!_usedNodeKeys[walletType]!.contains(nextNode.key)) { - _usedNodeKeys[walletType]!.add(nextNode.key); + if (!_usedNodeKeys[nodeWalletType]!.contains(nextNode.key)) { + _usedNodeKeys[nodeWalletType]!.add(nextNode.key); } printV( 'Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw} (attempt $_switchingAttempts/$_maxNodeSwitchingAttempts)'); - printV('Used nodes for ${walletType}: ${_usedNodeKeys[walletType]}'); + printV('Used nodes for ${nodeWalletType}: ${_usedNodeKeys[nodeWalletType]}'); // Update the current node in settings - settingsStore.nodes[walletType] = nextNode; + settingsStore.nodes[nodeWalletType] = nextNode; // Connect the wallet to the new node await appStore.wallet!.connectToNode(node: nextNode); diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index e7facde3b..dae50a2f9 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,12 +1,9 @@ -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'; import 'package:cake_wallet/entities/mnemonic_item.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; @@ -36,14 +33,15 @@ class SeedValidator extends Validator { case WalletType.monero: return monero!.getMoneroWordList(language); case WalletType.ethereum: - return ethereum!.getEthereumWordList(language); + case WalletType.polygon: + case WalletType.base: + case WalletType.arbitrum: + return evm!.getEVMWordList(language); case WalletType.bitcoinCash: return getBitcoinWordList(language); case WalletType.nano: case WalletType.banano: return nano!.getNanoWordList(language); - case WalletType.polygon: - return polygon!.getPolygonWordList(language); case WalletType.solana: return solana!.getSolanaWordList(language); case WalletType.tron: @@ -54,10 +52,6 @@ class SeedValidator extends Validator { return zano!.getWordList(language); case WalletType.decred: 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/trade_monitor.dart b/lib/core/trade_monitor.dart index 8dbb6407d..e28be8ff5 100644 --- a/lib/core/trade_monitor.dart +++ b/lib/core/trade_monitor.dart @@ -95,7 +95,6 @@ class TradeMonitor { } if (_tradeTimers.containsKey(trade.id)) { - printV('Trade ${trade.id} is already being monitored'); continue; } else { _startTradeMonitoring(trade, provider!); @@ -141,7 +140,6 @@ class TradeMonitor { } if (_isFinalState(trade.state)) { - printV('Skipping trade ${trade.id} because it\'s in a final state'); return true; } diff --git a/lib/core/universal_address_detector.dart b/lib/core/universal_address_detector.dart index 41a4f5948..4c3933844 100644 --- a/lib/core/universal_address_detector.dart +++ b/lib/core/universal_address_detector.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; class AddressDetectionResult { AddressDetectionResult({ @@ -16,6 +17,7 @@ class AddressDetectionResult { this.callbackMessage, required this.isValid, this.errorMessage, + this.chainId, }); final String address; @@ -29,6 +31,7 @@ class AddressDetectionResult { final String? callbackMessage; final bool isValid; final String? errorMessage; + final int? chainId; } /// Universal address detector that can identify cryptocurrency addresses from various formats @@ -70,11 +73,13 @@ class UniversalAddressDetector { // Determine currency from scheme final currency = CryptoCurrency.fromString(uri.scheme.toLowerCase()); + final walletType = cryptoCurrencyToWalletType(currency); + final chainId = getChainIdByCryptoCurrency(currency); return AddressDetectionResult( address: paymentRequest.address, detectedCurrency: currency, - detectedWalletType: cryptoCurrencyToWalletType(currency), + detectedWalletType: walletType, amount: paymentRequest.amount, note: paymentRequest.note, scheme: paymentRequest.scheme, @@ -82,6 +87,7 @@ class UniversalAddressDetector { callbackUrl: paymentRequest.callbackUrl, callbackMessage: paymentRequest.callbackMessage, isValid: true, + chainId: chainId, ); } catch (e) { return AddressDetectionResult( @@ -226,11 +232,15 @@ class UniversalAddressDetector { // Test each pattern in order of specificity for (final pattern in detectionPatterns) { if (pattern.pattern.hasMatch(cleanInput)) { + final walletType = cryptoCurrencyToWalletType(pattern.currency); + final chainId = getChainIdByCryptoCurrency(pattern.currency); + return AddressDetectionResult( address: cleanInput, detectedCurrency: pattern.currency, - detectedWalletType: cryptoCurrencyToWalletType(pattern.currency), + detectedWalletType: walletType, isValid: true, + chainId: chainId, ); } } diff --git a/lib/di.dart b/lib/di.dart index fcf45fb17..31703b4a2 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,10 +4,9 @@ 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'; +import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; @@ -87,12 +86,10 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/exchange/trade.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/decred/decred.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; @@ -1204,7 +1201,10 @@ Future setup({ SettingsStoreBase.walletPasswordDirectInput, ); case WalletType.ethereum: - return ethereum!.createEthereumWalletService(SettingsStoreBase.walletPasswordDirectInput); + case WalletType.polygon: + case WalletType.base: + case WalletType.arbitrum: + return evm!.createEVMWalletService(param1, SettingsStoreBase.walletPasswordDirectInput); case WalletType.bitcoinCash: return bitcoinCash!.createBitcoinCashWalletService(_unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput); case WalletType.dogecoin: @@ -1212,8 +1212,6 @@ Future setup({ case WalletType.nano: case WalletType.banano: return nano!.createNanoWalletService(SettingsStoreBase.walletPasswordDirectInput); - case WalletType.polygon: - return polygon!.createPolygonWalletService(SettingsStoreBase.walletPasswordDirectInput); case WalletType.solana: return solana!.createSolanaWalletService(SettingsStoreBase.walletPasswordDirectInput); case WalletType.tron: @@ -1224,10 +1222,6 @@ Future setup({ return zano!.createZanoWalletService(); case WalletType.decred: 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/ens_record.dart b/lib/entities/ens_record.dart index 35ed46995..8e725f21f 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,7 +1,5 @@ -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'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; @@ -10,25 +8,11 @@ import 'package:ens_dart/ens_dart.dart'; import 'package:web3dart/web3dart.dart'; class EnsRecord { - static Future fetchEnsAddress(String name, {WalletBase? wallet}) async { - Web3Client? _client; - if (wallet != null && wallet.type == WalletType.ethereum) { - _client = ethereum!.getWeb3Client(wallet); - } - - if (wallet != null && wallet.type == WalletType.polygon) { - _client = polygon!.getWeb3Client(wallet); - } - - if (wallet != null && wallet.type == WalletType.base) { - _client = base!.getWeb3Client(wallet); - } - - if (wallet != null && wallet.type == WalletType.arbitrum) { - _client = arbitrum!.getWeb3Client(wallet); + if (wallet != null && (isEVMCompatibleChain(wallet.type))) { + _client = evm!.getWeb3Client(wallet); } if (_client == null) { diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 4f856db7d..1b7a1e0d3 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -19,6 +19,7 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.haven: path = 'assets/haven_node_list.yml'; break; + // TODO: (refactoring) each wallet would have its path, so `wallet.nodePath` would be decided based on chain id in Evm wallet case WalletType.ethereum: path = 'assets/ethereum_server_list.yml'; break; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index a9acc2a6b..8109c4ea2 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -84,6 +84,7 @@ class PreferencesKey { static const useArbiScan = 'use_arbitrum_scan'; static const useTronGrid = 'use_trongrid'; static const useMempoolFeeAPI = 'use_mempool_fee_api'; + static const evmHiddenChainIds = 'evm_hidden_chain_ids'; static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; @@ -94,6 +95,7 @@ class PreferencesKey { static const lookupsOpenAlias = 'looks_up_open_alias'; static const lookupsENS = 'looks_up_ens'; static const lookupsWellKnown = 'looks_up_well_known'; + static const useBlinkProtection = 'use_blink_protection'; static const usePayjoin = 'use_payjoin'; static const showPayjoinCard = 'show_payjoin_card'; static const showCameraConsent = 'show_camera_consent'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 4538da8e6..d346e3460 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,11 +1,8 @@ -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'; import 'package:cake_wallet/dogecoin/dogecoin.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/decred/decred.dart'; @@ -23,15 +20,13 @@ List priorityForWalletType(WalletType type) { case WalletType.litecoin: return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.ethereum: - return ethereum!.getTransactionPriorities(); + case WalletType.polygon: + case WalletType.base: + return evm!.getTransactionPriorities(); case WalletType.bitcoinCash: return bitcoinCash!.getTransactionPriorities(); case WalletType.dogecoin: return dogecoin!.getTransactionPriorities(); - case WalletType.polygon: - return polygon!.getTransactionPriorities(); - case WalletType.base: - return base!.getTransactionPriorities(); case WalletType.arbitrum: case WalletType.nano: case WalletType.banano: diff --git a/lib/entities/wallet_contact.dart b/lib/entities/wallet_contact.dart index 0939f18f2..641dedb80 100644 --- a/lib/entities/wallet_contact.dart +++ b/lib/entities/wallet_contact.dart @@ -1,9 +1,9 @@ import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_type.dart'; class WalletContact implements ContactBase { - WalletContact(this.address, this.name, this.type); - //: super(name, address, type); + WalletContact(this.address, this.name, this.type, {this.walletType}); @override String address; @@ -13,4 +13,8 @@ class WalletContact implements ContactBase { @override CryptoCurrency type; + + /// Wallet type of the wallet this contact belongs to + /// Used for EVM chain filtering + final WalletType? walletType; } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart deleted file mode 100644 index 26bb8cd19..000000000 --- a/lib/ethereum/cw_ethereum.dart +++ /dev/null @@ -1,283 +0,0 @@ -part of 'ethereum.dart'; - -class CWEthereum extends Ethereum { - @override - List getEthereumWordList(String language) => EVMChainMnemonics.englishWordlist; - - WalletService createEthereumWalletService(bool isDirect) => - EthereumWalletService(isDirect, client: EthereumClient()); - - @override - WalletCredentials createEthereumNewWalletCredentials({ - required String name, - String? mnemonic, - WalletInfo? walletInfo, - String? password, - String? passphrase, - }) => - EVMChainNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); - - @override - WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ - required String name, - required String mnemonic, - required String password, - String? passphrase, - }) => - EVMChainRestoreWalletFromSeedCredentials( - name: name, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); - - @override - WalletCredentials createEthereumRestoreWalletFromPrivateKey({ - required String name, - required String privateKey, - required String password, - }) => - EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); - - @override - WalletCredentials createEthereumHardwareWalletCredentials({ - required String name, - required HardwareAccountData hwAccountData, - WalletInfo? walletInfo, - }) => - EVMChainRestoreWalletFromHardware( - name: name, hwAccountData: hwAccountData, walletInfo: walletInfo); - - @override - String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; - - @override - String getPrivateKey(WalletBase wallet) { - final privateKeyHolder = (wallet as EthereumWallet).evmChainPrivateKey; - if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); - return ""; - } - - @override - String getPublicKey(WalletBase wallet) { - final privateKeyInUnitInt = (wallet as EthereumWallet).evmChainPrivateKey; - return privateKeyInUnitInt.address.hex; - } - - @override - TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; - - @override - TransactionPriority getEthereumTransactionPrioritySlow() => EVMChainTransactionPriority.slow; - - @override - List getTransactionPriorities() => EVMChainTransactionPriority.all; - - @override - TransactionPriority deserializeEthereumTransactionPriority(int raw) => - EVMChainTransactionPriority.deserialize(raw: raw); - - Object createEthereumTransactionCredentials( - List outputs, { - 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, - memo: out.memo)) - .toList(), - priority: priority as EVMChainTransactionPriority, - currency: currency, - feeRate: feeRate, - ); - - Object createEthereumTransactionCredentialsRaw( - List outputs, { - TransactionPriority? priority, - required CryptoCurrency currency, - required int feeRate, - }) => - EVMChainTransactionCredentials( - outputs, - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - ); - - @override - int formatterEthereumParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); - - @override - double formatterEthereumAmountToDouble( - {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 EthereumWallet).erc20Currencies; - - @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as EthereumWallet).addErc20Token(token as Erc20Token); - - @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as EthereumWallet).deleteErc20Token(token as Erc20Token); - - @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => - (wallet as EthereumWallet).removeTokenTransactionsInHistory(token as Erc20Token); - - @override - Future getErc20Token(WalletBase wallet, String contractAddress) => - (wallet as EthereumWallet).getErc20Token(contractAddress, 'eth'); - - @override - CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as EVMChainTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.eth.title) { - return CryptoCurrency.eth; - } - - wallet as EthereumWallet; - - return wallet.erc20Currencies.firstWhere( - (element) => transaction.tokenSymbol == element.symbol, - ); - } - - @override - void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) => - (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); - - @override - Web3Client? getWeb3Client(WalletBase wallet) => (wallet as EthereumWallet).getWeb3Client(); - - @override - String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; - - @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); - } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( - service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); - } - } - - @override - HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => - EVMChainLedgerService(connection); - - @override - HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) => - EVMChainBitboxService(manager); - - @override - HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect) => - EVMChainTrezorService(connect); - - @override - List getDefaultTokenContractAddresses() { - 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()); - } - - @override - Future isApprovalRequired( - WalletBase wallet, String tokenContract, String spender, BigInt requiredAmount) => - (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); - - @override - Future createTokenApproval(WalletBase wallet, BigInt amount, String spender, - CryptoCurrency token, TransactionPriority priority) => - (wallet as EVMChainWallet).createApprovalTransaction( - 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); - - // Integrations - @override - Future getDEuroSavingsBalance(WalletBase wallet) => - DEuro(wallet as EthereumWallet).savingsBalance; - - @override - Future getDEuroAccruedInterest(WalletBase wallet) => - DEuro(wallet as EthereumWallet).accruedInterest; - - @override - Future getDEuroInterestRate(WalletBase wallet) => - DEuro(wallet as EthereumWallet).interestRate; - - @override - Future getDEuroSavingsApproved(WalletBase wallet) => - DEuro(wallet as EthereumWallet).approvedBalance; - - @override - Future addDEuroSaving( - WalletBase wallet, BigInt amount, TransactionPriority priority) => - DEuro(wallet as EthereumWallet) - .depositSavings(amount, priority as EVMChainTransactionPriority); - - @override - Future removeDEuroSaving( - WalletBase wallet, BigInt amount, TransactionPriority priority) => - DEuro(wallet as EthereumWallet) - .withdrawSavings(amount, priority as EVMChainTransactionPriority); - - @override - Future reinvestDEuroInterest( - WalletBase wallet, TransactionPriority priority) => - DEuro(wallet as EthereumWallet).reinvestInterest(priority as EVMChainTransactionPriority); - - @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/evm/cw_evm.dart b/lib/evm/cw_evm.dart new file mode 100644 index 000000000..952a2a048 --- /dev/null +++ b/lib/evm/cw_evm.dart @@ -0,0 +1,516 @@ +part of 'evm.dart'; + +class CWEVM extends EVM { + @override + List getEVMWordList(String language) => EVMChainMnemonics.englishWordlist; + + @override + WalletService createEVMWalletService(WalletType walletType, bool isDirect) { + return EVMChainWalletService(isDirect); + } + + @override + WalletCredentials createEVMNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + String? password, + String? mnemonic, + String? passphrase, + }) { + return EVMChainNewWalletCredentials( + name: name, + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + } + + @override + WalletCredentials createEVMRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + String? passphrase, + }) { + return EVMChainRestoreWalletFromSeedCredentials( + name: name, + password: password, + mnemonic: mnemonic, + passphrase: passphrase, + ); + } + + @override + WalletCredentials createEVMRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) { + return EVMChainRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: privateKey, + ); + } + + @override + WalletCredentials createEVMHardwareWalletCredentials({ + required String name, + required HardwareAccountData hwAccountData, + WalletInfo? walletInfo, + }) { + return EVMChainRestoreWalletFromHardware( + name: name, + hwAccountData: hwAccountData, + walletInfo: walletInfo, + ); + } + + @override + String getAddress(WalletBase wallet) => (wallet as EVMChainWallet).walletAddresses.address; + + @override + String getPrivateKey(WalletBase wallet) { + final privateKeyHolder = (wallet as EVMChainWallet).evmChainPrivateKey; + if (privateKeyHolder is EthPrivateKey) { + return bytesToHex(privateKeyHolder.privateKey); + } + return ""; + } + + @override + String getPublicKey(WalletBase wallet) { + final privateKeyInUnitInt = (wallet as EVMChainWallet).evmChainPrivateKey; + return privateKeyInUnitInt.address.hex; + } + + @override + TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; + + @override + TransactionPriority getEVMTransactionPrioritySlow() => EVMChainTransactionPriority.slow; + + @override + List getTransactionPriorities() => EVMChainTransactionPriority.all; + + @override + TransactionPriority deserializeEVMTransactionPriority(int raw) => + EVMChainTransactionPriority.deserialize(raw: raw); + + @override + Object createEVMTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + TransactionPriority? priority, + int? feeRate, + bool useBlinkProtection = true, + }) { + return 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, + memo: out.memo)) + .toList(), + priority: priority as EVMChainTransactionPriority?, + currency: currency, + feeRate: feeRate, + useBlinkProtection: useBlinkProtection, + ); + } + + @override + Object createEVMTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + bool useBlinkProtection = true, + }) { + return EVMChainTransactionCredentials( + outputs, + priority: priority as EVMChainTransactionPriority?, + currency: currency, + feeRate: feeRate, + useBlinkProtection: useBlinkProtection, + ); + } + + @override + int formatterEVMParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); + + @override + double formatterEVMAmountToDouble({ + 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 EVMChainWallet).erc20Currencies; + + @override + Future addErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EVMChainWallet).addErc20Token(token as Erc20Token); + + @override + Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => + (wallet as EVMChainWallet).deleteErc20Token(token as Erc20Token); + + @override + Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => + (wallet as EVMChainWallet).removeTokenTransactionsInHistory(token as Erc20Token); + + @override + Future getErc20Token(WalletBase wallet, String contractAddress) { + final evmWallet = wallet as EVMChainWallet; + final chainName = EVMChainUtils.getDefaultTokenSymbol(evmWallet.selectedChainId).toLowerCase(); + return evmWallet.getErc20Token(contractAddress, chainName); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as EVMChainTransactionInfo; + final evmWallet = wallet as EVMChainWallet; + + final nativeCurrency = evmWallet.currency; + final nativeCurrencyTitle = nativeCurrency.title; + final currentChainId = evmWallet.selectedChainId; + + // If transaction is from a different chain, we will return native currency as fallback + // This can happen during chain switching when old transactions are still visible + if (transaction.chainId != currentChainId) { + return nativeCurrency; + } + + if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title || + transaction.tokenSymbol == "MATIC") { + return CryptoCurrency.maticpoly; + } + + if (transaction.tokenSymbol == nativeCurrencyTitle) { + return nativeCurrency; + } + + // Otherwise, it's an ERC20 token + // Also using firstWhereOrNull to handle cases where token isn't found (e.g., during chain switch) + final erc20Token = evmWallet.erc20Currencies.firstWhereOrNull( + (element) => + transaction.contractAddress?.toLowerCase() == element.contractAddress.toLowerCase(), + ); + + return erc20Token ?? nativeCurrency; + } + + @override + void updateScanProviderUsageState(WalletBase wallet, bool isEnabled) => + (wallet as EVMChainWallet).updateScanProviderUsageState(isEnabled); + + @override + Web3Client? getWeb3Client(WalletBase wallet) => (wallet as EVMChainWallet).getWeb3Client(); + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; + + @override + Future isApprovalRequired( + WalletBase wallet, + String tokenContract, + String spender, + BigInt requiredAmount, + ) => + (wallet as EVMChainWallet).isApprovalRequired(tokenContract, spender, requiredAmount); + + @override + Future createTokenApproval( + WalletBase wallet, + BigInt amount, + String spender, + CryptoCurrency token, + TransactionPriority? priority, { + bool useBlinkProtection = true, + }) { + final evmWallet = wallet as EVMChainWallet; + final feeCurrency = EVMChainUtils.getFeeCurrency(evmWallet.selectedChainId); + return evmWallet.createApprovalTransaction( + amount, + spender, + token, + priority as EVMChainTransactionPriority?, + feeCurrency, + useBlinkProtection: useBlinkProtection, + ); + } + + @override + Future createRawCallDataTransaction( + WalletBase wallet, + String to, + String dataHex, + BigInt valueWei, + TransactionPriority? priority, { + bool useBlinkProtection = true, + }) => + (wallet as EVMChainWallet).createCallDataTransaction( + to, + dataHex, + valueWei, + priority as EVMChainTransactionPriority?, + useBlinkProtection: useBlinkProtection, + ); + + @override + Future setHardwareWalletService( + WalletBase wallet, + HardwareWalletService service, + ) async { + final evmWallet = wallet as EVMChainWallet; + final privateKey = evmWallet.evmChainPrivateKey; + final derivationPath = (await wallet.walletInfo.getDerivationInfo()).derivationPath; + + if (service is EVMChainLedgerService) { + (privateKey as EvmLedgerCredentials) + .setLedgerConnection(service.ledgerConnection, derivationPath); + } else if (service is EVMChainBitboxService) { + (privateKey as EvmBitboxCredentials).setBitbox(service.manager, derivationPath); + } else if (service is EVMChainTrezorService) { + (privateKey as EvmTrezorCredentials).setTrezorConnect(service.connect, derivationPath); + } + } + + @override + HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => + EVMChainLedgerService(connection); + + @override + HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) => + EVMChainBitboxService(manager); + + @override + HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect) => + EVMChainTrezorService(connect); + + @override + List getDefaultTokenContractAddresses(WalletBase wallet) { + final chainId = getSelectedChainId(wallet); + if (chainId == null) return []; + return EVMChainDefaultTokens.getDefaultTokenAddresses(chainId); + } + + @override + bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { + final evmWallet = wallet as EVMChainWallet; + return evmWallet.erc20Currencies + .any((element) => element.contractAddress.toLowerCase() == contractAddress.toLowerCase()); + } + + @override + String? getEVMNativeEstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).nativeTxEstimatedFee; + + @override + String? getEVMERC20EstimatedFee(WalletBase wallet) => + (wallet as EVMChainWallet).erc20TxEstimatedFee; + + // Chain-specific integrations (only for Ethereum) + @override + Future? getDEuroSavingsBalance(WalletBase wallet) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).savingsBalance; + } + return null; + } + + @override + Future? getDEuroAccruedInterest(WalletBase wallet) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).accruedInterest; + } + return null; + } + + @override + Future? getDEuroInterestRate(WalletBase wallet) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).interestRate; + } + return null; + } + + @override + Future? getDEuroSavingsApproved(WalletBase wallet) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).approvedBalance; + } + return null; + } + + @override + Future? addDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).depositSavings(amount, priority as EVMChainTransactionPriority); + } + return null; + } + + @override + Future? removeDEuroSaving( + WalletBase wallet, BigInt amount, TransactionPriority priority) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).withdrawSavings(amount, priority as EVMChainTransactionPriority); + } + return null; + } + + @override + Future? reinvestDEuroInterest( + WalletBase wallet, TransactionPriority priority) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).reinvestInterest(priority as EVMChainTransactionPriority); + } + return null; + } + + @override + Future? enableDEuroSaving(WalletBase wallet, TransactionPriority priority) { + if (wallet.chainId == 1 && wallet is EVMChainWallet) { + return DEuro(wallet).enableSavings(priority as EVMChainTransactionPriority); + } + return null; + } + + // Registry helper methods + static final EvmChainRegistry _registry = EvmChainRegistry(); + + @override + int getChainIdByWalletType(WalletType walletType) { + final config = _registry.getChainConfigByWalletType(walletType); + return config?.chainId ?? 1; // Default to Ethereum + } + + @override + String getChainNameByWalletType(WalletType walletType) { + final config = _registry.getChainConfigByWalletType(walletType); + return config?.shortCode ?? 'eth'; + } + + @override + String getTokenNameByWalletType(WalletType walletType) { + final config = _registry.getChainConfigByWalletType(walletType); + return config?.nativeCurrency.title ?? 'ETH'; + } + + @override + String getCaip2ByChainId(int chainId) { + final config = _registry.getChainConfig(chainId); + return config?.caip2 ?? 'eip155:1'; + } + + @override + String getChainNameByChainId(int chainId) { + final config = _registry.getChainConfig(chainId); + return config?.shortCode ?? 'eth'; + } + + @override + String getTokenNameByChainId(int chainId) { + final config = _registry.getChainConfig(chainId); + return config?.nativeCurrency.title ?? 'ETH'; + } + + @override + int? getChainIdByTag(String tag) { + final config = _registry.getChainConfigByTag(tag); + return config?.chainId; + } + + @override + int? getChainIdByTitle(String title) { + // Try as tag first (uppercase) + final tagResult = getChainIdByTag(title.toUpperCase()); + if (tagResult != null) return tagResult; + + // Try as lowercase title + return getChainIdByTag(title.toLowerCase()); + } + + @override + WalletType? getWalletTypeByChainId(int chainId) { + return _registry.getWalletTypeByChainId(chainId); + } + + @override + List getAllChains() { + final allChains = _registry.getAllChains(); + return allChains + .map((config) => ChainInfo( + chainId: config.chainId, + name: config.name, + shortCode: config.shortCode, + )) + .toList(); + } + + @override + ChainInfo? getCurrentChain(WalletBase wallet) { + if (wallet is EVMChainWallet) { + final config = wallet.selectedChainConfig; + if (config == null) return null; + return ChainInfo( + chainId: config.chainId, + name: config.name, + shortCode: config.shortCode, + ); + } + return null; + } + + @override + int? getSelectedChainId(WalletBase wallet) { + if (wallet is EVMChainWallet) { + return wallet.selectedChainId; + } + return null; + } + + @override + Future selectChain(WalletBase wallet, int chainId, {required Node node}) async { + if (wallet is EVMChainWallet) { + await wallet.selectChain(chainId, node: node); + } + } + + @override + String? getExplorerUrlForChainId(int chainId, {bool showProtocol = true}) { + final config = _registry.getChainConfig(chainId); + + if (config != null && config.explorerUrls.isNotEmpty) { + final url = config.explorerUrls.first; + return showProtocol + ? url + : url.replaceAll('https://', '').replaceAll('http://', '').split('/')[0]; + } + return null; + } + + @override + bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId); +} diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart index 0ceeba9c2..8e2413c8a 100644 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -29,7 +29,7 @@ class ChainflipExchangeProvider extends ExchangeProvider { CryptoCurrency.sol, CryptoCurrency.usdcsol, CryptoCurrency.arbEth, - // TODO: Add CryptoCurrency.usdcarb + CryptoCurrency.usdcArb, // TODO: Add CryptoCurrency.dot ]; diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index ca003b435..31ab6d55b 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -353,6 +353,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { return 'lightning'; case 'AVAXC': return 'cchain'; + case 'ARB': + return 'arbitrum'; default: return tag.toLowerCase(); } diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 09b305620..6065b05fc 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -443,6 +443,8 @@ class ExolixExchangeProvider extends ExchangeProvider { switch (tag) { case 'POLY': return 'Polygon'; + case 'ARB': + return 'Arbitrum'; default: return tag; } diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index 5641d2bc4..b2915ee84 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -433,6 +433,8 @@ class LetsExchangeExchangeProvider extends ExchangeProvider { return 'ERC20'; case 'BSC': return 'BEP20'; + case 'ARB': + return 'ARBITRUM'; default: return currency.tag!; } diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index cd639b6e1..1b9e6a4f1 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -346,15 +346,13 @@ class StealthExExchangeProvider extends ExchangeProvider { // Parsing 'from' currency with network tag final fromCurrency = deposit['symbol'] as String; final fromNetwork = deposit['network'] as String?; - final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? ''); - final fromTag = _normalizedFromNetwork == 'mainnet' ? null : fromNetwork; + final fromTag = fromNetwork == 'mainnet' ? null : fromNetwork; final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag); // Parsing 'to' currency with network tag final toCurrency = withdrawal['symbol'] as String; final toNetwork = withdrawal['network'] as String?; - final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? ''); - final toTag = _normalizedToNetwork == 'mainnet' ? null : toNetwork; + final toTag = toNetwork == 'mainnet' ? null : toNetwork; final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag); final payoutAddress = withdrawal['address'] as String; @@ -381,7 +379,8 @@ class StealthExExchangeProvider extends ExchangeProvider { createdAt: createdAt, isRefund: status == 'refunded', extraId: extraId, - userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}', + userCurrencyFromRaw: + '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}', userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}', ); } @@ -436,21 +435,13 @@ class StealthExExchangeProvider extends ExchangeProvider { return null; } - String _getName(CryptoCurrency currency) { if (currency == CryptoCurrency.usdcEPoly) return 'usdce'; return currency.title.toLowerCase(); } - String _normalizeNetworkType(String network) { - return switch (network.toUpperCase()) { - 'ARBITRUM' => 'mainnet', - _ => network, - }; - } - String _getNetwork(CryptoCurrency currency) { - if (currency == CryptoCurrency.arb) return 'arbitrum'; + if (currency == CryptoCurrency.arb || currency.tag == 'ARB') return 'arbitrum'; if (currency.tag == null) return 'mainnet'; if (currency == CryptoCurrency.maticpoly) return 'mainnet'; diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart index 8b39bda97..4b399d6db 100644 --- a/lib/exchange/provider/swapsxyz_exchange_provider.dart +++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart @@ -751,6 +751,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider { 'KAS' => 'KASPA', 'TON' => 'TONCOIN', 'BCH' => 'BITCOIN CASH', + 'ARB' => 'ARBITRUM', _ => network.toUpperCase(), }; } diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 59e5aff1a..1b27d7598 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -64,7 +64,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { static const coinPath = '/coin'; static const providersListPath = '/exchanges'; - String _lastUsedRateId; List _provider; @@ -101,7 +100,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { final uri = await _getUri(coinPath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); - if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -119,13 +117,12 @@ class TrocadorExchangeProvider extends ExchangeProvider { } @override - Future fetchRate({ - required CryptoCurrency from, - required CryptoCurrency to, - required double amount, - required bool isFixedRateMode, - required bool isReceiveAmount - }) async { + Future fetchRate( + {required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount}) async { try { if (amount == 0) return 0.0; @@ -143,7 +140,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { final uri = await _getUri(newRatePath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); - final responseJSON = json.decode(response.body) as Map; final fromAmount = double.parse(responseJSON['amount_from'].toString()); @@ -240,7 +236,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { if (isFixedRateMode) 'amount_to': request.toAmount, 'address': request.toAddress, 'refund': request.refundAddress, - 'refund_memo' : '0', + 'refund_memo': '0', }; if (isFixedRateMode) { @@ -279,13 +275,12 @@ class TrocadorExchangeProvider extends ExchangeProvider { final uri = await _getUri(createTradePath, params); final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}); - - + if (response.statusCode == 400) { final responseJSON = json.decode(response.body) as Map; final error = responseJSON['error'] as String; final message = responseJSON['message'] as String; - + ExchangeProviderLogger.logError( provider: description, function: 'createTrade', @@ -304,7 +299,7 @@ class TrocadorExchangeProvider extends ExchangeProvider { 'url': uri.toString(), }, ); - + throw Exception('${error}\n$message'); } @@ -402,10 +397,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { @override Future findTradeById({required String id}) async { final uri = await _getUri(tradePath, {'id': id}); - return ProxyWrapper().get(clearnetUri: uri, headers: {'API-Key': apiKey}).then((response) async { + return ProxyWrapper() + .get(clearnetUri: uri, headers: {'API-Key': apiKey}).then((response) async { if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); - final responseListJson = json.decode(response.body) as List; final responseJSON = responseListJson.first; @@ -422,15 +417,22 @@ class TrocadorExchangeProvider extends ExchangeProvider { final fromCurrency = responseJSON['ticker_from'] as String; final fromNetwork = responseJSON['network_from'] as String?; final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? ''); - final fromTag = _normalizedFromNetwork.isEmpty || _normalizedFromNetwork == fromCurrency.toUpperCase() || _normalizedFromNetwork == 'Mainnet' - ? null : _normalizedFromNetwork; + final fromTag = _normalizedFromNetwork.isEmpty || + _normalizedFromNetwork == fromCurrency.toUpperCase() || + _normalizedFromNetwork == 'Mainnet' + ? null + : _normalizedFromNetwork; final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag); final toCurrency = responseJSON['ticker_to'] as String; final networkTo = responseJSON['network_to'] as String?; final _normalizedToNetwork = _normalizeNetworkType(networkTo ?? ''); - final toTag = _normalizedToNetwork.isEmpty || _normalizedToNetwork == toCurrency.toUpperCase() || _normalizedFromNetwork == 'Mainnet' ? null : _normalizedToNetwork; + final toTag = _normalizedToNetwork.isEmpty || + _normalizedToNetwork == toCurrency.toUpperCase() || + _normalizedFromNetwork == 'Mainnet' + ? null + : _normalizedToNetwork; final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag); return Trade( @@ -448,7 +450,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { providerId: providerId, providerName: providerName, extraId: addressProviderMemo, - userCurrencyFromRaw: '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}', + userCurrencyFromRaw: + '${fromCurrency.toUpperCase()}' + '_' + '${fromTag?.toUpperCase() ?? ''}', userCurrencyToRaw: '${toCurrency.toUpperCase()}' + '_' + '${toTag?.toUpperCase() ?? ''}', ); }); @@ -457,7 +460,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { Future> fetchProviders() async { final uri = await _getUri(providersListPath, {'api_key': apiKey}); final response = await ProxyWrapper().get(clearnetUri: uri); - if (response.statusCode != 200) throw Exception('Unexpected http status: ${response.statusCode}'); @@ -485,6 +487,8 @@ class TrocadorExchangeProvider extends ExchangeProvider { return 'MATIC'; case CryptoCurrency.zec: return 'Mainnet'; + case CryptoCurrency.arb: + return 'Mainnet'; default: return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet'; } @@ -502,6 +506,10 @@ class TrocadorExchangeProvider extends ExchangeProvider { } String _normalizeTag(String tag) { + if (tag.contains('ARB')) { + return 'Arbitrum'; + } + switch (tag) { case 'ETH': return 'ERC20'; @@ -527,8 +535,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { }; } - - Future _getUri(String path, Map queryParams) async { final uri = Uri.http(onionApiAuthority, path, queryParams); diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index 35e0d9184..d05ed4c09 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -44,6 +44,7 @@ class XOSwapExchangeProvider extends ExchangeProvider { 'EOS': 'eosio', 'XLM': 'stellar', 'BASE': 'basemainnet', + 'ARB': 'arbitrum', }; static const supportedTags = [ @@ -64,6 +65,7 @@ class XOSwapExchangeProvider extends ExchangeProvider { 'EOS', 'XLM', 'BASE', + 'ARB', ]; diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 68248ba65..9485ad4a9 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -41,12 +43,13 @@ class Trade extends HiveObject { this.routerValue, this.routerChainId, this.sourceTokenAmountRaw, - this.requiresTokenApproval + this.requiresTokenApproval, + this.chainId, }) { if (provider != null) providerRaw = provider.raw; fromRaw = from?.raw ?? -1; - toRaw = to?.raw ?? -1; + toRaw = to?.raw ?? -1; if (state != null) stateRaw = state.raw; } @@ -128,7 +131,9 @@ class Trade extends HiveObject { bool? isRefund; @HiveField(21) - bool? isSendAll; /// Must be set on createTrade; + bool? isSendAll; + + /// Must be set on createTrade; @HiveField(22) String? router; @@ -167,13 +172,18 @@ class Trade extends HiveObject { @HiveField(33, defaultValue: false) bool? requiresTokenApproval; + @HiveField(34) + int? chainId; + CryptoCurrency? get userCurrencyFrom { if (userCurrencyFromRaw == null || userCurrencyFromRaw!.isEmpty) { return null; } final underscoreIndex = userCurrencyFromRaw!.indexOf('_'); final title = userCurrencyFromRaw!.substring(0, underscoreIndex); - final tag = userCurrencyFromRaw!.substring(underscoreIndex + 1); + String tag = userCurrencyFromRaw!.substring(underscoreIndex + 1); + + if (tag.contains('ARB')) tag = 'ARB'; return CryptoCurrency( title: title, @@ -201,6 +211,12 @@ class Trade extends HiveObject { ); } + String get chainName { + if (chainId == null) return ''; + + return evm!.getChainNameByChainId(chainId!).capitalized(); + } + static Trade fromMap(Map map) { return Trade( id: map['id'] as String, @@ -219,6 +235,7 @@ class Trade extends HiveObject { isSendAll: map['isSendAll'] as bool?, router: map['router'] as String?, extraId: map['extra_id'] as String?, + chainId: map['chain_id'] as int?, ); } @@ -239,6 +256,7 @@ class Trade extends HiveObject { 'isSendAll': isSendAll, 'router': router, 'extra_id': extraId, + 'chain_id': chainId, }; } @@ -291,6 +309,7 @@ class TradeAdapter extends TypeAdapter { routerChainId: fields[31] as int?, sourceTokenAmountRaw: fields[32] as String?, requiresTokenApproval: fields[33] as bool?, + chainId: fields[34] as int?, ) ..providerRaw = fields[1] == null ? 0 : fields[1] as int ..fromRaw = (fields[2] as int?) ?? -1 @@ -369,7 +388,9 @@ class TradeAdapter extends TypeAdapter { ..writeByte(32) ..write(obj.sourceTokenAmountRaw) ..writeByte(33) - ..write(obj.requiresTokenApproval); + ..write(obj.requiresTokenApproval) + ..writeByte(34) + ..write(obj.chainId); } @override diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart deleted file mode 100644 index 94396f83c..000000000 --- a/lib/polygon/cw_polygon.dart +++ /dev/null @@ -1,263 +0,0 @@ -part of 'polygon.dart'; - -class CWPolygon extends Polygon { - @override - List getPolygonWordList(String language) => EVMChainMnemonics.englishWordlist; - - WalletService createPolygonWalletService(bool isDirect) => - PolygonWalletService(isDirect, client: PolygonClient()); - - @override - WalletCredentials createPolygonNewWalletCredentials({ - required String name, - String? mnemonic, - WalletInfo? walletInfo, - String? password, - String? passphrase, - }) => - EVMChainNewWalletCredentials( - name: name, - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - passphrase: passphrase, - ); - - @override - WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ - required String name, - required String mnemonic, - required String password, - String? 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); - - @override - WalletCredentials createPolygonHardwareWalletCredentials({ - required String name, - required HardwareAccountData hwAccountData, - WalletInfo? walletInfo, - }) => - EVMChainRestoreWalletFromHardware( - name: name, - hwAccountData: hwAccountData, - walletInfo: walletInfo, - ); - - @override - String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; - - @override - String getPrivateKey(WalletBase wallet) { - final privateKeyHolder = (wallet as PolygonWallet).evmChainPrivateKey; - if (privateKeyHolder is EthPrivateKey) return bytesToHex(privateKeyHolder.privateKey); - return ""; - } - - @override - String getPublicKey(WalletBase wallet) { - final privateKeyInUnitInt = (wallet as PolygonWallet).evmChainPrivateKey; - return privateKeyInUnitInt.address.hex; - } - - @override - TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; - - @override - TransactionPriority getPolygonTransactionPrioritySlow() => EVMChainTransactionPriority.slow; - - @override - List getTransactionPriorities() => EVMChainTransactionPriority.all; - - @override - TransactionPriority deserializePolygonTransactionPriority(int raw) => - EVMChainTransactionPriority.deserialize(raw: raw); - - Object createPolygonTransactionCredentials( - List outputs, { - 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, - ); - - Object createPolygonTransactionCredentialsRaw( - List outputs, { - TransactionPriority? priority, - required CryptoCurrency currency, - required int feeRate, - }) => - EVMChainTransactionCredentials( - outputs, - priority: priority as EVMChainTransactionPriority?, - currency: currency, - feeRate: feeRate, - ); - - @override - int formatterPolygonParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); - - @override - double formatterPolygonAmountToDouble({ - 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 PolygonWallet).erc20Currencies; - - @override - Future addErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as PolygonWallet).addErc20Token(token as Erc20Token); - - @override - Future deleteErc20Token(WalletBase wallet, CryptoCurrency token) => - (wallet as PolygonWallet).deleteErc20Token(token as Erc20Token); - - @override - Future removeTokenTransactionsInHistory(WalletBase wallet, CryptoCurrency token) => - (wallet as PolygonWallet).removeTokenTransactionsInHistory(token as Erc20Token); - - @override - Future getErc20Token(WalletBase wallet, String contractAddress) => - (wallet as PolygonWallet).getErc20Token(contractAddress, 'polygon'); - - @override - CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as EVMChainTransactionInfo; - if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title || - transaction.tokenSymbol == "MATIC") { - return CryptoCurrency.maticpoly; - } - - wallet as PolygonWallet; - - return wallet.erc20Currencies.firstWhere( - (element) => - transaction.contractAddress?.toLowerCase() == element.contractAddress?.toLowerCase(), - ); - } - - @override - void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) => - (wallet as PolygonWallet).updateScanProviderUsageState(isEnabled); - - @override - Web3Client? getWeb3Client(WalletBase wallet) => (wallet as PolygonWallet).getWeb3Client(); - - @override - String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress; - - 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); - - @override - Future createTokenApproval( - WalletBase wallet, - BigInt amount, - String spender, - CryptoCurrency token, - TransactionPriority priority, - ) => - (wallet as EVMChainWallet).createApprovalTransaction( - amount, - spender, - token, - priority as EVMChainTransactionPriority, - "POL", - ); - - @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); - } else if (service is EVMChainTrezorService) { - ((wallet as EVMChainWallet).evmChainPrivateKey as EvmTrezorCredentials).setTrezorConnect( - service.connect, (await wallet.walletInfo.getDerivationInfo()).derivationPath); - } - } - - @override - HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection) => - EVMChainLedgerService(connection); - - @override - HardwareWalletService getBitboxHardwareWalletService(bitbox.BitboxManager manager) => - EVMChainBitboxService(manager, chainId: 137); - - @override - HardwareWalletService getTrezorHardwareWalletService(trezor.TrezorConnect connect) => - EVMChainTrezorService(connect, chainId: 137); - - @override - List getDefaultTokenContractAddresses() => - DefaultPolygonErc20Tokens().initialPolygonErc20Tokens.map((e) => e.contractAddress).toList(); - - @override - bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) { - final polygonWallet = wallet as PolygonWallet; - return polygonWallet.erc20Currencies.any( - (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/check_connection.dart b/lib/reactions/check_connection.dart index e7e62c873..8cee07276 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/tor.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:cw_core/utils/print_verbose.dart'; @@ -7,6 +8,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/evm/evm.dart'; Timer? _checkConnectionTimer; @@ -34,14 +36,20 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore if (wallet.type != WalletType.bitcoin && (wallet.syncStatus is LostConnectionSyncStatus || wallet.syncStatus is FailedSyncStatus)) { - final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); + int? chainId; + if (isEVMCompatibleChain(wallet.type)) { + chainId = evm!.getSelectedChainId(wallet); + } + + final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId); + final alive = await node.requestNode(); if (alive) { if (settingsStore.currentBuiltinTor) { await ensureTorStarted(context: null); } - - await wallet.connectToNode(node: settingsStore.getCurrentNode(wallet.type)); + + await wallet.connectToNode(node: node); } } } catch (e) { diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index a821254c7..097590567 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -1,10 +1,8 @@ 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'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -36,23 +34,9 @@ Future startFiatRateUpdate( torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); Iterable? currencies; - if (appStore.wallet!.type == WalletType.ethereum) { + if (isEVMCompatibleChain(appStore.wallet!.type)) { currencies = - ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - - if (appStore.wallet!.type == WalletType.polygon) { - currencies = - polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - - if (appStore.wallet!.type == WalletType.base) { - currencies = - base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - if (appStore.wallet!.type == WalletType.arbitrum) { - currencies = - arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + evm!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } if (appStore.wallet!.type == WalletType.solana) { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 4b862d180..d37436755 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,12 +1,10 @@ -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'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; -import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/tor.dart'; @@ -67,7 +65,12 @@ void startCurrentWalletChangeReaction( await getIt.get().ensureGroupHasHashedIdentifier(wallet); - final node = settingsStore.getCurrentNode(wallet.type); + int? chainId; + if (isEVMCompatibleChain(wallet.type)) { + chainId = evm!.getSelectedChainId(wallet); + } + + final node = settingsStore.getCurrentNode(wallet.type, chainId: chainId); startWalletSyncStatusChangeReaction(wallet, settingsStore); startCheckConnectionReaction(wallet, settingsStore); @@ -91,7 +94,7 @@ void startCurrentWalletChangeReaction( if (settingsStore.currentBuiltinTor) { await ensureTorStarted(context: null); } - + await wallet.connectToNode(node: node); SyncingSyncStatus.blockHistory.clear(); if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) { @@ -124,21 +127,8 @@ void startCurrentWalletChangeReaction( torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); Iterable? currencies; - if (wallet.type == WalletType.ethereum) { - currencies = - ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - if (wallet.type == WalletType.polygon) { - currencies = - polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - if (wallet.type == WalletType.base) { - currencies = - base!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); - } - if (wallet.type == WalletType.arbitrum) { - currencies = - arbitrum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + if (isEVMCompatibleChain(wallet.type)) { + currencies = evm!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); } if (wallet.type == WalletType.solana) { currencies = diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart index 0357b12c8..a7929d346 100644 --- a/lib/reactions/wallet_connect.dart +++ b/lib/reactions/wallet_connect.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/et import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_chain_id.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cake_wallet/evm/evm.dart'; bool isEVMCompatibleChain(WalletType walletType) { switch (walletType) { @@ -16,13 +17,29 @@ bool isEVMCompatibleChain(WalletType walletType) { } } -bool isNFTACtivatedChain(WalletType walletType) { +// Blink Protection is supported on Ethereum and Base chains +//TODO: Add BNB Smart Chain to this list when we integrate it +bool canSupportBlinkProtection(int? chainId) { + if (chainId == null) return false; + + return chainId == 1 || chainId == 8453; +} + +bool isNFTACtivatedChain(WalletType walletType, int? chainId) { + if (chainId != null) { + switch (chainId) { + case 1: + case 8453: + case 137: + case 42161: + return true; + default: + return false; + } + } + switch (walletType) { - case WalletType.polygon: - case WalletType.ethereum: - case WalletType.base: case WalletType.solana: - case WalletType.arbitrum: return true; default: return false; @@ -31,10 +48,10 @@ bool isNFTACtivatedChain(WalletType walletType) { bool isWalletConnectCompatibleChain(WalletType walletType) { switch (walletType) { + case WalletType.solana: case WalletType.polygon: case WalletType.ethereum: case WalletType.base: - case WalletType.solana: case WalletType.arbitrum: return true; default: @@ -42,7 +59,11 @@ bool isWalletConnectCompatibleChain(WalletType walletType) { } } -String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) { +String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType, {int? chainId}) { + if (chainId != null) { + return evm!.getCaip2ByChainId(chainId); + } + switch (walletType) { case WalletType.ethereum: return EVMChainId.ethereum.chain(); @@ -73,51 +94,26 @@ List getChainSupportedMethodsOnWalletType(WalletType walletType) { } } -int getChainIdBasedOnWalletType(WalletType walletType) { - switch (walletType) { - case WalletType.polygon: - 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: - return 1; +String getChainNameBasedOnWalletType(WalletType walletType, {int? chainId}) { + if (walletType == WalletType.solana) { + return 'mainnet'; } + + if (chainId != null) { + return evm!.getChainNameByChainId(chainId); + } + + return evm!.getChainNameByWalletType(walletType); } -String getChainNameBasedOnWalletType(WalletType walletType) { - switch (walletType) { - case WalletType.ethereum: - return 'eth'; - case WalletType.polygon: - return 'polygon'; - case WalletType.base: - return 'base'; - case WalletType.arbitrum: - return 'arbitrum'; - case WalletType.solana: - return 'mainnet'; - default: - return ''; +String getTokenNameBasedOnWalletType(WalletType walletType, {int? chainId}) { + if (walletType == WalletType.solana) { + return 'SOL'; } -} -String getTokenNameBasedOnWalletType(WalletType walletType) { - switch (walletType) { - case WalletType.ethereum: - return 'ETH'; - case WalletType.polygon: - return 'MATIC'; - case WalletType.base: - return 'BASE'; - case WalletType.arbitrum: - return 'ARB'; - case WalletType.solana: - return 'SOL'; - default: - return ''; + if (chainId != null) { + return evm!.getTokenNameByChainId(chainId); } + + return evm!.getTokenNameByWalletType(walletType); } diff --git a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart index 6e88f4b2d..1f47cdb92 100644 --- a/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart +++ b/lib/src/screens/connect_device/select_hardware_wallet_account_page.dart @@ -149,7 +149,9 @@ class _SelectHardwareWalletAccountFormState extends State showDialog( + context: context, + builder: (context) => EvmSwitcher( + chains: dashboardViewModel.availableChains, + currentChain: dashboardViewModel.currentChain, + onChainSelected: (chainId) => dashboardViewModel.selectChain(chainId), + hiddenChainIds: dashboardViewModel.settingsStore.evmHiddenChainIds, + onHiddenChanged: (hidden) => + dashboardViewModel.settingsStore.setEvmHiddenChainIds(hidden), + ), + ), + child: Container( + child: SvgPicture.asset( + 'assets/images/evm_switcher.svg', + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 30, + ), + ), + ); + } + return Observer( builder: (context) { return ServicesUpdatesWidget( diff --git a/lib/src/screens/dashboard/pages/address_page.dart b/lib/src/screens/dashboard/pages/address_page.dart index 20b1b9157..358b13e8d 100644 --- a/lib/src/screens/dashboard/pages/address_page.dart +++ b/lib/src/screens/dashboard/pages/address_page.dart @@ -207,40 +207,45 @@ class AddressPage extends BasePage { ), ), SizedBox(height: 20), - Center( - child: SizedBox( - height: 40, - width: addressListViewModel.walletImages.length * 32.0, - child: Stack( - children: [ - for (int i = addressListViewModel.walletImages.length - 1; i >= 0; i--) - Positioned( - left: i * 25.0, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.surfaceContainer, - width: 3, - ), - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(24), - ), - child: ClipOval( - child: CakeImageWidget( - height: 35, - width: 35, - imageUrl: addressListViewModel.walletImages[i], - color: addressListViewModel.walletImages.last == - addressListViewModel.walletImages[i] - ? Theme.of(context).colorScheme.onSurfaceVariant - : null, + Observer( + builder: (_) { + final walletImages = addressListViewModel + .getWalletImages(addressListViewModel.selectedChainId); + return Center( + child: SizedBox( + height: 40, + width: walletImages.length * 32.0, + child: Stack( + children: [ + for (int i = walletImages.length - 1; i >= 0; i--) + Positioned( + left: i * 25.0, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + width: 3, + ), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(24), + ), + child: ClipOval( + child: CakeImageWidget( + height: 35, + width: 35, + imageUrl: walletImages[i], + color: walletImages.last == walletImages[i] + ? Theme.of(context).colorScheme.onSurfaceVariant + : null, + ), + ), ), ), - ), - ), - ], - ), - ), + ], + ), + ), + ); + }, ), ], ), diff --git a/lib/src/screens/dashboard/pages/balance/balance_page.dart b/lib/src/screens/dashboard/pages/balance/balance_page.dart index 88ddc55d5..8fd769cc5 100644 --- a/lib/src/screens/dashboard/pages/balance/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance/balance_page.dart @@ -22,7 +22,7 @@ class BalancePage extends StatelessWidget { Widget build(BuildContext context) { return Observer( builder: (context) { - final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type); + final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type, dashboardViewModel.wallet.chainId); return DefaultTabController( key: ValueKey(isNFTActivated), length: isNFTActivated ? 2 : 1, diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart index ac9a7966e..2b54ec144 100644 --- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart +++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart @@ -169,6 +169,21 @@ class CryptoBalanceWidget extends StatelessWidget { }), Observer( builder: (_) { + if (dashboardViewModel.balanceViewModel.formattedBalances.isEmpty) { + return Center( + child: Container( + child: Text( + 'Loading balances...', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + height: 1, + ), + textAlign: TextAlign.center, + ), + ), + ); + } + return ListView.separated( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, diff --git a/lib/src/screens/dashboard/pages/cake_features_page.dart b/lib/src/screens/dashboard/pages/cake_features_page.dart index bd381a968..2aad19743 100644 --- a/lib/src/screens/dashboard/pages/cake_features_page.dart +++ b/lib/src/screens/dashboard/pages/cake_features_page.dart @@ -59,7 +59,7 @@ class CakeFeaturesPage extends StatelessWidget { ), ), Observer(builder: (_) { - if (dashboardViewModel.type == WalletType.ethereum) { + if (dashboardViewModel.wallet.chainId == 1) { return DashBoardRoundedCardWidget( shadowBlur: dashboardViewModel.getShadowBlur(), shadowSpread: dashboardViewModel.getShadowSpread(), diff --git a/lib/src/screens/dashboard/sign_page.dart b/lib/src/screens/dashboard/sign_page.dart index e2889f1a5..83ce7b9dd 100644 --- a/lib/src/screens/dashboard/sign_page.dart +++ b/lib/src/screens/dashboard/sign_page.dart @@ -24,11 +24,13 @@ class SignPage extends BasePage { _pages.add(SignForm( key: signFormKey, type: signViewModel.wallet.type, + chainId: signViewModel.wallet.chainId, includeAddress: signViewModel.signIncludesAddress, )); _pages.add(VerifyForm( key: verifyFormKey, type: signViewModel.wallet.type, + chainId: signViewModel.wallet.chainId, )); } diff --git a/lib/src/screens/dashboard/widgets/sign_form.dart b/lib/src/screens/dashboard/widgets/sign_form.dart index 7f9f1b8d9..8556f2085 100644 --- a/lib/src/screens/dashboard/widgets/sign_form.dart +++ b/lib/src/screens/dashboard/widgets/sign_form.dart @@ -11,12 +11,14 @@ class SignForm extends StatefulWidget { SignForm({ Key? key, required this.type, + required this.chainId, required this.includeAddress, }) : super(key: key); final WalletType type; final bool includeAddress; - + final int? chainId; + @override SignFormState createState() => SignFormState(); } @@ -76,7 +78,7 @@ class SignFormState extends State with AutomaticKeepAliveClientMixin { onSelectedContact: (contact) { addressController.text = contact.address; }, - selectedCurrency: walletTypeToCryptoCurrency(widget.type), + selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), fillColor: Theme.of(context).colorScheme.surface, ), ], diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index e50cb9718..7d163076e 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -33,39 +33,40 @@ class TradeRow extends StatelessWidget { final receiveAmountCrypto = to.toString(); return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.fromLTRB(24, 8, 24, 8), - color: Colors.transparent, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(50), - child: ImageUtil.getImageFromPath( - imagePath: provider.image, height: 36, width: 36),), - Positioned( - right: 0, - bottom: 2, - child: Container( - height: 8, - width: 8, - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: _statusColor(context, swapState), - borderRadius: BorderRadius.circular(12), - ), + onTap: onTap, + child: Container( + padding: EdgeInsets.fromLTRB(24, 8, 24, 8), + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(50), + child: + ImageUtil.getImageFromPath(imagePath: provider.image, height: 36, width: 36), + ), + Positioned( + right: 0, + bottom: 2, + child: Container( + height: 8, + width: 8, + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _statusColor(context, swapState), + borderRadius: BorderRadius.circular(12), ), ), - ], - ), - SizedBox(width: 12), - Expanded( - child: Column( + ), + ], + ), + SizedBox(width: 12), + Expanded( + child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( diff --git a/lib/src/screens/dashboard/widgets/verify_form.dart b/lib/src/screens/dashboard/widgets/verify_form.dart index 318aca9b0..66d8978a4 100644 --- a/lib/src/screens/dashboard/widgets/verify_form.dart +++ b/lib/src/screens/dashboard/widgets/verify_form.dart @@ -8,10 +8,12 @@ class VerifyForm extends StatefulWidget { VerifyForm({ Key? key, required this.type, + required this.chainId, }) : super(key: key); final WalletType type; - + final int? chainId; + @override VerifyFormState createState() => VerifyFormState(); } @@ -66,7 +68,7 @@ class VerifyFormState extends State with AutomaticKeepAliveClientMix onSelectedContact: (contact) { addressController.text = contact.address; }, - selectedCurrency: walletTypeToCryptoCurrency(widget.type), + selectedCurrency: walletTypeToCryptoCurrency(widget.type, chainId: widget.chainId), ), const SizedBox(height: 20), AddressTextField( diff --git a/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart index ba60bdb73..03e9496fe 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_external_send_page.dart @@ -72,7 +72,7 @@ class ExchangeTradeExternalSendPage extends BasePage { context, Routes.fullscreenQR, arguments: QrViewData( - embeddedImagePath: exchangeTradeViewModel.qrImage, + embeddedImagePath: exchangeTradeViewModel.trade.from?.iconPath, data: exchangeTradeViewModel.paymentUri?.toString() ?? exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel, @@ -96,7 +96,7 @@ class ExchangeTradeExternalSendPage extends BasePage { data: exchangeTradeViewModel.paymentUri?.toString() ?? exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel, - embeddedImagePath: exchangeTradeViewModel.qrImage, + embeddedImagePath: exchangeTradeViewModel.trade.from?.iconPath, size: 230, ), ), diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 44840dbed..7f2c65876 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -257,6 +257,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.privacySettingsViewModel.setDisableBulletin(value); }, ), + if (widget.privacySettingsViewModel.canUseBlinkProtection) + SettingsSwitcherCell( + title: S.current.use_blink_protection, + value: widget.privacySettingsViewModel.useBlinkProtection, + onValueChange: (BuildContext _, bool value) { + widget.privacySettingsViewModel.setUseBlinkProtection(value); + }, + ), SettingsSwitcherCell( title: S.current.add_custom_node, value: widget.privacySettingsViewModel.addCustomNode, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 584a7cc3e..aa9e27c34 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -143,7 +143,7 @@ class WalletTypeFormState extends State { child: SelectButton( key: ValueKey('new_wallet_type_${type.name}_button_key'), image: Image.asset( - walletTypeToCryptoCurrency(type).iconPath ?? '', + getCryptoCurrencyForWalletListItem(type).iconPath ?? '', height: 24, width: 24, ), diff --git a/lib/src/screens/new_wallet/wallet_group_display_page.dart b/lib/src/screens/new_wallet/wallet_group_display_page.dart index bddb8f493..da9a325f3 100644 --- a/lib/src/screens/new_wallet/wallet_group_display_page.dart +++ b/lib/src/screens/new_wallet/wallet_group_display_page.dart @@ -95,7 +95,7 @@ class WalletGroupsDisplayBody extends StatelessWidget { isSelected: walletGroupsDisplayViewModel.selectedSingleWallet == wallet, leadingWidget: Image.asset( - walletTypeToCryptoCurrency(wallet.type).iconPath!, + getCryptoCurrencyForWalletListItem(wallet.type).iconPath!, width: 32, height: 32, ), diff --git a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart index 413739876..f77a4e60e 100644 --- a/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart +++ b/lib/src/screens/new_wallet/widgets/grouped_wallet_expansion_tile.dart @@ -123,7 +123,7 @@ class GroupedWalletExpansionTile extends StatelessWidget { : SizedBox(width: 7), SizedBox(width: 24), Image.asset( - walletTypeToCryptoCurrency(item.type).iconPath!, + getCryptoCurrencyForWalletListItem(item.type).iconPath!, width: 32, height: 32, ), diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 1c97dbda2..3cc1d50f3 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -37,6 +37,9 @@ import 'package:cake_wallet/src/widgets/address_text_field.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; +import 'package:cake_wallet/store/app_store.dart'; class SendCard extends StatefulWidget { SendCard({ @@ -176,6 +179,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin _handleSwapFlow(paymentViewModel, result), + onSwap: (bottomSheetContext) => + _handleSwapFlow(paymentViewModel, result, bottomSheetContext), + onSwitchNetwork: () => _handleSwitchNetwork( + paymentViewModel, + walletSwitcherViewModel, + paymentRequest, + result, + ), ); }, ); @@ -245,10 +256,14 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin(); + final node = appStore.settingsStore.getCurrentNode( + sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node); + } + await sendViewModel.wallet.updateBalance(); - sendViewModel - .setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title); + + final detectedCurrency = result.addressDetectionResult!.detectedCurrency; + if (detectedCurrency != null) { + sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title); + } + _applyPaymentRequest(paymentRequest); } } @@ -303,7 +331,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin(); + final node = appStore.settingsStore.getCurrentNode( + sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node); + } + await Future.delayed(const Duration(seconds: 2)); if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { Navigator.of(loadingBottomSheetContext!).pop(); @@ -331,6 +370,94 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: S.of(context).loading_your_wallet, + ); + }, + ); + } + }); + + // If EVM wallet and chainId is specified, switch to that chain + if (isEVMCompatibleChain(sendViewModel.wallet.type) && result.chainId != null) { + final appStore = getIt.get(); + final node = appStore.settingsStore.getCurrentNode( + sendViewModel.wallet.type, + chainId: result.chainId, + ); + await evm!.selectChain(sendViewModel.wallet, result.chainId!, node: node); + } + + await Future.delayed(const Duration(seconds: 2)); + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + await sendViewModel.wallet.updateBalance(); + sendViewModel + .setSelectedCryptoCurrency(result.addressDetectionResult!.detectedCurrency!.title); + _applyPaymentRequest(paymentRequest); + } + } + } + + Future _handleSwitchNetwork( + PaymentViewModel paymentViewModel, + WalletSwitcherViewModel walletSwitcherViewModel, + PaymentRequest paymentRequest, + PaymentFlowResult result, + ) async { + if (result.type != PaymentFlowType.evmNetworkSelection || result.wallet == null) return; + + if (context.mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + try { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: S.of(context).loading_your_wallet, + ); + }, + ); + } + }); + + await paymentViewModel.selectChain(); + + await Future.delayed(const Duration(seconds: 2)); + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + await sendViewModel.wallet.updateBalance(); + final detectedCurrency = result.addressDetectionResult?.detectedCurrency; + if (detectedCurrency != null) { + sendViewModel.setSelectedCryptoCurrency(detectedCurrency.title); + } + _applyPaymentRequest(paymentRequest); + } catch (e) { + if (loadingBottomSheetContext != null && loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + printV('Switch network error: $e'); } } @@ -346,10 +473,17 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin _handleSwapFlow(PaymentViewModel paymentViewModel, PaymentFlowResult result) async { - if (mounted && Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } + Future _handleSwapFlow( + PaymentViewModel paymentViewModel, + PaymentFlowResult result, + BuildContext bottomSheetContext, + ) async { + Navigator.of(bottomSheetContext).pop(); + + await Future.delayed(const Duration(milliseconds: 100)); + + if (!mounted) return; + final bottomSheet = getIt.get(param1: result); await showModalBottomSheet( context: context, @@ -549,31 +683,21 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin