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