WIP eth refactor

This commit is contained in:
julian 2023-02-23 16:59:58 -06:00
parent 1653bb2096
commit 5aed55235c
17 changed files with 470 additions and 374 deletions

View file

@ -0,0 +1,11 @@
import 'package:stackwallet/models/ethereum/eth_token.dart';
class Erc20Token extends EthToken {
Erc20Token({
required super.contractAddress,
required super.name,
required super.symbol,
required super.decimals,
required super.balance,
});
}

View file

@ -0,0 +1,11 @@
import 'package:stackwallet/models/ethereum/eth_token.dart';
class Erc721Token extends EthToken {
Erc721Token({
required super.contractAddress,
required super.name,
required super.symbol,
required super.decimals,
required super.balance,
});
}

View file

@ -0,0 +1,15 @@
class EthToken {
EthToken({
required this.contractAddress,
required this.name,
required this.symbol,
required this.decimals,
required this.balance,
});
final String contractAddress;
final String name;
final String symbol;
final int decimals;
final int balance;
}

View file

@ -207,5 +207,6 @@ enum TransactionSubType {
none,
bip47Notification, // bip47 payment code notification transaction flag
mint, // firo specific
join; // firo specific
join, // firo specific
ethToken; // eth token
}

View file

@ -3,30 +3,22 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/pages/token_view/all_tokens_view.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
import 'package:stackwallet/pages/token_view/all_tokens_view.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/services/coins/manager.dart';
class MyTokensView extends ConsumerStatefulWidget {
const MyTokensView({
@ -41,7 +33,7 @@ class MyTokensView extends ConsumerStatefulWidget {
final ChangeNotifierProvider<Manager> managerProvider;
final String walletId;
final String walletAddress;
final List<dynamic> tokens;
final List<EthToken> tokens;
@override
ConsumerState<MyTokensView> createState() => _TokenDetailsViewState();

View file

@ -1,49 +1,47 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ethereum/eth_token.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/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/ethereum/ethereum_token_service.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/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
class MyTokenSelectItem extends ConsumerWidget {
const MyTokenSelectItem(
{Key? key,
required this.managerProvider,
required this.walletId,
required this.walletAddress,
required this.tokenData,
required this.token,
required})
: super(key: key);
final ChangeNotifierProvider<Manager> managerProvider;
final String walletId;
final String walletAddress;
final Map<dynamic, dynamic> tokenData;
final EthToken token;
@override
Widget build(BuildContext context, WidgetRef ref) {
int balance = tokenData["balance"] as int;
int tokenDecimals = int.parse(tokenData["decimals"] as String);
final balanceInDecimal = (balance / (pow(10, tokenDecimals)));
final balanceInDecimal = Format.satoshisToEthTokenAmount(
token.balance,
token.decimals,
);
return RoundedWhiteContainer(
padding: const EdgeInsets.all(0),
child: MaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
key: Key("walletListItemButtonKey_${tokenData["symbol"]}"),
key: Key("walletListItemButtonKey_${token.symbol}"),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
@ -53,8 +51,8 @@ class MyTokenSelectItem extends ConsumerWidget {
onPressed: () {
final mnemonicList = ref.read(managerProvider).mnemonic;
final token = EthereumToken(
tokenData: tokenData,
final tokenService = EthereumTokenService(
token: token,
walletMnemonic: mnemonicList,
secureStore: ref.read(secureStoreProvider));
@ -62,11 +60,11 @@ class MyTokenSelectItem extends ConsumerWidget {
TokenView.routeName,
arguments: Tuple4(
walletId,
tokenData,
token,
ref
.read(walletsChangeNotifierProvider)
.getManagerProvider(walletId),
token),
tokenService),
);
},
@ -89,12 +87,12 @@ class MyTokenSelectItem extends ConsumerWidget {
Row(
children: [
Text(
tokenData["name"] as String,
token.name,
style: STextStyles.titleBold12(context),
),
const Spacer(),
Text(
"$balanceInDecimal ${tokenData["symbol"]}",
"$balanceInDecimal ${token.symbol}",
style: STextStyles.itemSubtitle(context),
),
],
@ -105,7 +103,7 @@ class MyTokenSelectItem extends ConsumerWidget {
Row(
children: [
Text(
tokenData["symbol"] as String,
token.symbol,
style: STextStyles.itemSubtitle(context),
),
const Spacer(),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart';
import 'package:stackwallet/services/coins/manager.dart';
@ -14,7 +15,7 @@ class MyTokensList extends StatelessWidget {
final ChangeNotifierProvider<Manager> managerProvider;
final String walletId;
final List<dynamic> tokens;
final List<EthToken> tokens;
final String walletAddress;
@override
@ -31,7 +32,7 @@ class MyTokensList extends StatelessWidget {
managerProvider: managerProvider,
walletId: walletId,
walletAddress: walletAddress,
tokenData: tokens[index] as Map<dynamic, dynamic>,
token: tokens[index],
),
);
},

View file

@ -4,6 +4,7 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart';
@ -12,10 +13,10 @@ import 'package:stackwallet/providers/global/auto_swb_service_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/ethereum/ethereum_token_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_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/tokens/ethereum/ethereum_token.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
@ -32,9 +33,9 @@ class TokenView extends ConsumerStatefulWidget {
const TokenView({
Key? key,
required this.walletId,
required this.tokenData,
required this.managerProvider,
required this.token,
required this.managerProvider,
required this.tokenService,
this.eventBus,
}) : super(key: key);
@ -42,9 +43,9 @@ class TokenView extends ConsumerStatefulWidget {
static const double navBarHeight = 65.0;
final String walletId;
final Map<dynamic, dynamic> tokenData;
final EthToken token;
final ChangeNotifierProvider<Manager> managerProvider;
final EthereumToken token;
final EthereumTokenService tokenService;
final EventBus? eventBus;
@override
@ -189,7 +190,7 @@ class _TokenViewState extends ConsumerState<TokenView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
widget.token.initializeExisting();
widget.tokenService.initializeExisting();
// print("MY TOTAL BALANCE IS ${widget.token.totalBalance}");
final coin = ref.watch(managerProvider.select((value) => value.coin));
@ -221,7 +222,7 @@ class _TokenViewState extends ConsumerState<TokenView> {
),
Expanded(
child: Text(
widget.tokenData["name"] as String,
widget.token.name,
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),

View file

@ -5,6 +5,7 @@ import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart';
import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart';
@ -27,6 +28,7 @@ import 'package:stackwallet/providers/wallet/public_private_balance_state_provid
import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_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';
@ -35,7 +37,7 @@ import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
@ -783,21 +785,43 @@ class _WalletViewState extends ConsumerState<WalletView> {
.read(managerProvider)
.currentReceivingAddress;
List<dynamic> tokens =
await getWalletTokens(await ref
final response = await showLoading<
EthereumResponse<List<EthToken>>>(
whileFuture:
EthereumAPI.getWalletTokens(
address: await ref
.read(managerProvider)
.currentReceivingAddress);
.currentReceivingAddress),
message: "Loading tokens",
isDesktop: false,
context: context,
);
if (mounted) {
await Navigator.of(context).pushNamed(
if (response.value != null) {
await Navigator.of(context)
.pushNamed(
MyTokensView.routeName,
arguments: Tuple4(
managerProvider,
walletId,
walletAddress,
tokens,
response.value!,
),
);
} else {
await showDialog<void>(
context: context,
builder: (context) {
return StackOkDialog(
title:
"Failed to fetch tokens",
message: response.exception
.toString(),
);
},
);
}
}
},
),

View file

@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
import 'package:stackwallet/models/contact_address_entry.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/paynym/paynym_account_lite.dart';
import 'package:stackwallet/models/send_view_auto_fill_data.dart';
@ -126,15 +128,13 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/ethereum/ethereum_token_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart';
import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:tuple/tuple.dart';
import 'models/isar/models/blockchain_data/transaction.dart';
class RouteGenerator {
static const bool useMaterialPageRoute = true;
@ -1431,7 +1431,7 @@ class RouteGenerator {
case MyTokensView.routeName:
if (args is Tuple4<ChangeNotifierProvider<Manager>, String, String,
List<dynamic>>) {
List<EthToken>>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => MyTokensView(
@ -1461,15 +1461,15 @@ class RouteGenerator {
// }
case TokenView.routeName:
if (args is Tuple4<String, Map<dynamic, dynamic>,
ChangeNotifierProvider<Manager>, EthereumToken>) {
if (args is Tuple4<String, EthToken, ChangeNotifierProvider<Manager>,
EthereumTokenService>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => TokenView(
walletId: args.item1,
tokenData: args.item2,
token: args.item2,
managerProvider: args.item3,
token: args.item4,
tokenService: args.item4,
),
settings: RouteSettings(
name: settings.name,

View file

@ -11,6 +11,7 @@ import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
@ -206,9 +207,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<FeeObject> get fees => _feeObject ??= _getFees();
Future<FeeObject>? _feeObject;
Future<FeeObject> _getFees() async {
return await getFees();
}
Future<FeeObject> _getFees() => EthereumAPI.getFees();
//Full rescan is not needed for ETH since we have a balance
@override
@ -536,7 +535,8 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
if (!needsRefresh) {
var allOwnAddresses = await _fetchAllOwnAddresses();
AddressTransaction addressTransactions = await fetchAddressTransactions(
AddressTransaction addressTransactions =
await EthereumAPI.fetchAddressTransactions(
allOwnAddresses.elementAt(0).value, "txlist");
if (addressTransactions.message == "OK") {
final allTxs = addressTransactions.result;
@ -812,7 +812,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
String thisAddress = await currentReceivingAddress;
AddressTransaction txs =
await fetchAddressTransactions(thisAddress, "txlist");
await EthereumAPI.fetchAddressTransactions(thisAddress, "txlist");
if (txs.message == "OK") {
final allTxs = txs.result;

View file

@ -0,0 +1,263 @@
import 'dart:convert';
import 'dart:math';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:http/http.dart';
import 'package:stackwallet/models/ethereum/erc721_token.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/logger.dart';
import '../../models/ethereum/erc20_token.dart';
import '../../models/ethereum/eth_token.dart';
class AbiRequestResponse {
final String message;
final String result;
final String status;
const AbiRequestResponse({
required this.message,
required this.result,
required this.status,
});
factory AbiRequestResponse.fromJson(Map<String, dynamic> json) {
return AbiRequestResponse(
message: json['message'] as String,
result: json['result'] as String,
status: json['status'] as String,
);
}
}
class EthereumResponse<T> {
final T? value;
final Exception? exception;
EthereumResponse(this.value, this.exception);
}
abstract class EthereumAPI {
static const blockExplorer = "https://blockscout.com/eth/mainnet/api";
static const abiUrl =
"https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update
static const gasTrackerUrl =
"https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle";
static Future<AddressTransaction> fetchAddressTransactions(
String address, String action) async {
try {
final response = await get(Uri.parse(
"$blockExplorer?module=account&action=$action&address=$address"));
if (response.statusCode == 200) {
return AddressTransaction.fromJson(
jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception(
'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}');
}
} catch (e, s) {
throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}');
}
}
static Future<EthereumResponse<List<EthToken>>> getWalletTokens({
required String address,
}) async {
try {
final uri = Uri.parse(
"$blockExplorer?module=account&action=tokenlist&address=$address",
);
final response = await get(uri);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json["message"] == "OK") {
final result =
List<Map<String, dynamic>>.from(json["result"] as List);
final List<EthToken> tokens = [];
for (final map in result) {
if (map["type"] == "ERC-20") {
tokens.add(
Erc20Token(
balance: int.parse(map["balance"] as String),
contractAddress: map["contractAddress"] as String,
decimals: int.parse(map["decimals"] as String),
name: map["name"] as String,
symbol: map["symbol"] as String,
),
);
} else if (map["type"] == "ERC-721") {
tokens.add(
Erc721Token(
balance: int.parse(map["balance"] as String),
contractAddress: map["contractAddress"] as String,
decimals: int.parse(map["decimals"] as String),
name: map["name"] as String,
symbol: map["symbol"] as String,
),
);
} else {
throw Exception("Unsupported token type found: ${map["type"]}");
}
}
return EthereumResponse(
tokens,
null,
);
} else {
throw Exception(json["message"] as String);
}
} else {
throw Exception(
"getWalletTokens($address) failed with status code: "
"${response.statusCode}",
);
}
} catch (e, s) {
Logging.instance.log(
"getWalletTokens(): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(
null,
Exception(e.toString()),
);
}
}
static Future<List<dynamic>> getWalletTokenTransactions(
String address) async {
AddressTransaction tokens =
await fetchAddressTransactions(address, "tokentx");
List<dynamic> tokensList = [];
var tokenMap = {};
if (tokens.message == "OK") {
final allTxs = tokens.result;
allTxs.forEach((element) {
print("=========================================================");
print("THING: $element");
print("=========================================================");
String key = element["tokenSymbol"] as String;
tokenMap[key] = {};
tokenMap[key]["balance"] = 0;
if (tokenMap.containsKey(key)) {
tokenMap[key]["contractAddress"] =
element["contractAddress"] as String;
tokenMap[key]["decimals"] = element["tokenDecimal"];
tokenMap[key]["name"] = element["tokenName"];
tokenMap[key]["symbol"] = element["tokenSymbol"];
if (checksumEthereumAddress(address) == address) {
tokenMap[key]["balance"] += int.parse(element["value"] as String);
} else {
tokenMap[key]["balance"] -= int.parse(element["value"] as String);
}
}
});
tokenMap.forEach((key, value) {
tokensList.add(value as Map<dynamic, dynamic>);
});
return tokensList;
}
return <dynamic>[];
}
static Future<GasTracker> getGasOracle() async {
final response = await get(Uri.parse(gasTrackerUrl));
if (response.statusCode == 200) {
return GasTracker.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to load gas oracle');
}
}
static Future<FeeObject> getFees() async {
GasTracker fees = await getGasOracle();
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: feesFast.toInt(),
medium: feesStandard.toInt(),
slow: feesSlow.toInt());
}
//Validate that a custom token is valid and is ERC-20, a token will be valid
static Future<EthereumResponse<EthToken>> getTokenByContractAddress(
String contractAddress) async {
try {
final response = await get(Uri.parse(
"$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress"));
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json["message"] == "OK") {
final map = Map<String, dynamic>.from(json["result"] as Map);
EthToken? token;
if (map["type"] == "ERC-20") {
token = Erc20Token(
balance: int.parse(map["balance"] as String),
contractAddress: map["contractAddress"] as String,
decimals: int.parse(map["decimals"] as String),
name: map["name"] as String,
symbol: map["symbol"] as String,
);
} else if (map["type"] == "ERC-721") {
token = Erc721Token(
balance: int.parse(map["balance"] as String),
contractAddress: map["contractAddress"] as String,
decimals: int.parse(map["decimals"] as String),
name: map["name"] as String,
symbol: map["symbol"] as String,
);
} else {
throw Exception("Unsupported token type found: ${map["type"]}");
}
return EthereumResponse(
token,
null,
);
} else {
throw Exception(json["message"] as String);
}
} else {
throw Exception(
"getTokenByContractAddress($contractAddress) failed with status code: "
"${response.statusCode}",
);
}
} catch (e, s) {
Logging.instance.log(
"getWalletTokens(): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(
null,
Exception(e.toString()),
);
}
}
static Future<AbiRequestResponse> fetchTokenAbi(
String contractAddress) async {
final response = await get(Uri.parse(
"$abiUrl?module=contract&action=getabi&address=$contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
if (response.statusCode == 200) {
return AbiRequestResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}");
}
}
}

View file

@ -1,16 +1,15 @@
import 'dart:convert';
import 'dart:math';
import 'package:decimal/decimal.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:http/http.dart';
import 'package:stackwallet/models/ethereum/eth_token.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/tokens/token_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/constants.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';
@ -20,55 +19,15 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web3dart/web3dart.dart' as transaction;
class AbiRequestResponse {
final String message;
final String result;
final String status;
const AbiRequestResponse({
required this.message,
required this.result,
required this.status,
});
factory AbiRequestResponse.fromJson(Map<String, dynamic> json) {
return AbiRequestResponse(
message: json['message'] as String,
result: json['result'] as String,
status: json['status'] as String,
);
}
}
class TokenData {
final String message;
final Map<String, dynamic> result;
final String status;
const TokenData({
required this.message,
required this.result,
required this.status,
});
factory TokenData.fromJson(Map<String, dynamic> json) {
return TokenData(
message: json['message'] as String,
result: json['result'] as Map<String, dynamic>,
status: json['status'] as String,
);
}
}
const int MINIMUM_CONFIRMATIONS = 3;
class EthereumToken extends TokenServiceAPI {
@override
class EthereumTokenService {
final EthToken token;
late bool shouldAutoSync;
late EthereumAddress _contractAddress;
late EthPrivateKey _credentials;
late DeployedContract _contract;
late Map<dynamic, dynamic> _tokenData;
late ContractFunction _balanceFunction;
late ContractFunction _sendFunction;
late Future<List<String>> _walletMnemonic;
@ -80,30 +39,16 @@ class EthereumToken extends TokenServiceAPI {
final _gasLimit = 200000;
EthereumToken({
required Map<dynamic, dynamic> tokenData,
EthereumTokenService({
required this.token,
required Future<List<String>> walletMnemonic,
required SecureStorageInterface secureStore,
}) {
_contractAddress =
EthereumAddress.fromHex(tokenData["contractAddress"] as String);
_contractAddress = EthereumAddress.fromHex(token.contractAddress);
_walletMnemonic = walletMnemonic;
_tokenData = tokenData;
_secureStore = secureStore;
}
Future<AbiRequestResponse> fetchTokenAbi() async {
final response = await get(Uri.parse(
"$abiUrl?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
if (response.statusCode == 200) {
return AbiRequestResponse.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception("ERROR GETTING TOKENABI ${response.reasonPhrase}");
}
}
@override
Future<List<String>> get allOwnAddresses =>
_allOwnAddresses ??= _fetchAllOwnAddresses();
Future<List<String>>? _allOwnAddresses;
@ -115,21 +60,18 @@ class EthereumToken extends TokenServiceAPI {
return addresses;
}
@override
Future<Decimal> get availableBalance async {
return await totalBalance;
}
@override
Coin get coin => Coin.ethereum;
@override
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
final amount = txData['recipientAmt'];
final decimalAmount =
Format.satoshisToAmount(amount as int, coin: Coin.ethereum);
final bigIntAmount = amountToBigInt(
decimalAmount.toDouble(), int.parse(_tokenData["decimals"] as String));
final bigIntAmount =
amountToBigInt(decimalAmount.toDouble(), token.decimals);
final sentTx = await _client.sendTransaction(
_credentials,
@ -147,7 +89,6 @@ class EthereumToken extends TokenServiceAPI {
return sentTx;
}
@override
Future<String> get currentReceivingAddress async {
final _currentReceivingAddress = await _credentials.extractAddress();
final checkSumAddress =
@ -155,40 +96,21 @@ class EthereumToken extends TokenServiceAPI {
return checkSumAddress;
}
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
final fee = estimateFee(
feeRate, _gasLimit, int.parse(_tokenData["decimals"] as String));
final fee = estimateFee(feeRate, _gasLimit, token.decimals);
return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin);
}
@override
Future<FeeObject> get fees => _feeObject ??= _getFees();
Future<FeeObject>? _feeObject;
Future<FeeObject> _getFees() async {
return await getFees();
return await EthereumAPI.getFees();
}
@override
Future<void> initializeExisting() async {
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(' ');
@ -199,8 +121,7 @@ class EthereumToken extends TokenServiceAPI {
_credentials = EthPrivateKey.fromHex(privateKey);
_contract = DeployedContract(
ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String),
_contractAddress);
ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress);
_balanceFunction = _contract.function('balanceOf');
_sendFunction = _contract.function('transfer');
_client = await getEthClient();
@ -208,15 +129,15 @@ class EthereumToken extends TokenServiceAPI {
// print(_credentials.p)
}
@override
Future<void> initializeNew() async {
AbiRequestResponse abi = await fetchTokenAbi();
AbiRequestResponse abi =
await EthereumAPI.fetchTokenAbi(_contractAddress.hex);
//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);
key: '${_contractAddress.hex}_tokenAbi', value: _tokenAbi);
} else {
throw Exception('Failed to load token abi');
}
@ -230,25 +151,21 @@ class EthereumToken extends TokenServiceAPI {
_credentials = EthPrivateKey.fromHex(privateKey);
_contract = DeployedContract(
ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String),
_contractAddress);
ContractAbi.fromJson(_tokenAbi, token.name), _contractAddress);
_balanceFunction = _contract.function('balanceOf');
_sendFunction = _contract.function('transfer');
_client = await getEthClient();
}
@override
// TODO: implement isRefreshing
bool get isRefreshing => throw UnimplementedError();
@override
Future<int> get maxFee async {
final fee = (await fees).fast;
final feeEstimate = await estimateFeeFor(0, fee);
return feeEstimate;
}
@override
Future<Map<String, dynamic>> prepareSend(
{required String address,
required int satoshiAmount,
@ -292,13 +209,11 @@ class EthereumToken extends TokenServiceAPI {
return txData;
}
@override
Future<void> refresh() {
// TODO: implement refresh
throw UnimplementedError();
}
@override
Future<Decimal> get totalBalance async {
final balanceRequest = await _client.call(
contract: _contract,
@ -306,40 +221,35 @@ class EthereumToken extends TokenServiceAPI {
params: [_credentials.address]);
String balance = balanceRequest.first.toString();
int tokenDecimals = int.parse(_tokenData["decimals"] as String);
final balanceInDecimal = (int.parse(balance) / (pow(10, tokenDecimals)));
final balanceInDecimal = Format.satoshisToEthTokenAmount(
int.parse(balance),
token.decimals,
);
return Decimal.parse(balanceInDecimal.toString());
}
@override
Future<TransactionData> get transactionData =>
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
Future<TransactionData> _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<Map<String, dynamic>> midSortedArray = [];
AddressTransaction txs =
await fetchAddressTransactions(thisAddress, "tokentx");
await EthereumAPI.fetchAddressTransactions(thisAddress, "tokentx");
if (txs.message == "OK") {
final allTxs = txs.result;
allTxs.forEach((element) {
for (var element in allTxs) {
Map<String, dynamic> 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
int decimal = token.decimals; //Eth has up to 18 decimal places
final transactionAmountInDecimal =
transactionAmount / (pow(10, decimal));
@ -360,18 +270,12 @@ class EthereumToken extends TokenServiceAPI {
}
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"] = <dynamic>[];
midSortedTx["fees"] = Format.decimalAmountToSatoshis(
Decimal.parse(txFeeDecimal.toString()), coin);
@ -383,7 +287,7 @@ class EthereumToken extends TokenServiceAPI {
midSortedTx["height"] = int.parse(element['blockNumber'].toString());
midSortedArray.add(midSortedTx);
});
}
}
midSortedArray.sort((a, b) =>
@ -428,39 +332,10 @@ class EthereumToken extends TokenServiceAPI {
return txModel;
}
@override
bool validateAddress(String address) {
return isValidEthereumAddress(address);
}
//Validate that a custom token is valid and is ERC-20, a token will be valid
@override
Future<TokenData> getTokenByContractAddress(String contractAddress) async {
final response = await get(Uri.parse(
"$blockExplorer?module=token&action=getToken&contractaddress=$contractAddress"));
if (response.statusCode == 200) {
return TokenData.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception("ERROR GETTING TOKEN ${response.reasonPhrase}");
}
}
//Validate that a custom token is valid and is ERC-20
@override
Future<bool> isValidToken(String contractAddress) async {
TokenData tokenData = await getTokenByContractAddress(contractAddress);
if (tokenData.message == "OK") {
final result = tokenData.result;
if (result["type"] == "ERC-20") {
return true;
}
return false;
}
return false;
}
Future<NodeModel> getCurrentNode() async {
return NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??

View file

@ -1,62 +0,0 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/prefs.dart';
abstract class TokenServiceAPI {
TokenServiceAPI();
factory TokenServiceAPI.from(
Map<dynamic, dynamic> tokenData,
Future<List<String>> walletMnemonic,
SecureStorageInterface secureStorageInterface,
TransactionNotificationTracker tracker,
Prefs prefs,
) {
return EthereumToken(
tokenData: tokenData,
walletMnemonic: walletMnemonic,
secureStore: secureStorageInterface,
// tracker: tracker,
);
}
Coin get coin;
bool get isRefreshing;
bool get shouldAutoSync;
set shouldAutoSync(bool shouldAutoSync);
Future<Map<String, dynamic>> prepareSend({
required String address,
required int satoshiAmount,
Map<String, dynamic>? args,
});
Future<String> confirmSend({required Map<String, dynamic> txData});
Future<FeeObject> get fees;
Future<int> get maxFee;
Future<String> get currentReceivingAddress;
Future<Decimal> get availableBalance;
Future<Decimal> get totalBalance;
Future<List<String>> get allOwnAddresses;
Future<TransactionData> get transactionData;
Future<void> refresh();
bool validateAddress(String address);
Future<void> initializeNew();
Future<void> initializeExisting();
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
Future<bool> isValidToken(String contractAddress);
}

View file

@ -1,12 +1,8 @@
import 'dart:convert';
import 'dart:math';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:ethereum_addresses/ethereum_addresses.dart';
import "package:hex/hex.dart";
import 'package:http/http.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
class AddressTransaction {
final String message;
@ -58,62 +54,7 @@ class GasTracker {
}
}
const blockExplorer = "https://blockscout.com/eth/mainnet/api";
const abiUrl =
"https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update
const hdPathEthereum = "m/44'/60'/0'/0";
const _gasTrackerUrl =
"https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle";
Future<AddressTransaction> fetchAddressTransactions(
String address, String action) async {
try {
final response = await get(Uri.parse(
"$blockExplorer?module=account&action=$action&address=$address"));
if (response.statusCode == 200) {
return AddressTransaction.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception(
'ERROR GETTING TRANSACTIONS WITH STATUS ${response.statusCode}');
}
} catch (e, s) {
throw Exception('ERROR GETTING TRANSACTIONS ${e.toString()}');
}
}
Future<List<dynamic>> getWalletTokens(String address) async {
AddressTransaction tokens =
await fetchAddressTransactions(address, "tokentx");
List<dynamic> tokensList = [];
var tokenMap = {};
if (tokens.message == "OK") {
final allTxs = tokens.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"] as String;
tokenMap[key]["decimals"] = element["tokenDecimal"];
tokenMap[key]["name"] = element["tokenName"];
tokenMap[key]["symbol"] = element["tokenSymbol"];
if (checksumEthereumAddress(address) == address) {
tokenMap[key]["balance"] += int.parse(element["value"] as String);
} else {
tokenMap[key]["balance"] -= int.parse(element["value"] as String);
}
}
});
tokenMap.forEach((key, value) {
tokensList.add(value as Map<dynamic, dynamic>);
});
return tokensList;
}
return <dynamic>[];
}
String getPrivateKey(String mnemonic, String mnemonicPassphrase) {
final isValidMnemonic = bip39.validateMnemonic(mnemonic);
@ -129,31 +70,6 @@ String getPrivateKey(String mnemonic, String mnemonicPassphrase) {
return HEX.encode(addressAtIndex.privateKey as List<int>);
}
Future<GasTracker> getGasOracle() async {
final response = await get(Uri.parse(_gasTrackerUrl));
if (response.statusCode == 200) {
return GasTracker.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to load gas oracle');
}
}
Future<FeeObject> getFees() async {
GasTracker fees = await getGasOracle();
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: feesFast.toInt(),
medium: feesStandard.toInt(),
slow: feesSlow.toInt());
}
double estimateFee(int feeRate, int gasLimit, int decimals) {
final gweiAmount = feeRate / (pow(10, 9));
final fee = gasLimit * gweiAmount;

View file

@ -1,3 +1,4 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:decimal/decimal.dart';
@ -19,6 +20,12 @@ abstract class Format {
scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin));
}
static Decimal satoshisToEthTokenAmount(int sats, int decimalPlaces) {
return (Decimal.fromInt(sats) /
Decimal.fromInt(pow(10, decimalPlaces).toInt()))
.toDecimal(scaleOnInfinitePrecision: decimalPlaces);
}
///
static String satoshiAmountToPrettyString(
int sats, String locale, Coin coin) {

View file

@ -0,0 +1,43 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
Future<T> showLoading<T>({
required Future<T> whileFuture,
required BuildContext context,
required String message,
String? subMessage,
bool isDesktop = false,
}) async {
unawaited(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Container(
color: Theme.of(context)
.extension<StackColors>()!
.overlay
.withOpacity(0.6),
child: CustomLoadingOverlay(
message: message,
subMessage: subMessage,
eventBus: null,
),
),
),
),
);
final result = await whileFuture;
// TODO: update to flutter 3.7.x to take advantage of context.mounted
// if (mounted) {
Navigator.of(context, rootNavigator: isDesktop).pop();
// }
return result;
}