WIP: Add test ETH Token functionality in stack

This commit is contained in:
likho 2023-01-26 20:08:12 +02:00
parent d4653ea794
commit dbcbfe342c
13 changed files with 861 additions and 1144 deletions

View file

@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/token_view/token_view.dart';
import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/providers/global/tokens_provider.dart';
import 'package:stackwallet/services/tokens/ethereum/ethereum_token.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -57,13 +55,15 @@ class MyTokenSelectItem extends ConsumerWidget {
final mnemonicList = ref.read(managerProvider).mnemonic;
final token = EthereumToken(
contractAddress: tokenData["contractAddress"] as String,
// contractAddress: tokenData["contractAddress"] as String,
tokenData: tokenData,
walletMnemonic: mnemonicList);
Navigator.of(context).pushNamed(
TokenView.routeName,
arguments: Tuple3(
arguments: Tuple4(
walletId,
tokenData,
ref
.read(walletsChangeNotifierProvider)
.getManagerProvider(walletId),

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/token_summary_info.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary_info.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class TokenSummary extends StatelessWidget {
const TokenSummary({
Key? key,
required this.walletId,
required this.managerProvider,
required this.initialSyncStatus,
this.aspectRatio = 2.0,
this.minHeight = 100.0,
this.minWidth = 200.0,
this.maxHeight = 250.0,
this.maxWidth = 400.0,
}) : super(key: key);
final String walletId;
final ChangeNotifierProvider<Manager> managerProvider;
final WalletSyncStatus initialSyncStatus;
final double aspectRatio;
final double minHeight;
final double minWidth;
final double maxHeight;
final double maxWidth;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: aspectRatio,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: minHeight,
minWidth: minWidth,
maxHeight: maxHeight,
maxWidth: minWidth,
),
child: Stack(
children: [
Consumer(
builder: (_, ref, __) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.colorForCoin(ref.watch(
managerProvider.select((value) => value.coin))),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
);
},
),
Positioned.fill(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Spacer(
flex: 5,
),
Expanded(
flex: 6,
child: SvgPicture.asset(
Assets.svg.ellipse1,
// fit: BoxFit.fitWidth,
// clipBehavior: Clip.none,
),
),
const SizedBox(
width: 25,
),
],
),
),
// Positioned.fill(
// child:
// Column(
// mainAxisAlignment: MainAxisAlignment.end,
// children: [
Align(
alignment: Alignment.bottomCenter,
child: Row(
children: [
const Spacer(
flex: 1,
),
Expanded(
flex: 3,
child: SvgPicture.asset(
Assets.svg.ellipse2,
// fit: BoxFit.f,
// clipBehavior: Clip.none,
),
),
const SizedBox(
width: 13,
),
],
),
),
// ],
// ),
// ),
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: TokenSummaryInfo(
walletId: walletId,
managerProvider: managerProvider,
initialSyncStatus: initialSyncStatus,
),
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,298 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart';
import 'package:stackwallet/providers/providers.dart';
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/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_text.dart';
class TokenSummaryInfo extends StatefulWidget {
const TokenSummaryInfo({
Key? key,
required this.walletId,
required this.managerProvider,
required this.initialSyncStatus,
}) : super(key: key);
final String walletId;
final ChangeNotifierProvider<Manager> managerProvider;
final WalletSyncStatus initialSyncStatus;
@override
State<TokenSummaryInfo> createState() => _TokenSummaryInfoState();
}
class _TokenSummaryInfoState extends State<TokenSummaryInfo> {
late final String walletId;
late final ChangeNotifierProvider<Manager> managerProvider;
void showSheet() {
showModalBottomSheet<dynamic>(
backgroundColor: Colors.transparent,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (_) => WalletBalanceToggleSheet(walletId: walletId),
);
}
Decimal? _balanceTotalCached;
Decimal? _balanceCached;
@override
void initState() {
walletId = widget.walletId;
managerProvider = widget.managerProvider;
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
return Row(
children: [
Expanded(
child: Consumer(
builder: (_, ref, __) {
final Coin coin =
ref.watch(managerProvider.select((value) => value.coin));
final externalCalls = ref.watch(prefsChangeNotifierProvider
.select((value) => value.externalCalls));
Future<Decimal>? totalBalanceFuture;
Future<Decimal>? availableBalanceFuture;
if (coin == Coin.firo || coin == Coin.firoTestNet) {
final firoWallet =
ref.watch(managerProvider.select((value) => value.wallet))
as FiroWallet;
totalBalanceFuture = firoWallet.availablePublicBalance();
availableBalanceFuture = firoWallet.availablePrivateBalance();
} else {
totalBalanceFuture = ref.watch(
managerProvider.select((value) => value.totalBalance));
availableBalanceFuture = ref.watch(
managerProvider.select((value) => value.availableBalance));
}
final locale = ref.watch(localeServiceChangeNotifierProvider
.select((value) => value.locale));
final baseCurrency = ref.watch(prefsChangeNotifierProvider
.select((value) => value.currency));
final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider
.select((value) => value.getPrice(coin)));
final _showAvailable =
ref.watch(walletBalanceToggleStateProvider.state).state ==
WalletBalanceToggleState.available;
return FutureBuilder(
future: _showAvailable
? availableBalanceFuture
: totalBalanceFuture,
builder: (fbContext, AsyncSnapshot<Decimal> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData &&
snapshot.data != null) {
if (_showAvailable) {
_balanceCached = snapshot.data!;
} else {
_balanceTotalCached = snapshot.data!;
}
}
Decimal? balanceToShow =
_showAvailable ? _balanceCached : _balanceTotalCached;
if (balanceToShow != null) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
if (coin == Coin.firo || coin == Coin.firoTestNet)
Text(
"${_showAvailable ? "Private" : "Public"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
Text(
"${_showAvailable ? "Available" : "Full"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
width: 8,
height: 4,
),
],
),
),
const Spacer(),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"${Format.localizedStringAsFixed(
value: balanceToShow,
locale: locale,
decimalPlaces: 8,
)} ${coin.ticker}",
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
),
if (externalCalls)
Text(
"${Format.localizedStringAsFixed(
value: priceTuple.item1 * balanceToShow,
locale: locale,
decimalPlaces: 2,
)} $baseCurrency",
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: showSheet,
child: Row(
children: [
if (coin == Coin.firo || coin == Coin.firoTestNet)
Text(
"${_showAvailable ? "Private" : "Public"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
if (coin != Coin.firo && coin != Coin.firoTestNet)
Text(
"${_showAvailable ? "Available" : "Full"} Balance",
style:
STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
const SizedBox(
width: 4,
),
SvgPicture.asset(
Assets.svg.chevronDown,
width: 8,
height: 4,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
],
),
),
const Spacer(),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.pageTitleH1(context).copyWith(
fontSize: 24,
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
AnimatedText(
stringsToLoopThrough: const [
"Loading balance",
"Loading balance.",
"Loading balance..",
"Loading balance..."
],
style: STextStyles.subtitle500(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFavoriteCard,
),
),
],
);
}
},
);
},
),
),
Column(
children: [
Consumer(
builder: (_, ref, __) {
return SvgPicture.asset(
Assets.svg.iconFor(
coin: ref.watch(
managerProvider.select((value) => value.coin),
),
),
width: 24,
height: 24,
);
},
),
const Spacer(),
WalletRefreshButton(
walletId: walletId,
initialSyncStatus: widget.initialSyncStatus,
),
],
)
],
);
}
}

