Files
hash-wallet/cw_evm/lib/evm_chain_client.dart
T
David AdegokeandGitHub 4b2af63fa5 CW-974: Add support for automatic node switching. (#2361)
* feat: Add support for automatic node switching.

This change:
- Adds isTrustedNodeForSwitching property to Node class.
- Adds UI Checkbox to mark a node as trusted for automatic switching in Node create or edit form.
- Implements checkNodeHealth method in wallet classes to verify node connectivity.
- Adds NodeSwitchingService for managing automatic node switching based on health checks.
- Adds setting option for user to enable/disable automated node switching

* fix(automatic-node-switch): Set node health check interval to 5 minutes in NodeSwitchingService

* refactor: Update handler method

* feat: Automatic Node Switching Improvements

This change:
- Moves enable automatic node switch to the Manage Nodes Page
- Adds check to verify node health before attempting to sync in EVM, Sol and Tron Wallets.
- Modifies trusted ndoes list for monero
- Adds trigger for Node Health Check when app resumes from background

* fix: Increase default request timeout for Tron and update health check interval for node switch

* feat: Enhance Node health check functionality.

This change adds additional checks for token balance in EVM, Solana and Tron Wallets in checkNodeHealth.

* fix: Update health check interval to 30 seconds in NodeSwitchingService

* Update bitcoin_electrum_server_list.yml

* fix: Electru wallet not connecting to new node after switching

* fix: Automated Node Switch not working on Decred Wallet

* fix: Add more nodes for AutoSwitching

This change:
- Enables more nodes across wallet types for auto switching
- Modifies CTA for checkbox
- Renames flags to fit

* fix: Add migration function for setting up default nodes for autoswitching

* fix: Review and testing fixes

* chore: Adjust balance and history fetch timer
2025-07-25 16:42:59 +03:00

441 lines
14 KiB
Dart

import 'dart:async';
import 'dart:convert';
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/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:flutter/foundation.dart';
import 'package:hex/hex.dart' as hex;
import 'package:web3dart/web3dart.dart';
import 'contract/erc20.dart';
abstract class EVMChainClient {
late final client = ProxyWrapper().getHttpIOClient();
Web3Client? _client;
//! To be overridden by all child classes
int get chainId;
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress});
Future<List<EVMChainTransactionModel>> fetchInternalTransactions(String address);
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes
bool connect(Node node) {
try {
Uri? rpcUri;
bool isModifiedNodeUri = false;
if (node.uriRaw == 'eth.nownodes.io' || node.uriRaw == 'matic.nownodes.io') {
isModifiedNodeUri = true;
String nowNodeApiKey = secrets.nowNodesApiKey;
rpcUri = Uri.https(node.uriRaw, '/$nowNodeApiKey');
}
_client =
Web3Client(isModifiedNodeUri ? rpcUri!.toString() : node.uri.toString(), client);
return true;
} catch (e) {
return false;
}
}
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
// _client?.pendingTransactions().listen((transactionHash) async {
// final transaction = await _client!.getTransactionByHash(transactionHash);
//
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
// onNewTransaction();
// }
// });
}
Future<EtherAmount> getBalance(EthereumAddress address, {bool throwOnError = false}) async {
try {
return await _client!.getBalance(address);
} catch (_) {
if (throwOnError) {
rethrow;
}
return EtherAmount.zero();
}
}
Future<int> getGasUnitPrice() async {
try {
final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int?> getGasBaseFee() async {
try {
final blockInfo = await _client!.getBlockInformation(isContainFullObj: false);
final baseFee = blockInfo.baseFeePerGas;
return baseFee?.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> getEstimatedGasUnitsForTransaction({
required EthereumAddress toAddress,
required EthereumAddress senderAddress,
required EtherAmount value,
String? contractAddress,
EtherAmount? gasPrice,
EtherAmount? maxFeePerGas,
Uint8List? data,
}) async {
try {
if (contractAddress == null) {
final estimatedGas = await _client!.estimateGas(
sender: senderAddress,
to: toAddress,
value: value,
);
return estimatedGas.toInt();
} else {
final contract = DeployedContract(
ethereumContractAbi,
EthereumAddress.fromHex(contractAddress),
);
final transfer = contract.function('transfer');
// Estimate gas units
final gasEstimate = await _client!.estimateGas(
sender: senderAddress,
to: EthereumAddress.fromHex(contractAddress),
data: data ?? transfer.encodeCall([
toAddress,
value.getInWei,
]),
);
return gasEstimate.toInt();
}
} catch (_) {
return 0;
}
}
Uint8List getEncodedDataForApprovalTransaction({
required EthereumAddress toAddress,
required EtherAmount value,
required EthereumAddress contractAddress,
}) {
final contract = DeployedContract(ethereumContractAbi, contractAddress);
final approve = contract.function('approve');
return approve.encodeCall([
toAddress,
value.getInWei,
]);
}
Future<PendingEVMChainTransaction> signTransaction({
required Credentials privateKey,
required String toAddress,
required BigInt amount,
required BigInt gasFee,
required int estimatedGasUnits,
required int maxFeePerGas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required String feeCurrency,
required int exponent,
String? contractAddress,
String? data,
int? gasPrice,
}) async {
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
contractAddress != null);
bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
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,
);
Uint8List signedTransaction;
final Function _sendTransaction;
if (isNativeToken) {
signedTransaction = await _client!.signTransaction(privateKey, transaction, chainId: chainId);
} else {
final erc20 = ERC20(
client: _client!,
address: EthereumAddress.fromHex(contractAddress!),
chainId: chainId,
);
signedTransaction = await erc20.transfer(
EthereumAddress.fromHex(toAddress),
amount,
credentials: privateKey,
transaction: transaction,
);
}
_sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction(
signedTransaction: prepareSignedTransactionForSending(signedTransaction),
amount: amount.toString(),
fee: gasFee,
feeCurrency: feeCurrency,
sendTransaction: _sendTransaction,
exponent: exponent,
);
}
Future<PendingEVMChainTransaction> signApprovalTransaction({
required Credentials privateKey,
required String spender,
required BigInt amount,
required BigInt gasFee,
required String feeCurrency,
required int estimatedGasUnits,
required int maxFeePerGas,
required EVMChainTransactionPriority priority,
required int exponent,
required String contractAddress,
int? gasPrice,
}) async {
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(contractAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: EtherAmount.zero(),
maxGas: estimatedGasUnits,
maxFeePerGas: EtherAmount.fromInt(EtherUnit.wei, maxFeePerGas),
gasPrice: gasPrice != null ? EtherAmount.fromInt(EtherUnit.wei, gasPrice) : null,
);
final erc20 = ERC20(
client: _client!,
address: EthereumAddress.fromHex(contractAddress),
chainId: chainId,
);
final signedTransaction = await erc20.approve(
EthereumAddress.fromHex(spender),
amount,
credentials: privateKey,
transaction: transaction,
);
return PendingEVMChainTransaction(
signedTransaction: prepareSignedTransactionForSending(signedTransaction),
amount: amount.toString(),
fee: gasFee,
feeCurrency: feeCurrency,
sendTransaction: () => sendTransaction(signedTransaction),
exponent: exponent,
isInfiniteApproval: amount.toRadixString(16) == 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
);
}
Transaction createTransaction({
required EthereumAddress from,
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
EtherAmount? gasPrice,
EtherAmount? maxFeePerGas,
Uint8List? data,
int? maxGas,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
data: data,
maxGas: maxGas,
gasPrice: gasPrice,
maxFeePerGas: maxFeePerGas,
);
}
Future<String> sendTransaction(Uint8List signedTransaction) async {
return await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
}
Future getTransactionDetails(String transactionHash) async {
// Wait for the transaction receipt to become available
TransactionReceipt? receipt;
while (receipt == null) {
receipt = await _client!.getTransactionReceipt(transactionHash);
await Future.delayed(const Duration(seconds: 1));
}
// Print the receipt information
log('Transaction Hash: ${receipt.transactionHash}');
log('Block Hash: ${receipt.blockHash}');
log('Block Number: ${receipt.blockNumber}');
log('Gas Used: ${receipt.gasUsed}');
/*
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 21000
*/
// Wait for the transaction receipt to become available
TransactionInformation? transactionInformation;
while (transactionInformation == null) {
log("********************************");
transactionInformation = await _client!.getTransactionByHash(transactionHash);
await Future.delayed(const Duration(seconds: 1));
}
// Print the receipt information
log('Transaction Hash: ${transactionInformation.hash}');
log('Block Hash: ${transactionInformation.blockHash}');
log('Block Number: ${transactionInformation.blockNumber}');
log('Gas Used: ${transactionInformation.gas}');
/*
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 53000
*/
}
Future<EVMChainERC20Balance> fetchERC20Balances(
EthereumAddress userAddress, String contractAddress) async {
try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final balance = await erc20.balanceOf(userAddress);
int exponent = (await erc20.decimals()).toInt();
return EVMChainERC20Balance(balance, exponent: exponent);
} on RangeError catch (_) {
throw Exception('Invalid token contract for this network.');
} catch (e) {
throw Exception('Could not fetch balances: ${e.toString()}');
}
}
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 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'] ?? '';
final decimal = decodedResponse['decimals'] ?? '0';
final iconPath = decodedResponse['logo'] ?? '';
return Erc20Token(
name: name,
symbol: filteredSymbol,
contractAddress: contractAddress,
decimal: int.tryParse(decimal) ?? 0,
iconPath: iconPath,
);
} 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;
}
}
Uint8List hexToBytes(String hexString) {
return Uint8List.fromList(
hex.HEX.decode(hexString.startsWith('0x') ? hexString.substring(2) : hexString));
}
void stop() {
_client?.dispose();
}
Web3Client? getWeb3Client() {
return _client;
}
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
//
// final contract = DeployedContract(
// contractAbi,
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
// );
// final decimalsFunction = contract.function('decimals');
// final decimals = await _client!.call(
// contract: contract,
// function: decimalsFunction,
// params: [],
// );
//
// int exponent = int.parse(decimals.first.toString());
// return exponent;
// }
}