From fd0b20d66102b777d7da25114a5fe15d03d80f9a Mon Sep 17 00:00:00 2001 From: likho Date: Fri, 27 Jan 2023 14:32:05 +0200 Subject: [PATCH] Complete adding ERC-20 functionality --- .../sub_widgets/my_token_select_item.dart | 7 +- .../coins/ethereum/ethereum_wallet.dart | 72 ------- .../tokens/ethereum/ethereum_token.dart | 181 ++++++++++++++++-- lib/services/tokens/token_service.dart | 2 +- lib/utilities/eth_commons.dart | 56 +++--- 5 files changed, 199 insertions(+), 119 deletions(-) diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 064949cd7..3fcc75619 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:tuple/tuple.dart'; @@ -35,7 +35,6 @@ class MyTokenSelectItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - print("TOKEN DATA IS $tokenData"); int balance = tokenData["balance"] as int; int tokenDecimals = int.parse(tokenData["decimals"] as String); final balanceInDecimal = (balance / (pow(10, tokenDecimals))); @@ -55,9 +54,9 @@ class MyTokenSelectItem extends ConsumerWidget { final mnemonicList = ref.read(managerProvider).mnemonic; final token = EthereumToken( - // contractAddress: tokenData["contractAddress"] as String, tokenData: tokenData, - walletMnemonic: mnemonicList); + walletMnemonic: mnemonicList, + secureStore: ref.read(secureStoreProvider)); Navigator.of(context).pushNamed( TokenView.routeName, diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index e056e334b..ff37014d5 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; import 'package:bip39/bip39.dart' as bip39; @@ -42,31 +41,9 @@ import 'package:stackwallet/utilities/default_nodes.dart'; const int MINIMUM_CONFIRMATIONS = 3; -//THis is used for mapping transactions per address from the block explorer -class AddressTransaction { - final String message; - final List result; - final String status; - - const AddressTransaction({ - required this.message, - required this.result, - required this.status, - }); - - factory AddressTransaction.fromJson(Map json) { - return AddressTransaction( - message: json['message'] as String, - result: json['result'] as List, - status: json['status'] as String, - ); - } -} - class EthereumWallet extends CoinServiceAPI { NodeModel? _ethNode; final _gasLimit = 21000; - final _blockExplorer = "https://blockscout.com/eth/mainnet/api?"; @override String get walletId => _walletId; @@ -436,42 +413,6 @@ class EthereumWallet extends CoinServiceAPI { String privateKey = getPrivateKey(mnemonic); _credentials = EthPrivateKey.fromHex(privateKey); - //Get ERC-20 transactions for wallet (So we can get the and save wallet's ERC-20 TOKENS - AddressTransaction tokenTransactions = await fetchAddressTransactions( - _credentials.address.toString(), "tokentx"); - var tokenMap = {}; - List> tokensList = []; - if (tokenTransactions.message == "OK") { - final allTxs = tokenTransactions.result; - - allTxs.forEach((element) { - String key = element["tokenSymbol"] as String; - tokenMap[key] = {}; - tokenMap[key]["balance"] = 0; - - if (tokenMap.containsKey(key)) { - tokenMap[key]["contractAddress"] = element["contractAddress"]; - tokenMap[key]["decimals"] = element["tokenDecimal"]; - tokenMap[key]["name"] = element["tokenName"]; - tokenMap[key]["symbol"] = element["tokenSymbol"]; - if (element["to"] == _credentials.address.toString()) { - tokenMap[key]["balance"] += int.parse(element["value"] as String); - } else { - tokenMap[key]["balance"] -= int.parse(element["value"] as String); - } - } - }); - - tokenMap.forEach((key, value) { - //Create New token - - tokensList.add(value as Map); - }); - - await _secureStore.write( - key: '${_walletId}_tokens', value: tokensList.toString()); - } - await DB.instance .put(boxName: walletId, key: "id", value: _walletId); await DB.instance @@ -846,19 +787,6 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - Future fetchAddressTransactions( - String address, String action) async { - final response = await get(Uri.parse( - "${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); - - if (response.statusCode == 200) { - return AddressTransaction.fromJson( - json.decode(response.body) as Map); - } else { - throw Exception('Failed to load transactions'); - } - } - Future _fetchTransactionData() async { String thisAddress = await currentReceivingAddress; final cachedTransactions = diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index 01407aa60..8998acaea 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:math'; +import 'package:devicelocale/devicelocale.dart'; import 'package:http/http.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/utilities/eth_commons.dart'; @@ -13,9 +14,14 @@ import 'package:stackwallet/services/tokens/token_service.dart'; import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/models/paymint/transactions_model.dart' as models; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as transaction; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/services/node_service.dart'; + class AbiRequestResponse { final String message; final String result; @@ -36,6 +42,8 @@ class AbiRequestResponse { } } +const int MINIMUM_CONFIRMATIONS = 3; + class EthereumToken extends TokenServiceAPI { @override late bool shouldAutoSync; @@ -50,28 +58,25 @@ class EthereumToken extends TokenServiceAPI { late String _tokenAbi; late Web3Client _client; late final TransactionNotificationTracker txTracker; + TransactionData? cachedTxData; - String rpcUrl = - 'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba'; final _gasLimit = 200000; EthereumToken({ required Map tokenData, required Future> walletMnemonic, - // required SecureStorageInterface secureStore, + required SecureStorageInterface secureStore, }) { _contractAddress = EthereumAddress.fromHex(tokenData["contractAddress"] as String); _walletMnemonic = walletMnemonic; _tokenData = tokenData; - // _secureStore = secureStore; + _secureStore = secureStore; } Future fetchTokenAbi() async { - print( - "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"); final response = await get(Uri.parse( - "$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$abiUrl?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); if (response.statusCode == 200) { return AbiRequestResponse.fromJson( json.decode(response.body) as Map); @@ -156,12 +161,24 @@ class EthereumToken extends TokenServiceAPI { @override Future initializeExisting() async { - //TODO - GET abi FROM secure store - AbiRequestResponse abi = await fetchTokenAbi(); - //Fetch token ABI so we can call token functions - if (abi.message == "OK") { - _tokenAbi = abi.result; + if ((await _secureStore.read( + key: '${_contractAddress.toString()}_tokenAbi')) != + null) { + _tokenAbi = (await _secureStore.read( + key: '${_contractAddress.toString()}_tokenAbi'))!; + } else { + AbiRequestResponse abi = await fetchTokenAbi(); + //Fetch token ABI so we can call token functions + if (abi.message == "OK") { + _tokenAbi = abi.result; + //Store abi in secure store + await _secureStore.write( + key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); + } else { + throw Exception('Failed to load token abi'); + } } + final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); @@ -175,17 +192,21 @@ class EthereumToken extends TokenServiceAPI { _balanceFunction = _contract.function('balanceOf'); _sendFunction = _contract.function('transfer'); _client = await getEthClient(); - print("${await totalBalance}"); } @override Future initializeNew() async { - //TODO - Save abi in secure store AbiRequestResponse abi = await fetchTokenAbi(); //Fetch token ABI so we can call token functions if (abi.message == "OK") { _tokenAbi = abi.result; + //Store abi in secure store + await _secureStore.write( + key: '${_contractAddress.toString()}_tokenAbi', value: _tokenAbi); + } else { + throw Exception('Failed to load token abi'); } + final mnemonic = await _walletMnemonic; String mnemonicString = mnemonic.join(' '); @@ -253,7 +274,6 @@ class EthereumToken extends TokenServiceAPI { "recipientAmt": satoshiAmount, }; - print("TX DATA TO BE SENT IS $txData"); return txData; } @@ -277,12 +297,13 @@ class EthereumToken extends TokenServiceAPI { } @override - // TODO: implement transactionData - Future get transactionData => throw UnimplementedError(); + Future get transactionData => + _transactionData ??= _fetchTransactionData(); + Future? _transactionData; @override Future updateSentCachedTxData(Map txData) async { - Decimal currentPrice = Decimal.parse(0.0 as String); + Decimal currentPrice = Decimal.zero; final locale = await Devicelocale.currentLocale; final String worthNow = Format.localizedStringAsFixed( value: @@ -321,12 +342,134 @@ class EthereumToken extends TokenServiceAPI { } } + Future _fetchTransactionData() async { + String thisAddress = await currentReceivingAddress; + // final cachedTransactions = {} as TransactionData?; + int latestTxnBlockHeight = 0; + + // final priceData = + // await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); + Decimal currentPrice = Decimal.zero; + final List> midSortedArray = []; + + AddressTransaction txs = + await fetchAddressTransactions(thisAddress, "tokentx"); + + if (txs.message == "OK") { + final allTxs = txs.result; + allTxs.forEach((element) { + Map midSortedTx = {}; + // create final tx map + midSortedTx["txid"] = element["hash"]; + int confirmations = int.parse(element['confirmations'].toString()); + + int transactionAmount = int.parse(element['value'].toString()); + int decimal = int.parse( + _tokenData["decimals"] as String); //Eth has up to 18 decimal places + final transactionAmountInDecimal = + transactionAmount / (pow(10, decimal)); + + //Convert to satoshi, default display for other coins + final satAmount = Format.decimalAmountToSatoshis( + Decimal.parse(transactionAmountInDecimal.toString()), coin); + + midSortedTx["confirmed_status"] = + (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); + midSortedTx["confirmations"] = confirmations; + midSortedTx["timestamp"] = element["timeStamp"]; + + if (checksumEthereumAddress(element["from"].toString()) == + thisAddress) { + midSortedTx["txType"] = "Sent"; + } else { + midSortedTx["txType"] = "Received"; + } + + midSortedTx["amount"] = satAmount; + final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / + Decimal.fromInt(Constants.satsPerCoin(coin))) + .toDecimal(scaleOnInfinitePrecision: 2) + .toStringAsFixed(2); + + //Calculate fees (GasLimit * gasPrice) + int txFee = int.parse(element['gasPrice'].toString()) * + int.parse(element['gasUsed'].toString()); + final txFeeDecimal = txFee / (pow(10, decimal)); + + midSortedTx["worthNow"] = worthNow; + midSortedTx["worthAtBlockTimestamp"] = worthNow; + midSortedTx["aliens"] = []; + midSortedTx["fees"] = Format.decimalAmountToSatoshis( + Decimal.parse(txFeeDecimal.toString()), coin); + midSortedTx["address"] = element["to"]; + midSortedTx["inputSize"] = 1; + midSortedTx["outputSize"] = 1; + midSortedTx["inputs"] = []; + midSortedTx["outputs"] = []; + midSortedTx["height"] = int.parse(element['blockNumber'].toString()); + + midSortedArray.add(midSortedTx); + }); + } + + midSortedArray.sort((a, b) => + (int.parse(b['timestamp'].toString())) - + (int.parse(a['timestamp'].toString()))); + + // buildDateTimeChunks + final Map result = {"dateTimeChunks": []}; + final dateArray = []; + + for (int i = 0; i < midSortedArray.length; i++) { + final txObject = midSortedArray[i]; + final date = + extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); + final txTimeArray = [txObject["timestamp"], date]; + + if (dateArray.contains(txTimeArray[1])) { + result["dateTimeChunks"].forEach((dynamic chunk) { + if (extractDateFromTimestamp( + int.parse(chunk['timestamp'].toString())) == + txTimeArray[1]) { + if (chunk["transactions"] == null) { + chunk["transactions"] = >[]; + } + chunk["transactions"].add(txObject); + } + }); + } else { + dateArray.add(txTimeArray[1]); + final chunk = { + "timestamp": txTimeArray[0], + "transactions": [txObject], + }; + result["dateTimeChunks"].add(chunk); + } + } + + // final transactionsMap = {} as Map; + // transactionsMap + // .addAll(TransactionData.fromJson(result).getAllTransactions()); + final txModel = TransactionData.fromMap( + TransactionData.fromJson(result).getAllTransactions()); + + cachedTxData = txModel; + return txModel; + } + @override bool validateAddress(String address) { return isValidEthereumAddress(address); } + Future getCurrentNode() async { + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + Future getEthClient() async { - return Web3Client(rpcUrl, Client()); + final node = await getCurrentNode(); + return Web3Client(node.host, Client()); } } diff --git a/lib/services/tokens/token_service.dart b/lib/services/tokens/token_service.dart index c19897b08..ca515f43c 100644 --- a/lib/services/tokens/token_service.dart +++ b/lib/services/tokens/token_service.dart @@ -19,7 +19,7 @@ abstract class TokenServiceAPI { return EthereumToken( tokenData: tokenData, walletMnemonic: walletMnemonic, - // secureStore: secureStorageInterface, + secureStore: secureStorageInterface, // tracker: tracker, ); } diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index 5a221f828..627c8f96b 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -4,24 +4,23 @@ import 'dart:math'; import 'package:http/http.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; -import 'flutter_secure_storage_interface.dart'; import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import "package:hex/hex.dart"; -class AccountModule { +class AddressTransaction { final String message; final List result; final String status; - const AccountModule({ + const AddressTransaction({ required this.message, required this.result, required this.status, }); - factory AccountModule.fromJson(Map json) { - return AccountModule( + factory AddressTransaction.fromJson(Map json) { + return AddressTransaction( message: json['message'] as String, result: json['result'] as List, status: json['status'] as String, @@ -30,32 +29,40 @@ class AccountModule { } class GasTracker { - final int code; - final Map data; + final double average; + final double fast; + final double slow; + // final Map data; const GasTracker({ - required this.code, - required this.data, + required this.average, + required this.fast, + required this.slow, }); factory GasTracker.fromJson(Map json) { return GasTracker( - code: json['code'] as int, - data: json['data'] as Map, + average: json['average'] as double, + fast: json['fast'] as double, + slow: json['slow'] as double, ); } } -// const blockExplorer = "https://blockscout.com/eth/mainnet/api"; -const blockExplorer = "https://api.etherscan.io/api"; +const blockExplorer = "https://blockscout.com/eth/mainnet/api"; +const abiUrl = + "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update const _hdPath = "m/44'/60'/0'/0"; -const _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow"; +const _gasTrackerUrl = + "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; -Future fetchAccountModule(String action, String address) async { +Future fetchAddressTransactions( + String address, String action) async { final response = await get(Uri.parse( - "${blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP")); + "$blockExplorer?module=account&action=$action&address=$address")); + if (response.statusCode == 200) { - return AccountModule.fromJson( + return AddressTransaction.fromJson( json.decode(response.body) as Map); } else { throw Exception('Failed to load transactions'); @@ -63,7 +70,8 @@ Future fetchAccountModule(String action, String address) async { } Future> getWalletTokens(String address) async { - AccountModule tokens = await fetchAccountModule("tokentx", address); + AddressTransaction tokens = + await fetchAddressTransactions(address, "tokentx"); List tokensList = []; var tokenMap = {}; if (tokens.message == "OK") { @@ -110,7 +118,6 @@ String getPrivateKey(String mnemonic) { Future getGasOracle() async { final response = await get(Uri.parse(_gasTrackerUrl)); - if (response.statusCode == 200) { return GasTracker.fromJson( json.decode(response.body) as Map); @@ -121,14 +128,17 @@ Future getGasOracle() async { Future getFees() async { GasTracker fees = await getGasOracle(); - final feesMap = fees.data; + final feesFast = fees.fast * (pow(10, 9)); + final feesStandard = fees.average * (pow(10, 9)); + final feesSlow = fees.slow * (pow(10, 9)); + return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 3, numberOfBlocksSlow: 3, - fast: feesMap['fast'] as int, - medium: feesMap['standard'] as int, - slow: feesMap['slow'] as int); + fast: feesFast.toInt(), + medium: feesStandard.toInt(), + slow: feesSlow.toInt()); } double estimateFee(int feeRate, int gasLimit, int decimals) {