View file

@ -15,6 +15,7 @@ import 'package:stackwallet/pages/send_view/send_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart';
import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart';
@ -25,7 +26,6 @@ import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/providers/ui/unread_notifications_provider.dart';
import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
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/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@ -55,6 +55,7 @@ class TokenView extends ConsumerStatefulWidget {
const TokenView({
Key? key,
required this.walletId,
required this.tokenData,
required this.managerProvider,
required this.token,
this.eventBus,
@ -64,6 +65,7 @@ class TokenView extends ConsumerStatefulWidget {
static const double navBarHeight = 65.0;
final String walletId;
final Map<dynamic, dynamic> tokenData;
final ChangeNotifierProvider<Manager> managerProvider;
final EthereumToken token;
final EventBus? eventBus;
@ -209,52 +211,11 @@ class _TokenViewState extends ConsumerState<TokenView> {
}
}
Widget _buildNetworkIcon(WalletSyncStatus status) {
switch (status) {
case WalletSyncStatus.unableToSync:
return SvgPicture.asset(
Assets.svg.radioProblem,
color: Theme.of(context).extension<StackColors>()!.accentColorRed,
width: 20,
height: 20,
);
case WalletSyncStatus.synced:
return SvgPicture.asset(
Assets.svg.radio,
color: Theme.of(context).extension<StackColors>()!.accentColorGreen,
width: 20,
height: 20,
);
case WalletSyncStatus.syncing:
return SvgPicture.asset(
Assets.svg.radioSyncing,
color: Theme.of(context).extension<StackColors>()!.accentColorYellow,
width: 20,
height: 20,
);
}
}
void _onExchangePressed(BuildContext context) async {
unawaited(_cnLoadingService.loadAll(ref));
final coin = ref.read(managerProvider).coin;
if (coin == Coin.epicCash) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Exchange not available for Epic Cash",
),
);
} else if (coin.name.endsWith("TestNet")) {
await showDialog<void>(
context: context,
builder: (_) => const StackOkDialog(
title: "Exchange not available for test net coins",
),
);
} else {
ref.read(currentExchangeNameStateProvider.state).state =
ChangeNowExchange.exchangeName;
final walletId = ref.read(managerProvider).walletId;
@ -279,8 +240,7 @@ class _TokenViewState extends ConsumerState<TokenView> {
.currencies
.firstWhere(
(element) =>
element.ticker.toLowerCase() !=
coin.ticker.toLowerCase(),
element.ticker.toLowerCase() != coin.ticker.toLowerCase(),
),
);
}
@ -298,73 +258,6 @@ class _TokenViewState extends ConsumerState<TokenView> {
);
}
}
}
Future<void> attemptAnonymize() async {
bool shouldPop = false;
unawaited(
showDialog(
context: context,
builder: (context) => WillPopScope(
child: const CustomLoadingOverlay(
message: "Anonymizing balance",
eventBus: null,
),
onWillPop: () async => shouldPop,
),
),
);
final firoWallet = ref.read(managerProvider).wallet as FiroWallet;
final publicBalance = await firoWallet.availablePublicBalance();
if (publicBalance <= Decimal.zero) {
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(TokenView.routeName),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "No funds available to anonymize!",
context: context,
),
);
}
return;
}
try {
await firoWallet.anonymizeAllPublicFunds();
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(TokenView.routeName),
);
unawaited(
showFloatingFlushBar(
type: FlushBarType.success,
message: "Anonymize transaction submitted",
context: context,
),
);
}
} catch (e) {
shouldPop = true;
if (mounted) {
Navigator.of(context).popUntil(
ModalRoute.withName(TokenView.routeName),
);
await showDialog<dynamic>(
context: context,
builder: (_) => StackOkDialog(
title: "Anonymize all failed",
message: "Reason: $e",
),
);
}
}
}
void _loadCNData() {
// unawaited future
@ -379,6 +272,8 @@ class _TokenViewState extends ConsumerState<TokenView> {
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
widget.token.initializeExisting();
// print("MY TOTAL BALANCE IS ${widget.token.totalBalance}");
final coin = ref.watch(managerProvider.select((value) => value.coin));
@ -407,148 +302,23 @@ class _TokenViewState extends ConsumerState<TokenView> {
const SizedBox(
width: 16,
),
Expanded(
child: Text(
widget.tokenData["name"] as String,
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
),
Expanded(
child: Text(
ref.watch(
managerProvider.select((value) => value.walletName)),
managerProvider.select((value) => value.coin.ticker)),
style: STextStyles.navBarTitle(context),
overflow: TextOverflow.ellipsis,
),
)
],
),
actions: [
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("TokenViewRadioButton"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: _buildNetworkIcon(_currentSyncStatus),
onPressed: () {
Navigator.of(context).pushNamed(
WalletNetworkSettingsView.routeName,
arguments: Tuple3(
walletId,
_currentSyncStatus,
_currentNodeStatus,
),
);
},
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("TokenViewAlertsButton"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
ref.watch(notificationsProvider.select((value) =>
value.hasUnreadNotificationsFor(walletId)))
? Assets.svg.bellNew(context)
: Assets.svg.bell,
width: 20,
height: 20,
color: ref.watch(notificationsProvider.select((value) =>
value.hasUnreadNotificationsFor(walletId)))
? null
: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
// reset unread state
ref.refresh(unreadNotificationsStateProvider);
Navigator.of(context)
.pushNamed(
NotificationsView.routeName,
arguments: walletId,
)
.then((_) {
final Set<int> unreadNotificationIds = ref
.read(unreadNotificationsStateProvider.state)
.state;
if (unreadNotificationIds.isEmpty) return;
List<Future<dynamic>> futures = [];
for (int i = 0;
i < unreadNotificationIds.length - 1;
i++) {
futures.add(ref
.read(notificationsProvider)
.markAsRead(
unreadNotificationIds.elementAt(i), false));
}
// wait for multiple to update if any
Future.wait(futures).then((_) {
// only notify listeners once
ref
.read(notificationsProvider)
.markAsRead(unreadNotificationIds.last, true);
});
});
},
),
),
),
Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
key: const Key("TokenViewSettingsButton"),
size: 36,
shadows: const [],
color:
Theme.of(context).extension<StackColors>()!.background,
icon: SvgPicture.asset(
Assets.svg.bars,
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
width: 20,
height: 20,
),
onPressed: () {
debugPrint("wallet view settings tapped");
Navigator.of(context).pushNamed(
WalletSettingsView.routeName,
arguments: Tuple4(
walletId,
ref.read(managerProvider).coin,
_currentSyncStatus,
_currentNodeStatus,
),
);
},
),
),
),
],
),
body: SafeArea(
child: Container(
@ -561,7 +331,7 @@ class _TokenViewState extends ConsumerState<TokenView> {
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WalletSummary(
child: TokenSummary(
walletId: walletId,
managerProvider: managerProvider,
initialSyncStatus: ref.watch(managerProvider
@ -571,72 +341,6 @@ class _TokenViewState extends ConsumerState<TokenView> {
),
),
),
if (coin == Coin.firo)
const SizedBox(
height: 10,
),
if (coin == Coin.firo)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonColor(context),
onPressed: () async {
await showDialog<void>(
context: context,
builder: (context) => StackDialog(
title: "Attention!",
message:
"You're about to anonymize all of your public funds.",
leftButton: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
rightButton: TextButton(
onPressed: () async {
Navigator.of(context).pop();
unawaited(attemptAnonymize());
},
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonColor(
context),
child: Text(
"Continue",
style: STextStyles.button(context),
),
),
),
);
},
child: Text(
"Anonymize funds",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
),
),
),
],
),
),
const SizedBox(
height: 20,
),
@ -715,91 +419,91 @@ class _TokenViewState extends ConsumerState<TokenView> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
bottom: 14,
left: 16,
right: 16,
),
child: SizedBox(
height: TokenView.navBarHeight,
child: WalletNavigationBar(
enableExchange:
Constants.enableExchange &&
ref.watch(managerProvider.select(
(value) => value.coin)) !=
Coin.epicCash,
height: TokenView.navBarHeight,
onExchangePressed: () =>
_onExchangePressed(context),
onReceivePressed: () async {
final coin =
ref.read(managerProvider).coin;
if (mounted) {
unawaited(
Navigator.of(context).pushNamed(
ReceiveView.routeName,
arguments: Tuple2(
walletId,
coin,
),
));
}
},
onSendPressed: () {
final walletId =
ref.read(managerProvider).walletId;
final coin =
ref.read(managerProvider).coin;
switch (ref
.read(
walletBalanceToggleStateProvider
.state)
.state) {
case WalletBalanceToggleState.full:
ref
.read(
publicPrivateBalanceStateProvider
.state)
.state = "Public";
break;
case WalletBalanceToggleState
.available:
ref
.read(
publicPrivateBalanceStateProvider
.state)
.state = "Private";
break;
}
Navigator.of(context).pushNamed(
SendView.routeName,
arguments: Tuple2(
walletId,
coin,
),
);
},
onBuyPressed: () {},
onTokensPressed: () async {
final walletAddress = await ref
.read(managerProvider)
.currentReceivingAddress;
List<dynamic> tokens =
await getWalletTokens(await ref
.read(managerProvider)
.currentReceivingAddress);
await Navigator.of(context).pushNamed(
MyTokensView.routeName,
arguments: Tuple4(managerProvider,
walletId, walletAddress, tokens),
);
},
),
),
),
// Padding(
// padding: const EdgeInsets.only(
// bottom: 14,
// left: 16,
// right: 16,
// ),
// child: SizedBox(
// height: TokenView.navBarHeight,
// child: WalletNavigationBar(
// enableExchange:
// Constants.enableExchange &&
// ref.watch(managerProvider.select(
// (value) => value.coin)) !=
// Coin.epicCash,
// height: TokenView.navBarHeight,
// onExchangePressed: () =>
// _onExchangePressed(context),
// onReceivePressed: () async {
// final coin =
// ref.read(managerProvider).coin;
// if (mounted) {
// unawaited(
// Navigator.of(context).pushNamed(
// ReceiveView.routeName,
// arguments: Tuple2(
// walletId,
// coin,
// ),
// ));
// }
// },
// onSendPressed: () {
// final walletId =
// ref.read(managerProvider).walletId;
// final coin =
// ref.read(managerProvider).coin;
// switch (ref
// .read(
// walletBalanceToggleStateProvider
// .state)
// .state) {
// case WalletBalanceToggleState.full:
// ref
// .read(
// publicPrivateBalanceStateProvider
// .state)
// .state = "Public";
// break;
// case WalletBalanceToggleState
// .available:
// ref
// .read(
// publicPrivateBalanceStateProvider
// .state)
// .state = "Private";
// break;
// }
// Navigator.of(context).pushNamed(
// SendView.routeName,
// arguments: Tuple2(
// walletId,
// coin,
// ),
// );
// },
// onBuyPressed: () {},
// onTokensPressed: () async {
// final walletAddress = await ref
// .read(managerProvider)
// .currentReceivingAddress;
//
// List<dynamic> tokens =
// await getWalletTokens(await ref
// .read(managerProvider)
// .currentReceivingAddress);
//
// await Navigator.of(context).pushNamed(
// MyTokensView.routeName,
// arguments: Tuple4(managerProvider,
// walletId, walletAddress, tokens),
// );
// },
// ),
// ),
// ),
],
),
],

