mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 09:47:37 +00:00
WIP eth refactor
This commit is contained in:
parent
1653bb2096
commit
5aed55235c
17 changed files with 470 additions and 374 deletions
11
lib/models/ethereum/erc20_token.dart
Normal file
11
lib/models/ethereum/erc20_token.dart
Normal 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,
|
||||
});
|
||||
}
|
11
lib/models/ethereum/erc721_token.dart
Normal file
11
lib/models/ethereum/erc721_token.dart
Normal 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,
|
||||
});
|
||||
}
|
15
lib/models/ethereum/eth_token.dart
Normal file
15
lib/models/ethereum/eth_token.dart
Normal 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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
263
lib/services/ethereum/ethereum_api.dart
Normal file
263
lib/services/ethereum/ethereum_api.dart
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) ??
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
43
lib/utilities/show_loading.dart
Normal file
43
lib/utilities/show_loading.dart
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue