CW-1250: Refactor L2 Chains Code (#2677)

* feat(evm): add ChainConfig model and related classes

- Add ChainConfig class for immutable EVM chain configuration
- Add ChainCapabilities class for chain feature flags
- Add FeeType enum and FeeModel class for fee configuration

* feat(evm): add EvmChainRegistry for centralized chain management

- Add singleton EvmChainRegistry class with chain configuration storage
- Initialize with Ethereum, Polygon, Base, and Arbitrum chains
- Add mappings for WalletType, tag, and CAIP-2 to chainId lookups
- Provide lookup methods for chain configurations by various identifiers
- Support for querying available chains and registered chain IDs

* feat(evm): add EVMChainClientFactory and consolidate chain clients

- Create EVMChainClientFactory for creating chain-specific clients based on chainId
- Move all chain clients (EthereumClient, PolygonClient, BaseClient, ArbitrumClient) into cw_evm/lib/clients/ folder
- Refactor EVMChainClient from abstract to concrete class with default implementations
- Add default implementations for fetchTransactions, fetchInternalTransactions, and prepareSignedTransactionForSending
- Update all client classes to use super(chainId: X) constructor pattern
- Factory returns EVMChainClient directly for unregistered chains instead of throwing
- Remove dependencies on cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages from pubspec.yaml
- Consolidate all EVM chain client implementations in one location for easier maintenance

* add ui for evm switcher on eth

* add inputs for evm network list edit switches

* refactor(evm): Make EVMChainWallet concrete and unify transaction classes
- Make EVMChainWallet and EVMChainWalletBase concrete classes (removed abstract)
- Implement all previously abstract methods in EVMChainWallet
- Make EVMChainTransactionInfo and EVMChainTransactionHistory concrete
- Reorganize default tokens into tokens/ folder:
- Refactor EVMChainDefaultTokens to import and use token classes from tokens/ folder
- Move clients to clients/ folder:
- Move evm_chain_formatter.dart to utils
- Update chain-specific transaction history classes to pass walletType to fromJson
- Update old wallet classes (EthereumWallet, PolygonWallet, etc.) to pass walletType
- Maintain compatibility with existing per-chain wallet classes

This refactoring enables the unified EVMChainWallet architecture while maintaining
full backward compatibility with existing per-chain wallet implementations.

* feat(evm): Add chain selection capability to EVMChainWallet
- Add selectedChainId observable field to track currently selected chain
- Add selectChain(chainId) action method to switch between EVM chains
- Add selectedChainConfig computed getter for accessing chain configuration
- Initialize selectedChainId from client.chainId in constructor
- Add _getClientForCurrentChain() helper method for future use

This enables chain switching functionality while maintaining full backward compatibility. The selectedChainId is initialized from the existing client, and all existing methods continue to work unchanged. Future increments will update methods to use selectedChainId internally.

* refactor(evm): Update client immediately on chain selection

- Update selectChain() to immediately create new client for selected chain
- Remove _getClientForCurrentChain() helper method
- Simplify all methods to use _client directly instead of helper calls
- Client is now always in sync with selectedChainId

This improves code simplicity and performance by eliminating repeated
client checks. The client is updated synchronously when selectChain()
is called, ensuring state consistency.

* featr(evm): Add unified evm proxy setup and fix chain initialization

- Add unified Evm proxy
- Add evm.dart to .gitignore for generated proxy
- Add generateEVM() to configure.dart for proxy generation
- Fix selectedChainId to initialize from registry based on walletInfo.type

Ensures selectedChainId always matches wallet type on initialization
and adds unified proxy infrastructure.

* refactor(evm): Update backward compat helpers to use registry via proxy

- Add registry helper methods to unified EVM proxy
- Update all lib/ files to use proxy instead of direct cw_evm imports
- Enforce proxy pattern: no direct imports from cw_evm in main app
- Update configure.dart to include registry methods in generated proxy

Maintains backward compatibility while centralizing chain data access
through the unified proxy pattern.

* feat(evm): Add chain selection UI to dashboard

- Add chain selection methods to DashboardViewModel (isEVMWallet,
  availableChains, currentChain, selectChain)
- Add chain dropdown widget to balance page for EVM wallets
- Dropdown shows current chain and allows switching between all
  registered EVM chains
- Only visible for EVM-compatible wallets
- Add chain selection methods to EVM proxy (getAllChains,
  getCurrentChain, selectChain)

Users can now switch between EVM chains (Ethereum, Polygon, Base,
Arbitrum) directly from the dashboard without creating separate wallets. All chain-related code follows the established proxy pattern.

* feat(evm): Add persistence for selected chain ID

- Add initialChainId parameter to EVMChainWallet constructor
- Save selectedChainId to wallet JSON in toJSON() method
- Load selectedChainId from JSON in open() method
- Handle backward compatibility: wallets without saved chain ID use
  wallet type's default chain ID
- Client is created with saved chain ID when opening wallet

Selected chain preference is now persisted across app sessions. Users will see their previously selected chain when reopening wallets.

* chore: Minor cleanup

* refactor(evm): Remove old per-chain packages, consolidate to unified cw_evm

- Delete cw_ethereum, cw_polygon, cw_base, cw_arbitrum packages
- Remove old proxy files (lib/ethereum/, lib/polygon/, lib/base/, lib/arbitrum/)
- Update all view models to use unified evm! proxy
- Move DEuro functionality to cw_evm/lib/deuro/
- Remove dependencies from pubspec.yaml and configure.dart
- Remove walletType parameter from EVM proxy methods

All EVM chains now use unified cw_evm package through single proxy interface. lib/ only depends on cw_evm, simplifying architecture and enabling easy addition of new L2 chains.

BREAKING CHANGE: Old per-chain packages removed

* refactor(evm): Update wallet handling to use unified EVM proxy

- Replace individual chain imports (Ethereum, Polygon, Base, Arbitrum) with a single EVM proxy import.
- Refactor wallet-related methods across various view models to utilize the new evm proxy for various usecases.

This change enhances code maintainability and prepares the codebase for the addition of new EVM chains.

* refactor(evm): Update model generator and enhance EVM classes

- Modify model_generator.sh to remove previous chain folders from the loop.
- Change EVMChainTransactionHistoryBase and EVMChainWalletBase classes to be abstract.
- Update import paths in configure.dart to reflect the new structure of the cw_evm package.

* add evm switcher icons to pubspec_base

* change evm switcher to sliding animation

* formatting fix

* dpi agnostic width

* feat(evm): Enhance chain handling and transaction history management

- Introduce getCurrentChainId function to EVMChainTransactionHistoryBase for dynamic transaction history file naming based on the current chain ID.
- Update EVMChainTransactionInfo to include chainId, ensuring accurate transaction data representation.
- Refactor EVMChainWalletBase to support automatic node connection and transaction history loading upon chain selection.
- Implement saveBackup and restoreWalletFilesFromBackup methods in EVMChainWalletService to manage wallet backups based on wallet type.
- Update various methods across the codebase to accommodate the new chain handling logic, improving overall functionality and user experience.

* chore: Cleanups and updates

* feat: add WalletType.evm and deprecate individual EVM chain types

Add unified WalletType.evm enum value for all EVM-compatible chains.
Deprecate old individual EVM types (ethereum, polygon, base, arbitrum)
with guidance to use WalletType.evm for new code.

Update serialization/deserialization functions to support WalletType.evm:
- serializeToInt() returns 18 for WalletType.evm
- deserializeFromInt() handles case 18
- walletTypeToString() and walletTypeToDisplayName() return 'EVM'
- cryptoCurrencyToWalletType() maps all EVM currencies to WalletType.evm

Old EVM types remain functional for backward compatibility with existing
wallets. This is the foundation for unifying EVM chain management.

* feat: update EVM detection functions to support WalletType.evm

Update all EVM detection and chain helper functions to support the unified WalletType.evm while maintaining backward compatibility with old EVM types.

All functions require chainId parameter when used with WalletType.evm, ensuring proper chain-specific behavior. Old EVM types continue to work without changes for backward compatibility.

* feat: add currency mapping support for unified EVM wallet type

- Update currency mapping to support WalletType.evm with chainId-based currency resolution.
- Override EVMChainWallet.currency getter to use selectedChainId from registry.
- Replace walletTypeToCryptoCurrency calls with wallet.currency in view models.
- Add WalletType.evm cases to switch statements and consolidate EVM transaction list item methods.
- Add explorer URL method to EVM interface

* feat: Update wallet operations with unified WalletType.evm

- Update wallet service to always create new wallets and restores with WalletType.evm, defaulting to Ethereum.
- Preserve old wallet types when opening existing wallets. Update wallet instance creation and opening logic to handle unified EVM type with chainId-based config lookup.

* Add WalletType.evm support across all switch statements
- Update all switch statements and conditionals to include WalletType.evm alongside existing EVM types.
- Add chainId-based node selection support for unified EVM wallets. - Fix redundant code in exchange view model.
- Ensure backward compatibility with old EVM wallet types.

* feat(evm): Implement chainId-based node management for WalletType.evm

- Add chainId-based node storage for unified EVM wallets, enabling separate node preferences per chain.
- Replace unsafe dynamic casts with type-safe getSelectedChainId method. Update all node retrieval and switching logic to support chainId-based selection.

* feat: Update UI components to show unified EVM wallet option

- Filter out old EVM wallet types (ethereum, polygon, base, arbitrum) from wallet creation and restore UI, showing only unified WalletType.evm option.
- Update wallet type generation to include WalletType.evm instead of individual
chain types.
- Update configure.dart to generate WalletType.evm in availableWalletTypes
- Add filtering checks to prevent old EVM types from appearing

* fix: Fix WalletType.evm support and complete remaining edge cases

Add missing WalletType.evm cases to switch statements to resolve compilation
errors. Complete remaining unification items:

- Add WalletType.evm support to node, wallet utils, and view models
- Update EVMChainDefaultTokens and EVMChainUtils to support chainId
- Fix token initialization and priority fee calculations for WalletType.evm
- Update payment view model to handle WalletType.evm with chainId

All identified edge cases from the unification plan are now complete.

* fix: Fix network switching issues for EVM wallets
- Fix null check errors when creating and restoring an EVM wallet
- Fix null check errors for balance getter when switching networks
- Fix fiat amount display for native currencies in transaction history

* refactor: Simplify EVM wallet chain selection in CryptoBalanceWidget

- Removed the old dropdown for chain selection and replaced it with a more integrated EvmSwitcher dialog.
- Updated EvmSwitcher to accept a list of available chains and handle chain selection more efficiently.
- Enhanced EvmSwitcherDataItem to include chainId for better identification of chains.
- Improved the overall UI flow for EVM wallet interactions.

* refactor: Enhance EVM wallet handling and chainId integration

- Simplified chainId checks across various components to ensure consistent handling for WalletType.evm.
- Updated EVMChainDefaultTokens and EVMChainUtils checks to allow all EVM wallets for chainId operations
- Handled backward compatibility for older wallet types in transaction info

* refactor: More enhancement and backward compatibility fixes for EVM Wallet

- Introduced a new method to retrieve cryptocurrency by chainId, improving the handling of WalletType.evm.
- Updated transaction history management to filter transactions based on the current chainId
- Refactored EVMChainTransactionInfo to accept chainId directly
- Modified utility functions in EVMChainUtils and TokenUtilities for better chainId handling

* refactor: Streamline EVM token retrieval and enhance chainId handling

* feat: Introduce comprehensive guide for adding new EVM L2 networks

- Added a detailed documentation file outlining the steps to integrate new EVM-compatible L2 networks into Cake Wallet.
- Included prerequisites, architecture overview, and a step-by-step guide covering chain configuration, native currency addition, default tokens, and node setup.
- Emphasized the unified architecture for EVM chains and backward compatibility considerations.
- Removed the outdated guide on adding new L2 networks to streamline documentation.

* feat: Reposition EVM switcher button on dashboard page

* just adding reminders for the refactoring [skip ci]

* more todos [skip ci]

* feat: enhance wallet functionality with chainId support

- Added chainId to `WalletBase` and updated currency method to use it.
- Modified EvmChainRegistry to initialize registry on call.
- Updated various components to pass chainId where necessary for currency conversions.
- Improved handling of chain IDs in wallet-related view models and UI components for better compatibility with EVM networks.

* fix: remove default chainId fallback in payment confirmation widget

* feat: Adapt anypay to new unified flow WIP

* fix: Issues with handling non evm swaps for anypay

* fixes to flow structure

* Add persistence to hidden chains on switcher

* refactor: simplify equality operator in Erc20Token class

* feat: add excluded arbutrum tokens for chainflip

* chore: Remove print statement [skip ci]

* refactor: dispaly QR image display for evm based on selected chain ID

* feat: enhance EVM wallet compatibility check for raw address input

* chore: Update doc to reflect changes [skip ci]

* feat: Integrate Blink for EVM wallets for anti-sandwich attacks.

- Add toggle in privacy settings to enable/disable
- When enabled, route all evm broadcast rpc calls via Blink

* feat: Integrate Blink for EVM wallets for anti-sandwich attacks.

- Add toggle in privacy settings to enable/disable
- When enabled, route all evm broadcast rpc calls via Blink

* feat: Integrate Blink for EVM wallets for anti-sandwich attacks.

- Add toggle in privacy settings to enable/disable
- When enabled, route all evm broadcast rpc calls via Blink

* feat: Handle same chainId scenario for pasted evm addresses

* feat: Add Blink Setting to Advanced Settings Page

* fix: Handle Blink supported chains

* fix: Fixes to evm swap

* fix: Add fallback for currency fetch

* fix: Add evm to supported list

* fix: Better handle balance errors

* fix: Update token check to use contract address comparison, resolves the usdt/usdt0 issue

* refactor: Simplify checks by cearing all tokens on chain switch, removing unnecessary currency checks

* fix: Simplify checks based from feedback on review

* fix: Exclude arbitrum from tx priority using chainId

* refactor: Handle chains not having transaction priority within refactor

* fix: Unified evm wallet fixes
- Handle switching networks while on tx history page
- Update NFTs when network is switching on listing page
- Fix arbitrum tx priority error on swap screen
- Switch check to display deuro from wallet type to chainId
- Add fallback check for getting token info for evm
- Better error message for max fee per gas less than block base fee error

* refactor: Update wallet checks from type to chainId for EVM compatibility checks

* refactor: Update eth chain utils and remove verified contract criteria in scam detection checks

* refactor: Enhance validation for deuro calls

* fix : Remove unneeded validation check for savings edit

* feat: Hide EVM chain switcher behind feature flag

* refactor: Remove EVM wallet type references and update related logic

* chore: Clean up wallet type references and remove repeated imports

* feat: Add fallback for derivation path and add arbitrum support to app scripts

* fix: NFT popup error

* fix: Transaction details fee regression

* fix: Fixes and improvements
- Add nonce parameter to transaction methods in EVM clients
- Remove brackets for create swaps
- Display contacts for selected wallettype alone
- Remove tx priorty switching for arb

* feat: Enhance Arbitrum support across swap providers

* fix: Add missing secret keys

* fix: display sending currency icon in send from external page

* refactor: Handle arbitrum edgecase for wallet list display image

* fix: Handle more cases of arbitrum image display for contact book

* exploring possible issues with tx history

* chore: switch to printV

* fix: Add an extra check for keys

* fix: Filter out eth spam transaction and modify parsing format

* fix: Handle contract decimals for sending tokens

* fix: Display proper balance error

* Update cw_evm/lib/utils/evm_chain_formatter.dart [skip ci]

---------

Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2026-01-16 02:45:49 +01:00
committed by GitHub
co-authored by Robert Malikowski Omar Hatem
parent 0e4855132c
commit 2a77d359f1
251 changed files with 7333 additions and 7788 deletions
+46
View File
@@ -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;
}
+46
View File
@@ -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;
}
+13
View File
@@ -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);
}
@@ -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<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress});
{String? contractAddress}) async {
try {
if (secrets.etherScanApiKey.isEmpty) {
printV('Etherscan API key is empty, cannot fetch transactions');
return [];
}
Future<List<EVMChainTransactionModel>> 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<String, dynamic>;
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<String, dynamic>, symbol, chainId),
)
.toList();
}
return [];
} catch (e) {
log(e.toString());
return [];
}
}
Future<List<EVMChainTransactionModel>> 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<String, dynamic>;
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<String, dynamic>, 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<EtherAmount> getBalance(EthereumAddress address, {bool throwOnError = false}) async {
Future<EtherAmount> 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<String> sendTransaction(Uint8List signedTransaction) async {
return await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
String _blinkUrl(String apiKey) => 'https://eth.blinklabs.xyz/v1/$apiKey';
Future<String> 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<Erc20Token?> 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<String, dynamic>;
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<Erc20Token?> 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<String, dynamic>;
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<Erc20Token?> 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));
+46
View File
@@ -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;
}
+203
View File
@@ -0,0 +1,203 @@
import 'package:cw_core/crypto_currency.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';
import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
const String savingsGatewayAddress = "0x073493d73258C4BEb6542e8dd3e1b2891C972303";
const String dEuroAddress = "0xbA3f535bbCcCcA2A154b573Ca6c5A49BAAE0a3ea";
const String frontendCode = "0x00000000000000000000000000000000000000000043616b652057616c6c6574";
class DEuro {
final SavingsGateway _savingsGateway;
final ERC20 _dEuro;
final EVMChainWallet _wallet;
DEuro(EVMChainWallet wallet)
: _wallet = wallet,
_savingsGateway = _getSavingsGateway(wallet.getWeb3Client()!),
_dEuro = _getDEuroToken(wallet.getWeb3Client()!);
static SavingsGateway _getSavingsGateway(Web3Client client) => SavingsGateway(
address: EthereumAddress.fromHex(savingsGatewayAddress),
client: client,
);
static ERC20 _getDEuroToken(Web3Client client) => ERC20(
address: EthereumAddress.fromHex(dEuroAddress),
client: client,
);
EthereumAddress get _address => EthereumAddress.fromHex(_wallet.walletAddresses.primaryAddress);
Future<BigInt> get savingsBalance async =>
(await _savingsGateway.savings(accountOwner: _address)).saved;
Future<BigInt> get accruedInterest => _savingsGateway.accruedInterest(accountOwner: _address);
Future<BigInt> get interestRate => _savingsGateway.currentRatePPM();
Future<BigInt> get approvedBalance => _dEuro.allowance(_address, _savingsGateway.self.address);
Future<void> _checkEthBalanceForGasFees(EVMChainTransactionPriority priority) async {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
final currentBalance = ethBalance.getInWei;
final gasFeesModel = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: BigInt.zero,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[17]
.encodeCall([BigInt.zero, hexToBytes(frontendCode)]),
);
final estimatedGasFee = BigInt.from(gasFeesModel.estimatedGasFee);
final requiredBalance = estimatedGasFee;
if (currentBalance < requiredBalance) {
throw InsufficientGasFeeException(
requiredGasFee: requiredBalance,
currentBalance: currentBalance,
);
}
}
Future<PendingEVMChainTransaction> depositSavings(
BigInt amount, EVMChainTransactionPriority priority) async {
try {
await _checkEthBalanceForGasFees(priority);
final signedTransaction = await _savingsGateway.save(
(amount: amount, frontendCode: hexToBytes(frontendCode)),
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: amount,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[17].encodeCall([amount, hexToBytes(frontendCode)]),
);
final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
return PendingEVMChainTransaction(
feeCurrency: "ETH",
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: BigInt.from(fee.estimatedGasFee),
amount: amount.toString(),
exponent: 18,
);
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei);
}
rethrow;
}
}
Future<PendingEVMChainTransaction> withdrawSavings(
BigInt amount, EVMChainTransactionPriority priority) async {
try {
await _checkEthBalanceForGasFees(priority);
final signedTransaction = await _savingsGateway.withdraw(
(target: _address, amount: amount, frontendCode: hexToBytes(frontendCode)),
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: amount,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[24].encodeCall([_address, amount, hexToBytes(frontendCode)]),
);
final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
return PendingEVMChainTransaction(
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: BigInt.from(fee.estimatedGasFee),
amount: amount.toString(),
feeCurrency: "ETH",
exponent: 18);
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei);
}
rethrow;
}
}
Future<PendingEVMChainTransaction> reinvestInterest(EVMChainTransactionPriority priority) async {
try {
await _checkEthBalanceForGasFees(priority);
final signedTransaction = await _savingsGateway.refreshBalance((owner: _address),
credentials: _wallet.evmChainPrivateKey,
);
final fee = await _wallet.calculateActualEstimatedFeeForCreateTransaction(
amount: BigInt.zero,
contractAddress: _savingsGateway.self.address.hexEip55,
receivingAddressHex: _savingsGateway.self.address.hexEip55,
priority: priority,
data: _savingsGateway.self.abi.functions[15].encodeCall([_address]),
);
final sendTransaction = () => _wallet.getWeb3Client()!.sendRawTransaction(signedTransaction);
return PendingEVMChainTransaction(
feeCurrency: "ETH",
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: BigInt.from(fee.estimatedGasFee),
amount: 0.toString(),
exponent: 18,
);
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei);
}
rethrow;
}
}
// Set an infinite approval to save gas in the future
Future<PendingEVMChainTransaction> enableSavings(EVMChainTransactionPriority priority) async {
try {
await _checkEthBalanceForGasFees(priority);
return (await _wallet.createApprovalTransaction(
BigInt.parse(
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
radix: 16,
),
_savingsGateway.self.address.hexEip55,
CryptoCurrency.deuro,
priority,
"ETH",
)) as PendingEVMChainTransaction;
} catch (e) {
if (e.toString().contains('insufficient funds for gas')) {
final ethBalance = await _wallet.getWeb3Client()!.getBalance(_address);
throw InsufficientGasFeeException(currentBalance: ethBalance.getInWei);
}
rethrow;
}
}
}
File diff suppressed because one or more lines are too long
+36
View File
@@ -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);
}
}
}
+22
View File
@@ -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<Erc20Token> getDefaultTokensByChainId(int chainId) {
return switch (chainId) {
1 => EthereumTokens.tokens,
137 => PolygonTokens.tokens,
8453 => BaseTokens.tokens,
42161 => ArbitrumTokens.tokens,
_ => [],
};
}
static List<String> getDefaultTokenAddresses(int chainId) {
return getDefaultTokensByChainId(chainId).map((token) => token.contractAddress).toList();
}
}
+4 -1
View File
@@ -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
-43
View File
@@ -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;
}
}
}
+203
View File
@@ -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<int, ChainConfig> _chains = {};
final Map<WalletType, int> _walletTypeToChainId = {};
final Map<int, WalletType> _chainIdToWalletType = {};
final Map<String, int> _tagToChainId = {};
final Map<String, int> _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<int> getRegisteredChainIds() => _chains.keys.toList();
List<ChainConfig> getAllChains() => _chains.values.toList();
List<WalletType> getRegisteredWalletTypes() =>
_walletTypeToChainId.keys.toList();
}
@@ -8,10 +8,12 @@ class EVMChainTransactionCredentials {
required this.priority,
required this.currency,
this.feeRate,
this.useBlinkProtection = true,
});
final List<OutputInfo> outputs;
final EVMChainTransactionPriority? priority;
final int? feeRate;
final CryptoCurrency currency;
final bool useBlinkProtection;
}
+50 -14
View File
@@ -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<EVMChainTransactionInfo> 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<String, EVMChainTransactionInfo>();
}
@@ -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<String, dynamic> val);
//! Common methods across all child classes
EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val) {
return EVMChainTransactionInfo.fromJson(val, getCurrentChainId());
}
Future<void> 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 = <String, EVMChainTransactionInfo>{};
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<String, EVMChainTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
void addMany(Map<String, EVMChainTransactionInfo> 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<Map<String, dynamic>> _read() async {
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
+30 -5
View File
@@ -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<String, dynamic> 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<String, dynamic> toJson() => {
@@ -79,5 +103,6 @@ abstract class EVMChainTransactionInfo extends TransactionInfo {
'from': from,
'evmSignatureName': evmSignatureName,
'contractAddress': contractAddress,
'chainId': chainId,
};
}
+4 -1
View File
@@ -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<String, dynamic> json, String defaultSymbol) =>
factory EVMChainTransactionModel.fromJson(Map<String, dynamic> 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,
);
}
+474 -78
View File
@@ -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<String, String> methodSignatureToType = {
'0xd505accf': 'permit',
};
abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
abstract class EVMChainWalletBase
extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
@@ -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<CryptoCurrency, EVMChainERC20Balance>.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<Erc20Token> erc20TokensBox;
late final Box<Erc20Token> evmChainErc20TokensBox;
late Box<Erc20Token> 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<SharedPreferences> 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<void> selectChain(int chainId, {required Node node}) async {
if (EvmChainRegistry().getChainConfig(chainId) == null) {
throw Exception('Chain config not found for chainId: $chainId');
}
// Future<EVMChainWallet> open({
// required String name,
// required String password,
// required WalletInfo walletInfo,
// });
if (selectedChainId == chainId) return;
List<String> get getDefaultTokenContractAddresses;
_client.stop();
Future<void> initErc20TokensBox();
balance.clear();
String getTransactionHistoryFileName();
selectedChainId = chainId;
_client = EVMChainClientFactory.createClient(selectedChainId);
Future<bool> 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<String> get getDefaultTokenContractAddresses =>
EVMChainDefaultTokens.getDefaultTokenAddresses(selectedChainId);
Future<void> 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<Erc20Token>(boxName);
} else {
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
}
addInitialTokens();
}
/// Ethereum-specific initialization with backward compatibility
Future<void> _initEthereumErc20TokensBox() async {
// Opens a box specific to this wallet
evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}",
);
erc20TokensBox = await CakeHive.openBox<Erc20Token>(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<bool> 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<bool> 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<int> _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>(
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<PendingTransaction> createApprovalTransaction(
BigInt amount,
String spender,
CryptoCurrency token,
EVMChainTransactionPriority? priority,
String feeCurrency,
) async {
Future<PendingTransaction> 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<void> _updateBalance() async {
@@ -826,12 +1061,66 @@ abstract class EVMChainWalletBase
}
Future<EVMChainERC20Balance> _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<void> _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<Erc20Token>().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<void> updateTransactionsHistory() async => await _updateTransactions();
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
List<Erc20Token> get erc20Currencies {
try {
if (!evmChainErc20TokensBox.isOpen) return [];
return evmChainErc20TokensBox.values.toList();
} catch (_) {
return [];
}
}
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
@@ -935,12 +1232,25 @@ abstract class EVMChainWalletBase
}
}
Future<void> deleteErc20Token(Erc20Token token) async {
await token.delete();
Future<void> 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<void> removeTokenTransactionsInHistory(Erc20Token token) async {
@@ -948,14 +1258,100 @@ abstract class EVMChainWalletBase
await transactionHistory.save();
}
Future<Erc20Token?> getErc20Token(String contractAddress, String chainName) async =>
await _client.getErc20Token(contractAddress, chainName);
Future<Erc20Token?> 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<EVMChainWallet> 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<String, dynamic>? data;
try {
final jsonSource = await encryptionFileUtils.read(path: path, password: password);
data = json.decode(jsonSource) as Map<String, dynamic>;
} 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<void> renameWalletFiles(String newWalletName) async {
final transactionHistoryFileNameForWallet = getTransactionHistoryFileName();
+352 -29
View File
@@ -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<T extends EVMChainWallet> 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<T extends EVMChainWallet> extends WalletSer
EVMChainWalletService(this.isDirect);
final bool isDirect;
final EvmChainRegistry _registry = EvmChainRegistry();
List<WalletType> get _evmWalletTypes {
return _registry.getRegisteredWalletTypes();
}
Future<WalletInfo?> _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<void> 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<void> 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<EVMChainWallet> 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<T> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet});
@override
Future<T> restoreFromHardwareWallet(EVMChainRestoreWalletFromHardware credentials);
@override
Future<T> openWallet(String name, String password);
@override
Future<void> rename(String currentName, String password, String newName);
@override
Future<T> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, {bool? isTestnet});
@override
Future<T> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, {bool? isTestnet});
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = await WalletInfo.get(wallet, getType());
Future<EVMChainWallet> 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<void> 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<EVMChainWallet> 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<EVMChainWallet> 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<EVMChainWallet> 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<bool> isWalletExit(String name) async {
for (final type in _evmWalletTypes) {
if (File(await pathForWallet(name: name, type: type)).existsSync()) {
return true;
}
}
return false;
}
@override
Future<void> 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<EVMChainWallet> _openWalletInstance({
required String name,
required String password,
required WalletInfo walletInfo,
required EncryptionFileUtils encryptionFileUtils,
}) {
return EVMChainWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtils,
);
}
}
+83
View File
@@ -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<Erc20Token> 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();
}
}
+76
View File
@@ -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<Erc20Token> 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();
}
}
+97
View File
@@ -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<Erc20Token> 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();
}
}
+90
View File
@@ -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<Erc20Token> 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();
}
}
+57
View File
@@ -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(".");
}
}
+115
View File
@@ -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(),
};
}
}
+63
View File
@@ -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<String> defaultRpcEndpoints;
final List<String> 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,
});
}
+1 -1
View File
@@ -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";