View file

@ -1,23 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/node_service_provider.dart';
import 'package:stackwallet/providers/global/tokens_service_provider.dart';
import 'package:stackwallet/providers/global/wallets_service_provider.dart';
import 'package:stackwallet/services/tokens.dart';
import 'package:stackwallet/services/wallets.dart';
int _count = 0;
final tokensChangeNotifierProvider = ChangeNotifierProvider<Tokens>((ref) {
if (kDebugMode) {
_count++;
debugPrint("tokensChangeNotifierProvider instantiation count: $_count");
}
final tokensService = ref.read(tokensServiceChangeNotifierProvider);
// final nodeService = ref.read(nodeServiceChangeNotifierProvider);
final tokens = Tokens.sharedInstance;
tokens.tokensService = tokensService;
return tokens;
});

View file

@ -1,20 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/services/tokens_service.dart';
import 'package:stackwallet/services/wallets_service.dart';
int _count = 0;
final tokensServiceChangeNotifierProvider =
ChangeNotifierProvider<TokensService>((ref) {
if (kDebugMode) {
_count++;
debugPrint(
"tokensServiceChangeNotifierProvider instantiation count: $_count");
}
return TokensService(
secureStorageInterface: ref.read(secureStoreProvider),
);
});

View file

@ -118,7 +118,6 @@ import 'package:stackwallet/services/coins/manager.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/services/tokens/token_manager.dart';
import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:tuple/tuple.dart';
@ -1328,14 +1327,15 @@ class RouteGenerator {
// }
case TokenView.routeName:
if (args
is Tuple3<String, ChangeNotifierProvider<Manager>, EthereumToken>) {
if (args is Tuple4<String, Map<dynamic, dynamic>,
ChangeNotifierProvider<Manager>, EthereumToken>) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => TokenView(
walletId: args.item1,
managerProvider: args.item2,
token: args.item3,
tokenData: args.item2,
managerProvider: args.item3,
token: args.item4,
),
settings: RouteSettings(
name: settings.name,

View file

@ -1,10 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import "package:hex/hex.dart";
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
@ -22,7 +20,6 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web3dart/web3dart.dart' as web3;
import 'package:web3dart/web3dart.dart' as Transaction;
import 'package:stackwallet/models/models.dart' as models;
@ -66,30 +63,10 @@ class AddressTransaction {
}
}
class GasTracker {
final int code;
final Map<String, dynamic> data;
const GasTracker({
required this.code,
required this.data,
});
factory GasTracker.fromJson(Map<String, dynamic> json) {
return GasTracker(
code: json['code'] as int,
data: json['data'] as Map<String, dynamic>,
);
}
}
class EthereumWallet extends CoinServiceAPI {
NodeModel? _ethNode;
final _gasLimit = 21000;
// final _blockExplorer = "https://blockscout.com/eth/mainnet/api?";
final _blockExplorer = "https://api.etherscan.io/api?";
final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow";
final _hdPath = "m/44'/60'/0'/0";
final _blockExplorer = "https://blockscout.com/eth/mainnet/api?";
@override
String get walletId => _walletId;
@ -213,7 +190,8 @@ class EthereumWallet extends CoinServiceAPI {
final amount = txData['recipientAmt'];
final decimalAmount =
Format.satoshisToAmount(amount as int, coin: Coin.ethereum);
final bigIntAmount = amountToBigInt(decimalAmount.toDouble());
const decimal = 18; //Eth has up to 18 decimal places
final bigIntAmount = amountToBigInt(decimalAmount.toDouble(), decimal);
final tx = Transaction.Transaction(
to: EthereumAddress.fromHex(txData['address'] as String),
@ -227,12 +205,6 @@ class EthereumWallet extends CoinServiceAPI {
return transaction;
}
BigInt amountToBigInt(num amount) {
const decimal = 18; //Eth has up to 18 decimal places
final amountToSendinDecimal = amount * (pow(10, decimal));
return BigInt.from(amountToSendinDecimal);
}
@override
Future<String> get currentReceivingAddress async {
final _currentReceivingAddress = _credentials.address;
@ -243,14 +215,8 @@ class EthereumWallet extends CoinServiceAPI {
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
final gweiAmount = feeRate / (pow(10, 9));
final fee = _gasLimit * gweiAmount;
//Convert gwei to ETH
final feeInWei = fee * (pow(10, 9));
final ethAmount = feeInWei / (pow(10, 18));
return Format.decimalAmountToSatoshis(
Decimal.parse(ethAmount.toString()), coin);
final fee = estimateFee(feeRate, _gasLimit, 18);
return Format.decimalAmountToSatoshis(Decimal.parse(fee.toString()), coin);
}
@override
@ -266,26 +232,7 @@ class EthereumWallet extends CoinServiceAPI {
Future<FeeObject>? _feeObject;
Future<FeeObject> _getFees() async {
GasTracker fees = await getGasOracle();
final feesMap = fees.data;
return FeeObject(
numberOfBlocksFast: 1,
numberOfBlocksAverage: 3,
numberOfBlocksSlow: 3,
fast: feesMap['fast'] as int,
medium: feesMap['standard'] as int,
slow: feesMap['slow'] as 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');
}
return await getFees();
}
//Full rescan is not needed for ETH since we have a balance
@ -331,20 +278,6 @@ class EthereumWallet extends CoinServiceAPI {
}
}
String getPrivateKey(String mnemonic) {
final isValidMnemonic = bip39.validateMnemonic(mnemonic);
if (!isValidMnemonic) {
throw 'Invalid mnemonic';
}
final seed = bip39.mnemonicToSeed(mnemonic);
final root = bip32.BIP32.fromSeed(seed);
const index = 0;
final addressAtIndex = root.derivePath("$_hdPath/$index");
return HEX.encode(addressAtIndex.privateKey as List<int>);
}
@override
Future<void> initializeNew() async {
await _prefs.init();
@ -467,15 +400,11 @@ class EthereumWallet extends CoinServiceAPI {
isSendAll = true;
}
print("SATOSHI AMOUNT BEFORE $satoshiAmount");
print("FEE IS $fee");
if (isSendAll) {
//Subtract fee amount from send amount
satoshiAmount -= feeEstimate;
}
print("SATOSHI AMOUNT AFTER $satoshiAmount");
Map<String, dynamic> txData = {
"fee": feeEstimate,
"feeInWei": fee,
@ -507,7 +436,6 @@ class EthereumWallet extends CoinServiceAPI {
String privateKey = getPrivateKey(mnemonic);
_credentials = EthPrivateKey.fromHex(privateKey);
// print(_credentials.address);
//Get ERC-20 transactions for wallet (So we can get the and save wallet's ERC-20 TOKENS
AddressTransaction tokenTransactions = await fetchAddressTransactions(
_credentials.address.toString(), "tokentx");
@ -515,7 +443,7 @@ class EthereumWallet extends CoinServiceAPI {
List<Map<dynamic, dynamic>> tokensList = [];
if (tokenTransactions.message == "OK") {
final allTxs = tokenTransactions.result;
print("RESULT IS $allTxs");
allTxs.forEach((element) {
String key = element["tokenSymbol"] as String;
tokenMap[key] = {};
@ -544,9 +472,6 @@ class EthereumWallet extends CoinServiceAPI {
key: '${_walletId}_tokens', value: tokensList.toString());
}
print("THIS WALLET TOKENS IS $tokenMap");
print("ALL TOKENS LIST IS $tokensList");
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance
@ -976,7 +901,9 @@ class EthereumWallet extends CoinServiceAPI {
if (checksumEthereumAddress(element["from"].toString()) ==
thisAddress) {
midSortedTx["txType"] = "Sent";
midSortedTx["txType"] = (int.parse(element["isError"] as String) == 0)
? "Sent"
: "Send Failed";
} else {
midSortedTx["txType"] = "Received";
}

View file

@ -1,374 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/tokens/token_manager.dart';
import 'package:stackwallet/services/tokens_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/services/wallets_service.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
import 'package:stackwallet/utilities/listenable_list.dart';
import 'package:stackwallet/utilities/listenable_map.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart';
// final ListenableList<ChangeNotifierProvider<Manager>> _nonFavorites =
// ListenableList();
// ListenableList<ChangeNotifierProvider<Manager>> get nonFavorites =>
// _nonFavorites;
//
// final ListenableList<ChangeNotifierProvider<Manager>> _favorites =
// ListenableList();
// ListenableList<ChangeNotifierProvider<Manager>> get favorites => _favorites;
class Tokens extends ChangeNotifier {
Tokens._private();
@override
dispose() {
debugPrint("Tokens dispose was called!!");
super.dispose();
}
static final Tokens _sharedInstance = Tokens._private();
static Tokens get sharedInstance => _sharedInstance;
late TokensService tokensService;
// late NodeService nodeService;
// mirrored maps for access to reading managers without using riverpod ref
static final ListenableMap<String, ChangeNotifierProvider<TokenManager>>
_managerProviderMap = ListenableMap();
static final ListenableMap<String, TokenManager> _managerMap =
ListenableMap();
// bool get hasWallets => _managerProviderMap.isNotEmpty;
List<ChangeNotifierProvider<TokenManager>> get managerProviders =>
_managerProviderMap.values.toList(growable: false);
List<TokenManager> get managers => _managerMap.values.toList(growable: false);
// List<String> getWalletIdsFor({required Coin coin}) {
// final List<String> result = [];
// for (final manager in _managerMap.values) {
// if (manager.coin == coin) {
// result.add(manager.walletId);
// }
// }
// return result;
// }
// Map<Coin, List<ChangeNotifierProvider<Manager>>> getManagerProvidersByCoin() {
// print("DOES THIS GET HERE?????");
// Map<Coin, List<ChangeNotifierProvider<Manager>>> result = {};
// for (final manager in _managerMap.values) {
// if (result[manager.coin] == null) {
// result[manager.coin] = [];
// }
// result[manager.coin]!.add(_managerProviderMap[manager.walletId]
// as ChangeNotifierProvider<Manager>);
// }
// return result;
// }
// List<ChangeNotifierProvider<Manager>> getManagerProvidersForCoin(Coin coin) {
// List<ChangeNotifierProvider<Manager>> result = [];
// for (final manager in _managerMap.values) {
// if (manager.coin == coin) {
// result.add(_managerProviderMap[manager.walletId]
// as ChangeNotifierProvider<Manager>);
// }
// }
// return result;
// }
ChangeNotifierProvider<TokenManager> getManagerProvider(
String contractAddress) {
print("WALLET ID HERE IS ${_managerProviderMap.length}");
return _managerProviderMap[contractAddress]
as ChangeNotifierProvider<TokenManager>;
}
TokenManager getManager(String contractAddress) {
return _managerMap[contractAddress] as TokenManager;
}
void addToken(
{required String contractAddress, required TokenManager manager}) {
_managerMap.add(contractAddress, manager, true);
_managerProviderMap.add(contractAddress,
ChangeNotifierProvider<TokenManager>((_) => manager), true);
notifyListeners();
}
//
// void removeWallet({required String walletId}) {
// if (_managerProviderMap[walletId] == null) {
// Logging.instance.log(
// "Wallets.removeWallet($walletId) failed. ManagerProvider with $walletId not found!",
// level: LogLevel.Warning);
// return;
// }
//
// final provider = _managerProviderMap[walletId]!;
//
// // in both non and favorites for removal
// _favorites.remove(provider, true);
// _nonFavorites.remove(provider, true);
//
// _managerProviderMap.remove(walletId, true);
// _managerMap.remove(walletId, true)!.exitCurrentWallet();
//
// notifyListeners();
// }
static bool hasLoaded = false;
Future<void> _initLinearly(
List<Tuple2<TokenManager, bool>> tuples,
) async {
for (final tuple in tuples) {
await tuple.item1.initializeExisting();
if (tuple.item2 && !tuple.item1.shouldAutoSync) {
tuple.item1.shouldAutoSync = true;
}
}
}
static int _count = 0;
Future<void> load(Prefs prefs) async {
debugPrint("++++++++++++++ Tokens().load() called: ${++_count} times");
if (hasLoaded) {
return;
}
hasLoaded = true;
// clear out any wallet hive boxes where the wallet was deleted in previous app run
// for (final walletId in DB.instance
// .values<String>(boxName: DB.boxNameWalletsToDeleteOnStart)) {
// await DB.instance.deleteBoxFromDisk(boxName: walletId);
// }
// // clear list
// await DB.instance
// .deleteAll<String>(boxName: DB.boxNameWalletsToDeleteOnStart);
//
// final map = await walletsService.walletNames;
// List<Future<dynamic>> walletInitFutures = [];
// List<Tuple2<Manager, bool>> walletsToInitLinearly = [];
// final favIdList = await walletsService.getFavoriteWalletIds();
// List<String> walletIdsToEnableAutoSync = [];
// bool shouldAutoSyncAll = false;
// switch (prefs.syncType) {
// case SyncingType.currentWalletOnly:
// // do nothing as this will be set when going into a wallet from the main screen
// break;
// case SyncingType.selectedWalletsAtStartup:
// walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup);
// break;
// case SyncingType.allWalletsOnStartup:
// shouldAutoSyncAll = true;
// break;
// }
// for (final entry in map.entries) {
// try {
// final walletId = entry.value.walletId;
//
// late final bool isVerified;
// try {
// isVerified =
// await walletsService.isMnemonicVerified(walletId: walletId);
// } catch (e, s) {
// Logging.instance.log("$e $s", level: LogLevel.Warning);
// isVerified = false;
// }
//
// Logging.instance.log(
// "LOADING WALLET: ${entry.value.toString()} IS VERIFIED: $isVerified",
// level: LogLevel.Info);
// if (isVerified) {
// if (_managerMap[walletId] == null &&
// _managerProviderMap[walletId] == null) {
// final coin = entry.value.coin;
// NodeModel node = nodeService.getPrimaryNodeFor(coin: coin) ??
// DefaultNodes.getNodeFor(coin);
// // ElectrumXNode? node = await nodeService.getCurrentNode(coin: coin);
//
// // folowing shouldn't be needed as the defaults get saved on init
// // if (node == null) {
// // node = DefaultNodes.getNodeFor(coin);
// //
// // // save default node
// // nodeService.add(node, false);
// // }
//
// final txTracker =
// TransactionNotificationTracker(walletId: walletId);
//
// final failovers = nodeService.failoverNodesFor(coin: coin);
//
// // load wallet
// final wallet = CoinServiceAPI.from(
// coin,
// walletId,
// entry.value.name,
// nodeService.secureStorageInterface,
// node,
// txTracker,
// prefs,
// failovers,
// );
//
// final manager = Manager(wallet);
//
// final shouldSetAutoSync = shouldAutoSyncAll ||
// walletIdsToEnableAutoSync.contains(manager.walletId);
//
// if (manager.coin == Coin.monero || manager.coin == Coin.wownero) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
// } else {
// walletInitFutures.add(manager.initializeExisting().then((value) {
// if (shouldSetAutoSync) {
// manager.shouldAutoSync = true;
// }
// }));
// }
//
// _managerMap.add(walletId, manager, false);
//
// final managerProvider =
// ChangeNotifierProvider<Manager>((_) => manager);
// _managerProviderMap.add(walletId, managerProvider, false);
//
// final favIndex = favIdList.indexOf(walletId);
//
// if (favIndex == -1) {
// _nonFavorites.add(managerProvider, true);
// } else {
// // it is a favorite
// if (favIndex >= _favorites.length) {
// _favorites.add(managerProvider, true);
// } else {
// _favorites.insert(favIndex, managerProvider, true);
// }
// }
// }
// } else {
// // wallet creation was not completed by user so we remove it completely
// await walletsService.deleteWallet(entry.value.name, false);
// }
// } catch (e, s) {
// Logging.instance.log("$e $s", level: LogLevel.Fatal);
// continue;
// }
// }
// if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) {
// await Future.wait([
// _initLinearly(walletsToInitLinearly),
// ...walletInitFutures,
// ]);
// notifyListeners();
// } else if (walletInitFutures.isNotEmpty) {
// await Future.wait(walletInitFutures);
// notifyListeners();
// } else if (walletsToInitLinearly.isNotEmpty) {
// await _initLinearly(walletsToInitLinearly);
// notifyListeners();
// }
}
// Future<void> loadAfterStackRestore(
// Prefs prefs, List<Manager> managers) async {
// List<Future<dynamic>> walletInitFutures = [];
// List<Tuple2<Manager, bool>> walletsToInitLinearly = [];
//
// final favIdList = await walletsService.getFavoriteWalletIds();
//
// List<String> walletIdsToEnableAutoSync = [];
// bool shouldAutoSyncAll = false;
// switch (prefs.syncType) {
// case SyncingType.currentWalletOnly:
// // do nothing as this will be set when going into a wallet from the main screen
// break;
// case SyncingType.selectedWalletsAtStartup:
// walletIdsToEnableAutoSync.addAll(prefs.walletIdsSyncOnStartup);
// break;
// case SyncingType.allWalletsOnStartup:
// shouldAutoSyncAll = true;
// break;
// }
//
// for (final manager in managers) {
// final walletId = manager.walletId;
//
// final isVerified =
// await walletsService.isMnemonicVerified(walletId: walletId);
// debugPrint(
// "LOADING RESTORED WALLET: ${manager.walletName} ${manager.walletId} IS VERIFIED: $isVerified");
//
// if (isVerified) {
// if (_managerMap[walletId] == null &&
// _managerProviderMap[walletId] == null) {
// final shouldSetAutoSync = shouldAutoSyncAll ||
// walletIdsToEnableAutoSync.contains(manager.walletId);
//
// if (manager.coin == Coin.monero || manager.coin == Coin.wownero) {
// walletsToInitLinearly.add(Tuple2(manager, shouldSetAutoSync));
// } else {
// walletInitFutures.add(manager.initializeExisting().then((value) {
// if (shouldSetAutoSync) {
// manager.shouldAutoSync = true;
// }
// }));
// }
//
// _managerMap.add(walletId, manager, false);
//
// final managerProvider =
// ChangeNotifierProvider<Manager>((_) => manager);
// _managerProviderMap.add(walletId, managerProvider, false);
//
// final favIndex = favIdList.indexOf(walletId);
//
// if (favIndex == -1) {
// _nonFavorites.add(managerProvider, true);
// } else {
// // it is a favorite
// if (favIndex >= _favorites.length) {
// _favorites.add(managerProvider, true);
// } else {
// _favorites.insert(favIndex, managerProvider, true);
// }
// }
// }
// } else {
// // wallet creation was not completed by user so we remove it completely
// await walletsService.deleteWallet(manager.walletName, false);
// }
// }
//
// if (walletInitFutures.isNotEmpty && walletsToInitLinearly.isNotEmpty) {
// await Future.wait([
// _initLinearly(walletsToInitLinearly),
// ...walletInitFutures,
// ]);
// notifyListeners();
// } else if (walletInitFutures.isNotEmpty) {
// await Future.wait(walletInitFutures);
// notifyListeners();
// } else if (walletsToInitLinearly.isNotEmpty) {
// await _initLinearly(walletsToInitLinearly);
// notifyListeners();
// }
// }
}

View file

@ -1,16 +1,20 @@
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'dart:math';
import 'package:http/http.dart';
import 'package:decimal/decimal.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/paymint/transactions_model.dart';
import 'package:stackwallet/services/tokens/token_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/services/tokens/token_service.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:web3dart/web3dart.dart';
import 'package:web3dart/web3dart.dart' as transaction;
class AbiRequestResponse {
final String message;
@ -35,118 +39,222 @@ class AbiRequestResponse {
class EthereumToken extends TokenServiceAPI {
@override
late bool shouldAutoSync;
late String _contractAddress;
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;
late SecureStorageInterface _secureStore;
late String _tokenAbi;
late Web3Client _client;
late final TransactionNotificationTracker txTracker;
String rpcUrl =
'https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba';
final _gasLimit = 200000;
EthereumToken({
required String contractAddress,
required Map<dynamic, dynamic> tokenData,
required Future<List<String>> walletMnemonic,
// required SecureStorageInterface secureStore,
}) {
_contractAddress = contractAddress;
_contractAddress =
EthereumAddress.fromHex(tokenData["contractAddress"] as String);
_walletMnemonic = walletMnemonic;
_tokenData = tokenData;
// _secureStore = secureStore;
}
Future<AbiRequestResponse> fetchTokenAbi() async {
print(
"$blockExplorer?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP");
final response = await get(Uri.parse(
"https://api.etherscan.io/api?module=contract&action=getabi&address=$_contractAddress&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
"$blockExplorer?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('Failed to load transactions');
throw Exception('Failed to load token abi');
}
}
@override
// TODO: implement allOwnAddresses
Future<List<String>> get allOwnAddresses => throw UnimplementedError();
Future<List<String>> get allOwnAddresses =>
_allOwnAddresses ??= _fetchAllOwnAddresses();
Future<List<String>>? _allOwnAddresses;
@override
// TODO: implement availableBalance
Future<Decimal> get availableBalance => throw UnimplementedError();
@override
// TODO: implement balanceMinusMaxFee
Future<Decimal> get balanceMinusMaxFee => throw UnimplementedError();
@override
// TODO: implement coin
Coin get coin => throw UnimplementedError();
@override
Future<String> confirmSend({required Map<String, dynamic> txData}) {
// TODO: implement confirmSend
throw UnimplementedError();
Future<List<String>> _fetchAllOwnAddresses() async {
List<String> addresses = [];
final ownAddress = _credentials.address;
addresses.add(ownAddress.toString());
return addresses;
}
@override
// TODO: implement currentReceivingAddress
Future<String> get currentReceivingAddress => throw UnimplementedError();
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) {
// TODO: implement estimateFeeFor
throw UnimplementedError();
Future<Decimal> get availableBalance async {
return await totalBalance;
}
@override
// TODO: implement fees
Future<FeeObject> get fees => throw UnimplementedError();
Future<Decimal> get balanceMinusMaxFee async =>
(await availableBalance) -
(Decimal.fromInt((await maxFee)) /
Decimal.fromInt(Constants.satsPerCoin(coin)))
.toDecimal();
@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 sentTx = await _client.sendTransaction(
_credentials,
transaction.Transaction.callContract(
contract: _contract,
function: _sendFunction,
parameters: [
EthereumAddress.fromHex(txData['address'] as String),
bigIntAmount
],
maxGas: _gasLimit,
gasPrice: EtherAmount.fromUnitAndValue(
EtherUnit.wei, txData['feeInWei'])));
return sentTx;
}
@override
Future<String> get currentReceivingAddress async {
final _currentReceivingAddress = await _credentials.extractAddress();
final checkSumAddress =
checksumEthereumAddress(_currentReceivingAddress.toString());
return checkSumAddress;
}
@override
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
final fee = estimateFee(
feeRate, _gasLimit, int.parse(_tokenData["decimals"] as String));
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();
}
@override
Future<void> initializeExisting() async {
//TODO - GET abi FROM secure store
AbiRequestResponse abi = await fetchTokenAbi();
//Fetch token ABI so we can call token functions
if (abi.message == "OK") {
_tokenAbi = abi.result;
}
final mnemonic = await _walletMnemonic;
String mnemonicString = mnemonic.join(' ');
//Get private key for given mnemonic
String privateKey = getPrivateKey(mnemonicString);
// TODO: implement initializeExisting
throw UnimplementedError();
_credentials = EthPrivateKey.fromHex(privateKey);
_contract = DeployedContract(
ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String),
_contractAddress);
_balanceFunction = _contract.function('balanceOf');
_sendFunction = _contract.function('transfer');
_client = await getEthClient();
print("${await totalBalance}");
}
@override
Future<void> initializeNew() async {
throw UnimplementedError();
//TODO - Save abi in secure store
AbiRequestResponse abi = await fetchTokenAbi();
//Fetch token ABI so we can call token functions
if (abi.message == "OK") {
_tokenAbi = abi.result;
}
final mnemonic = await _walletMnemonic;
String mnemonicString = mnemonic.join(' ');
@override
// TODO: implement isConnected
bool get isConnected => throw UnimplementedError();
//Get private key for given mnemonic
String privateKey = getPrivateKey(mnemonicString);
_credentials = EthPrivateKey.fromHex(privateKey);
_contract = DeployedContract(
ContractAbi.fromJson(_tokenAbi, _tokenData["name"] as String),
_contractAddress);
_balanceFunction = _contract.function('balanceOf');
_sendFunction = _contract.function('transfer');
_client = await getEthClient();
}
@override
// TODO: implement isRefreshing
bool get isRefreshing => throw UnimplementedError();
@override
// TODO: implement maxFee
Future<int> get maxFee => throw UnimplementedError();
@override
// TODO: implement pendingBalance
Future<Decimal> get pendingBalance => throw UnimplementedError();
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,
Map<String, dynamic>? args}) {
// TODO: implement prepareSend
throw UnimplementedError();
Map<String, dynamic>? args}) async {
final feeRateType = args?["feeRate"];
int fee = 0;
final feeObject = await fees;
switch (feeRateType) {
case FeeRateType.fast:
fee = feeObject.fast;
break;
case FeeRateType.average:
fee = feeObject.medium;
break;
case FeeRateType.slow:
fee = feeObject.slow;
break;
}
final feeEstimate = await estimateFeeFor(satoshiAmount, fee);
bool isSendAll = false;
final balance =
Format.decimalAmountToSatoshis(await availableBalance, coin);
if (satoshiAmount == balance) {
isSendAll = true;
}
if (isSendAll) {
//Subtract fee amount from send amount
satoshiAmount -= feeEstimate;
}
Map<String, dynamic> txData = {
"fee": feeEstimate,
"feeInWei": fee,
"address": address,
"recipientAmt": satoshiAmount,
};
print("TX DATA TO BE SENT IS $txData");
return txData;
}
@override
@ -156,22 +264,69 @@ class EthereumToken extends TokenServiceAPI {
}
@override
// TODO: implement totalBalance
Future<Decimal> get totalBalance => throw UnimplementedError();
Future<Decimal> get totalBalance async {
final balanceRequest = await _client.call(
contract: _contract,
function: _balanceFunction,
params: [_credentials.address]);
String balance = balanceRequest.first.toString();
int tokenDecimals = int.parse(_tokenData["decimals"] as String);
final balanceInDecimal = (int.parse(balance) / (pow(10, tokenDecimals)));
return Decimal.parse(balanceInDecimal.toString());
}
@override
// TODO: implement transactionData
Future<TransactionData> get transactionData => throw UnimplementedError();
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) {
// TODO: implement updateSentCachedTxData
throw UnimplementedError();
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
Decimal currentPrice = Decimal.parse(0.0 as String);
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin(coin)))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
} else {
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
}
@override
bool validateAddress(String address) {
// TODO: implement validateAddress
throw UnimplementedError();
return isValidEthereumAddress(address);
}
Future<Web3Client> getEthClient() async {
return Web3Client(rpcUrl, Client());
}
}

View file

@ -1,130 +0,0 @@
import 'dart:async';
import 'package:decimal/decimal.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:stackwallet/models/models.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/tokens/token_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
class TokenManager with ChangeNotifier {
final TokenServiceAPI _currentToken;
StreamSubscription<dynamic>? _backgroundRefreshListener;
/// optional eventbus parameter for testing only
TokenManager(this._currentToken, [EventBus? globalEventBusForTesting]) {
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
_backgroundRefreshListener = bus.on<UpdatedInBackgroundEvent>().listen(
(event) async {
// if (event.walletId == walletId) {
// notifyListeners();
// Logging.instance.log(
// "UpdatedInBackgroundEvent activated notifyListeners() in Manager instance $hashCode $walletName with: ${event.message}",
// level: LogLevel.Info);
// }
},
);
}
TokenServiceAPI get token => _currentToken;
bool get hasBackgroundRefreshListener => _backgroundRefreshListener != null;
bool get isRefreshing => _currentToken.isRefreshing;
bool get shouldAutoSync => _currentToken.shouldAutoSync;
set shouldAutoSync(bool shouldAutoSync) =>
_currentToken.shouldAutoSync = shouldAutoSync;
Future<Map<String, dynamic>> prepareSend({
required String address,
required int satoshiAmount,
Map<String, dynamic>? args,
}) async {
try {
final txInfo = await _currentToken.prepareSend(
address: address,
satoshiAmount: satoshiAmount,
args: args,
);
// notifyListeners();
return txInfo;
} catch (e) {
// rethrow to pass error in alert
rethrow;
}
}
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
try {
final txid = await _currentToken.confirmSend(txData: txData);
txData["txid"] = txid;
await _currentToken.updateSentCachedTxData(txData);
notifyListeners();
return txid;
} catch (e) {
// rethrow to pass error in alert
rethrow;
}
}
Future<FeeObject> get fees => _currentToken.fees;
Future<int> get maxFee => _currentToken.maxFee;
Future<String> get currentReceivingAddress =>
_currentToken.currentReceivingAddress;
// Future<String> get currentLegacyReceivingAddress =>
// _currentWallet.currentLegacyReceivingAddress;
Future<Decimal> get availableBalance async {
_cachedAvailableBalance = await _currentToken.availableBalance;
return _cachedAvailableBalance;
}
Decimal _cachedAvailableBalance = Decimal.zero;
Decimal get cachedAvailableBalance => _cachedAvailableBalance;
Future<Decimal> get pendingBalance => _currentToken.pendingBalance;
Future<Decimal> get balanceMinusMaxFee => _currentToken.balanceMinusMaxFee;
Future<Decimal> get totalBalance async {
_cachedTotalBalance = await _currentToken.totalBalance;
return _cachedTotalBalance;
}
Decimal _cachedTotalBalance = Decimal.zero;
Decimal get cachedTotalBalance => _cachedTotalBalance;
Future<List<String>> get allOwnAddresses => _currentToken.allOwnAddresses;
Future<TransactionData> get transactionData => _currentToken.transactionData;
Future<void> refresh() async {
await _currentToken.refresh();
notifyListeners();
}
bool validateAddress(String address) =>
_currentToken.validateAddress(address);
Future<void> initializeNew() => _currentToken.initializeNew();
Future<void> initializeExisting() => _currentToken.initializeExisting();
Future<bool> isOwnAddress(String address) async {
final allOwnAddresses = await this.allOwnAddresses;
return allOwnAddresses.contains(address);
}
bool get isConnected => _currentToken.isConnected;
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
return _currentToken.estimateFeeFor(satoshiAmount, feeRate);
}
}

View file

@ -10,17 +10,17 @@ abstract class TokenServiceAPI {
TokenServiceAPI();
factory TokenServiceAPI.from(
String contractAddress,
String walletId,
Map<dynamic, dynamic> tokenData,
Future<List<String>> walletMnemonic,
SecureStorageInterface secureStorageInterface,
TransactionNotificationTracker tracker,
Prefs prefs,
) {
return EthereumToken(
contractAddress: contractAddress,
walletId: walletId,
secureStore: secureStorageInterface,
tracker: tracker,
tokenData: tokenData,
walletMnemonic: walletMnemonic,
// secureStore: secureStorageInterface,
// tracker: tracker,
);
}
@ -44,7 +44,6 @@ abstract class TokenServiceAPI {
// Future<String> get currentLegacyReceivingAddress;
Future<Decimal> get availableBalance;
Future<Decimal> get pendingBalance;
Future<Decimal> get totalBalance;
Future<Decimal> get balanceMinusMaxFee;
@ -62,10 +61,6 @@ abstract class TokenServiceAPI {
Future<void> initializeNew();
Future<void> initializeExisting();
// void Function(bool isActive)? onIsActiveWalletChanged;
bool get isConnected;
Future<int> estimateFeeFor(int satoshiAmount, int feeRate);
// used for electrumx coins

View file

@ -1,7 +1,9 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'flutter_secure_storage_interface.dart';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
@ -27,13 +29,31 @@ class AccountModule {
}
}
const _blockExplorer = "https://api.etherscan.io/api?";
late SecureStorageInterface _secureStore;
class GasTracker {
final int code;
final Map<String, dynamic> data;
const GasTracker({
required this.code,
required this.data,
});
factory GasTracker.fromJson(Map<String, dynamic> json) {
return GasTracker(
code: json['code'] as int,
data: json['data'] as Map<String, dynamic>,
);
}
}
// const blockExplorer = "https://blockscout.com/eth/mainnet/api";
const blockExplorer = "https://api.etherscan.io/api";
const _hdPath = "m/44'/60'/0'/0";
const _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow";
Future<AccountModule> fetchAccountModule(String action, String address) async {
final response = await get(Uri.parse(
"${_blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
"${blockExplorer}module=account&action=$action&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
if (response.statusCode == 200) {
return AccountModule.fromJson(
json.decode(response.body) as Map<String, dynamic>);
@ -48,7 +68,6 @@ Future<List<dynamic>> getWalletTokens(String address) async {
var tokenMap = {};
if (tokens.message == "OK") {
final allTxs = tokens.result;
print("RESULT IS $allTxs");
allTxs.forEach((element) {
String key = element["tokenSymbol"] as String;
tokenMap[key] = {};
@ -88,3 +107,41 @@ String getPrivateKey(String mnemonic) {
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 feesMap = fees.data;
return FeeObject(
numberOfBlocksFast: 1,
numberOfBlocksAverage: 3,
numberOfBlocksSlow: 3,
fast: feesMap['fast'] as int,
medium: feesMap['standard'] as int,
slow: feesMap['slow'] as int);
}
double estimateFee(int feeRate, int gasLimit, int decimals) {
final gweiAmount = feeRate / (pow(10, 9));
final fee = gasLimit * gweiAmount;
//Convert gwei to ETH
final feeInWei = fee * (pow(10, 9));
final ethAmount = feeInWei / (pow(10, decimals));
return ethAmount;
}
BigInt amountToBigInt(num amount, int decimal) {
final amountToSendinDecimal = amount * (pow(10, decimal));
return BigInt.from(amountToSendinDecimal);
}