stack_wallet/lib/services/ethereum/ethereum_token_service.dart

594 lines
19 KiB
Dart
Raw Normal View History

import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
2023-01-25 09:29:20 +00:00
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/token_balance.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
2023-02-23 22:59:58 +00:00
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/eth_token_cache.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
2023-04-06 21:24:56 +00:00
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
2023-03-24 16:11:18 +00:00
import 'package:stackwallet/utilities/extensions/extensions.dart';
2023-03-24 00:05:35 +00:00
import 'package:stackwallet/utilities/extensions/impl/contract_abi.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:tuple/tuple.dart';
2023-02-24 14:45:34 +00:00
import 'package:web3dart/web3dart.dart' as web3dart;
2023-01-25 16:08:27 +00:00
2023-03-03 14:36:56 +00:00
class EthTokenWallet extends ChangeNotifier with EthTokenCache {
final EthereumWallet ethWallet;
final TransactionNotificationTracker tracker;
final SecureStorageInterface _secureStore;
2023-02-23 22:59:58 +00:00
// late web3dart.EthereumAddress _contractAddress;
2023-02-24 14:45:34 +00:00
late web3dart.EthPrivateKey _credentials;
late web3dart.DeployedContract _deployedContract;
2023-02-24 14:45:34 +00:00
late web3dart.ContractFunction _balanceFunction;
late web3dart.ContractFunction _sendFunction;
late web3dart.Web3Client _client;
2023-01-25 09:29:20 +00:00
static const _gasLimit = 200000;
2023-01-25 16:08:27 +00:00
2023-03-03 14:36:56 +00:00
EthTokenWallet({
required EthContract token,
required this.ethWallet,
2023-01-27 12:32:05 +00:00
required SecureStorageInterface secureStore,
required this.tracker,
}) : _secureStore = secureStore,
_tokenContract = token {
// _contractAddress = web3dart.EthereumAddress.fromHex(token.address);
initCache(ethWallet.walletId, token);
}
2023-01-25 09:29:20 +00:00
EthContract get tokenContract => _tokenContract;
EthContract _tokenContract;
TokenBalance get balance => _balance ??= getCachedBalance();
TokenBalance? _balance;
2023-01-25 09:29:20 +00:00
Coin get coin => Coin.ethereum;
2023-01-25 09:29:20 +00:00
2023-03-31 15:26:43 +00:00
Future<Map<String, dynamic>> prepareSend({
required String address,
2023-04-05 22:06:31 +00:00
required Amount amount,
2023-03-31 15:26:43 +00:00
Map<String, dynamic>? args,
}) async {
final feeRateType = args?["feeRate"];
int fee = 0;
final feeObject = await fees;
switch (feeRateType) {
case FeeRateType.fast:
fee = feeObject.fast;
break;
case FeeRateType.average:
fee = feeObject.medium;
break;
case FeeRateType.slow:
fee = feeObject.slow;
break;
}
2023-04-05 22:06:31 +00:00
final feeEstimate = estimateFeeFor(fee);
2023-03-31 15:26:43 +00:00
final client = await getEthClient();
2023-03-31 23:17:15 +00:00
final myAddress = await currentReceivingAddress;
final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress);
2023-03-31 15:26:43 +00:00
final est = await client.estimateGas(
2023-03-31 23:17:15 +00:00
sender: myWeb3Address,
2023-03-31 15:26:43 +00:00
to: web3dart.EthereumAddress.fromHex(address),
2023-04-05 22:06:31 +00:00
data: _sendFunction
.encodeCall([web3dart.EthereumAddress.fromHex(address), amount.raw]),
2023-03-31 15:26:43 +00:00
gasPrice: web3dart.EtherAmount.fromUnitAndValue(
web3dart.EtherUnit.wei,
fee,
),
amountOfGas: BigInt.from(_gasLimit),
);
2023-03-31 23:17:15 +00:00
final nonce = args?["nonce"] as int? ??
await client.getTransactionCount(myWeb3Address,
atBlock: const web3dart.BlockNum.pending());
final nResponse = await EthereumAPI.getAddressNonce(address: myAddress);
2023-03-31 21:14:45 +00:00
print("==============================================================");
2023-03-31 23:17:15 +00:00
print("TOKEN client.estimateGas: $est");
print("TOKEN estimateFeeFor : $feeEstimate");
print("TOKEN nonce custom response: $nResponse");
print("TOKEN actual nonce : $nonce");
2023-03-31 21:14:45 +00:00
print("==============================================================");
2023-03-31 15:26:43 +00:00
final tx = web3dart.Transaction.callContract(
contract: _deployedContract,
function: _sendFunction,
2023-04-05 22:06:31 +00:00
parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw],
2023-03-31 15:26:43 +00:00
maxGas: _gasLimit,
gasPrice: web3dart.EtherAmount.fromUnitAndValue(
web3dart.EtherUnit.wei,
fee,
),
2023-03-31 23:17:15 +00:00
nonce: nonce,
2023-03-31 15:26:43 +00:00
);
Map<String, dynamic> txData = {
"fee": feeEstimate,
"feeInWei": fee,
"address": address,
2023-04-05 22:06:31 +00:00
"recipientAmt": amount,
2023-03-31 15:26:43 +00:00
"ethTx": tx,
2023-03-31 23:17:15 +00:00
"chainId": (await client.getChainId()).toInt(),
"nonce": tx.nonce,
2023-03-31 15:26:43 +00:00
};
return txData;
}
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
2023-03-31 23:17:15 +00:00
try {
final txid = await _client.sendTransaction(
_credentials,
txData["ethTx"] as web3dart.Transaction,
chainId: txData["chainId"] as int,
);
try {
txData["txid"] = txid;
await updateSentCachedTxData(txData);
} catch (e, s) {
// do not rethrow as that would get handled as a send failure further up
// also this is not critical code and transaction should show up on \
// refresh regardless
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
}
notifyListeners();
return txid;
} catch (e) {
// rethrow to pass error in alert
rethrow;
}
}
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final txid = txData["txid"] as String;
final addressString = txData["address"] as String;
final response = await EthereumAPI.getEthTransactionByHash(txid);
final transaction = Transaction(
walletId: ethWallet.walletId,
txid: txid,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: TransactionType.outgoing,
subType: TransactionSubType.ethToken,
2023-04-06 23:49:13 +00:00
// precision may be lost here hence the following amountString
amount: (txData["recipientAmt"] as Amount).raw.toInt(),
amountString: (txData["recipientAmt"] as Amount).toJsonString(),
2023-03-31 23:17:15 +00:00
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: tokenContract.address,
slateId: null,
nonce: (txData["nonce"] as int?) ??
response.value?.nonce.toBigIntFromHex.toInt(),
inputs: [],
outputs: [],
);
Address? address = await ethWallet.db.getAddress(
ethWallet.walletId,
addressString,
);
address ??= Address(
walletId: ethWallet.walletId,
value: addressString,
publicKey: [],
derivationIndex: -1,
derivationPath: null,
type: AddressType.ethereum,
subType: AddressSubType.nonWallet,
2023-03-31 15:26:43 +00:00
);
2023-03-31 23:17:15 +00:00
await ethWallet.db.addNewTransactionData(
[
Tuple2(transaction, address),
],
ethWallet.walletId,
);
2023-01-25 09:29:20 +00:00
}
Future<String> get currentReceivingAddress async {
final address = await _currentReceivingAddress;
2023-03-01 21:19:53 +00:00
return checksumEthereumAddress(
address?.value ?? _credentials.address.toString());
}
2023-01-25 09:29:20 +00:00
Future<Address?> get _currentReceivingAddress => ethWallet.db
.getAddresses(ethWallet.walletId)
.filter()
.typeEqualTo(AddressType.ethereum)
.subTypeEqualTo(AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst();
2023-04-05 22:06:31 +00:00
Amount estimateFeeFor(int feeRate) {
return estimateFee(feeRate, _gasLimit, coin.decimals);
2023-01-25 09:29:20 +00:00
}
2023-03-29 18:49:12 +00:00
Future<FeeObject> get fees => EthereumAPI.getFees();
2023-01-25 09:29:20 +00:00
Future<EthContract> _updateTokenABI({
required EthContract forContract,
required String usingContractAddress,
}) async {
2023-03-24 00:05:35 +00:00
final abiResponse = await EthereumAPI.getTokenAbi(
name: forContract.name,
contractAddress: usingContractAddress,
);
// Fetch token ABI so we can call token functions
if (abiResponse.value != null) {
final updatedToken = forContract.copyWith(abi: abiResponse.value!);
// Store updated contract
final id = await MainDB.instance.putEthContract(updatedToken);
return updatedToken..id = id;
} else {
throw abiResponse.exception!;
}
}
Future<void> initialize() async {
final contractAddress =
web3dart.EthereumAddress.fromHex(tokenContract.address);
2023-03-24 00:05:35 +00:00
// if (tokenContract.abi == null) {
_tokenContract = await _updateTokenABI(
forContract: tokenContract,
usingContractAddress: contractAddress.hex,
);
// }
2023-01-27 12:32:05 +00:00
String? mnemonicString = await ethWallet.mnemonicString;
2023-01-25 16:08:27 +00:00
//Get private key for given mnemonic
String privateKey = getPrivateKey(
mnemonicString!,
(await ethWallet.mnemonicPassphrase) ?? "",
);
2023-02-24 14:45:34 +00:00
_credentials = web3dart.EthPrivateKey.fromHex(privateKey);
_deployedContract = web3dart.DeployedContract(
2023-03-24 00:05:35 +00:00
ContractAbiExtensions.fromJsonList(
jsonList: tokenContract.abi!,
name: tokenContract.name,
),
contractAddress,
);
try {
_balanceFunction = _deployedContract.function('balanceOf');
_sendFunction = _deployedContract.function('transfer');
} catch (_) {
2023-03-24 00:05:35 +00:00
//====================================================================
2023-03-29 21:57:52 +00:00
// final list = List<Map<String, dynamic>>.from(
// jsonDecode(tokenContract.abi!) as List);
// final functionNames = list.map((e) => e["name"] as String);
//
// if (!functionNames.contains("balanceOf")) {
// list.add(
// {
// "encoding": "0x70a08231",
// "inputs": [
// {"name": "account", "type": "address"}
// ],
// "name": "balanceOf",
// "outputs": [
// {"name": "val_0", "type": "uint256"}
// ],
// "signature": "balanceOf(address)",
// "type": "function"
// },
// );
// }
//
// if (!functionNames.contains("transfer")) {
// list.add(
// {
// "encoding": "0xa9059cbb",
// "inputs": [
// {"name": "dst", "type": "address"},
// {"name": "rawAmount", "type": "uint256"}
// ],
// "name": "transfer",
// "outputs": [
// {"name": "val_0", "type": "bool"}
// ],
// "signature": "transfer(address,uint256)",
// "type": "function"
// },
// );
// }
2023-03-24 00:05:35 +00:00
//--------------------------------------------------------------------
//====================================================================
// function not found so likely a proxy so we need to fetch the impl
//====================================================================
2023-03-29 21:57:52 +00:00
// final updatedToken = tokenContract.copyWith(abi: jsonEncode(list));
// // Store updated contract
// final id = await MainDB.instance.putEthContract(updatedToken);
// _tokenContract = updatedToken..id = id;
2023-03-24 00:05:35 +00:00
//--------------------------------------------------------------------
2023-03-29 21:57:52 +00:00
final contractAddressResponse =
await EthereumAPI.getProxyTokenImplementationAddress(
contractAddress.hex);
if (contractAddressResponse.value != null) {
_tokenContract = await _updateTokenABI(
forContract: tokenContract,
usingContractAddress: contractAddressResponse.value!,
);
} else {
throw contractAddressResponse.exception!;
}
2023-03-24 00:05:35 +00:00
//====================================================================
}
_deployedContract = web3dart.DeployedContract(
2023-03-24 00:05:35 +00:00
ContractAbiExtensions.fromJsonList(
jsonList: tokenContract.abi!,
name: tokenContract.name,
2023-03-02 00:02:53 +00:00
),
contractAddress,
2023-03-02 00:02:53 +00:00
);
_balanceFunction = _deployedContract.function('balanceOf');
_sendFunction = _deployedContract.function('transfer');
2023-03-02 00:02:53 +00:00
_client = await getEthClient();
2023-01-25 09:29:20 +00:00
unawaited(refresh());
}
2023-01-25 09:29:20 +00:00
bool get isRefreshing => _refreshLock;
bool _refreshLock = false;
Future<void> refresh() async {
if (!_refreshLock) {
_refreshLock = true;
try {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
ethWallet.walletId + tokenContract.address,
coin,
),
);
await refreshCachedBalance();
await _refreshTransactions();
} catch (e, s) {
Logging.instance.log(
"Caught exception in ${tokenContract.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s",
level: LogLevel.Warning,
);
} finally {
_refreshLock = false;
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
ethWallet.walletId + tokenContract.address,
coin,
),
);
notifyListeners();
}
}
2023-01-25 09:29:20 +00:00
}
Future<void> refreshCachedBalance() async {
final balanceRequest = await _client.call(
contract: _deployedContract,
function: _balanceFunction,
params: [_credentials.address],
);
String _balance = balanceRequest.first.toString();
final newBalance = TokenBalance(
contractAddress: tokenContract.address,
2023-04-05 22:06:31 +00:00
total: Amount.fromDecimal(
Decimal.parse(_balance),
fractionDigits: tokenContract.decimals,
),
spendable: Amount.fromDecimal(
Decimal.parse(_balance),
fractionDigits: tokenContract.decimals,
),
blockedTotal: Amount(
rawValue: BigInt.zero,
fractionDigits: tokenContract.decimals,
),
pendingSpendable: Amount(
rawValue: BigInt.zero,
fractionDigits: tokenContract.decimals,
),
2023-02-23 22:59:58 +00:00
);
await updateCachedBalance(newBalance);
notifyListeners();
}
2023-01-25 09:29:20 +00:00
Future<List<Transaction>> get transactions => ethWallet.db
.getTransactions(ethWallet.walletId)
.filter()
.otherDataEqualTo(tokenContract.address)
.sortByTimestampDesc()
.findAll();
2023-01-25 09:29:20 +00:00
2023-03-24 16:11:18 +00:00
String _addressFromTopic(String topic) =>
checksumEthereumAddress("0x${topic.substring(topic.length - 40)}");
Future<void> _refreshTransactions() async {
String addressString =
checksumEthereumAddress(await currentReceivingAddress);
2023-01-27 12:32:05 +00:00
final response = await EthereumAPI.getTokenTransactions(
address: addressString,
tokenContractAddress: tokenContract.address,
);
2023-01-27 12:32:05 +00:00
if (response.value == null) {
throw response.exception ??
Exception("Failed to fetch token transaction data");
}
2023-03-23 19:51:56 +00:00
// no need to continue if no transactions found
if (response.value!.isEmpty) {
return;
}
final response2 = await EthereumAPI.getEthTokenTransactionsByTxids(
response.value!.map((e) => e.transactionHash).toList(),
);
if (response2.value == null) {
throw response2.exception ??
Exception("Failed to fetch token transactions");
}
final List<Tuple2<EthTokenTxDto, EthTokenTxExtraDTO>> data = [];
for (final tokenDto in response.value!) {
data.add(
Tuple2(
tokenDto,
response2.value!.firstWhere(
(e) => e.hash == tokenDto.transactionHash,
),
),
);
}
2023-01-27 12:32:05 +00:00
final List<Tuple2<Transaction, Address?>> txnsData = [];
2023-01-27 12:32:05 +00:00
for (final tuple in data) {
// ignore all non Transfer events (for now)
if (tuple.item1.topics[0] == kTransferEventSignature) {
final Amount amount;
String fromAddress, toAddress;
amount = Amount(
rawValue: tuple.item1.data.toBigIntFromHex,
fractionDigits: tokenContract.decimals,
);
2023-03-24 16:11:18 +00:00
fromAddress = _addressFromTopic(
tuple.item1.topics[1],
);
toAddress = _addressFromTopic(
tuple.item1.topics[2],
);
bool isIncoming;
bool isSentToSelf = false;
if (fromAddress == addressString) {
isIncoming = false;
if (toAddress == addressString) {
isSentToSelf = true;
}
} else if (toAddress == addressString) {
isIncoming = true;
} else {
throw Exception("Unknown token transaction found for "
"${ethWallet.walletName} ${ethWallet.walletId}: "
"${tuple.item1.toString()}");
2023-03-24 16:11:18 +00:00
}
2023-01-27 12:32:05 +00:00
final txn = Transaction(
walletId: ethWallet.walletId,
txid: tuple.item1.transactionHash,
timestamp: tuple.item2.timestamp,
type:
isIncoming ? TransactionType.incoming : TransactionType.outgoing,
subType: TransactionSubType.ethToken,
amount: amount.raw.toInt(),
amountString: amount.toJsonString(),
fee: (tuple.item2.gasUsed.raw * tuple.item2.gasPrice.raw).toInt(),
height: tuple.item1.blockNumber,
isCancelled: false,
isLelantus: false,
slateId: null,
2023-03-31 16:15:42 +00:00
nonce: tuple.item2.nonce,
otherData: tuple.item1.address,
inputs: [],
outputs: [],
);
Address? transactionAddress = await ethWallet.db
.getAddresses(ethWallet.walletId)
.filter()
.valueEqualTo(toAddress)
.findFirst();
transactionAddress ??= Address(
walletId: ethWallet.walletId,
value: toAddress,
publicKey: [],
derivationIndex: isSentToSelf ? 0 : -1,
derivationPath: isSentToSelf
? (DerivationPath()..value = "$hdPathEthereum/0")
: null,
type: AddressType.ethereum,
subType: isSentToSelf
? AddressSubType.receiving
: AddressSubType.nonWallet,
);
2023-01-27 12:32:05 +00:00
txnsData.add(Tuple2(txn, transactionAddress));
}
}
await ethWallet.db.addNewTransactionData(txnsData, ethWallet.walletId);
// quick hack to notify manager to call notifyListeners if
// transactions changed
if (txnsData.isNotEmpty) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"${tokenContract.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}",
ethWallet.walletId,
),
);
2023-01-27 12:32:05 +00:00
}
}
2023-01-25 09:29:20 +00:00
bool validateAddress(String address) {
return isValidEthereumAddress(address);
}
2023-03-31 23:17:15 +00:00
NodeModel getCurrentNode() {
2023-01-27 12:32:05 +00:00
return NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
}
2023-02-24 14:45:34 +00:00
Future<web3dart.Web3Client> getEthClient() async {
2023-03-31 23:17:15 +00:00
final node = getCurrentNode();
2023-02-24 14:45:34 +00:00
return web3dart.Web3Client(node.host, Client());
2023-01-25 09:29:20 +00:00
}
}