diff --git a/assets/images/ethereum.png b/assets/images/dark/ethereum.png similarity index 100% rename from assets/images/ethereum.png rename to assets/images/dark/ethereum.png diff --git a/assets/images/forest/ethereum.png b/assets/images/forest/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/forest/ethereum.png differ diff --git a/assets/images/fruitSorbet/ethereum.png b/assets/images/fruitSorbet/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/fruitSorbet/ethereum.png differ diff --git a/assets/images/light/ethereum.png b/assets/images/light/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/light/ethereum.png differ diff --git a/assets/images/oceanBreeze/ethereum.png b/assets/images/oceanBreeze/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/oceanBreeze/ethereum.png differ diff --git a/assets/images/oledBlack/ethereum.png b/assets/images/oledBlack/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/oledBlack/ethereum.png differ diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 0d29f28a7..1992cd051 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -123,6 +123,7 @@ enum AddressType { mimbleWimble, unknown, nonWallet, + ethereum, } // do not modify diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 99ed79f73..18100eee7 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svget/electrumx_rpc/electrumx.dart'; -import 'package:http/http.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/secure_store_provider.dart'; diff --git a/lib/pages/token_view/sub_widgets/token_summary_info.dart b/lib/pages/token_view/sub_widgets/token_summary_info.dart index 40abaebe0..1c3c23dda 100644 --- a/lib/pages/token_view/sub_widgets/token_summary_info.dart +++ b/lib/pages/token_view/sub_widgets/token_summary_info.dart @@ -6,7 +6,6 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_ 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'; @@ -75,19 +74,12 @@ class _TokenSummaryInfoState extends State { Future? totalBalanceFuture; Future? 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 manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + totalBalanceFuture = Future(() => manager.balance.getTotal()); + availableBalanceFuture = + Future(() => manager.balance.getSpendable()); final locale = ref.watch(localeServiceChangeNotifierProvider .select((value) => value.locale)); diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart index 1645cdf78..cb0cc8bc3 100644 --- a/lib/pages/token_view/token_view.dart +++ b/lib/pages/token_view/token_view.dart @@ -1,54 +1,31 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; 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/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; -import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; -import 'package:stackwallet/pages/notification_views/notifications_view.dart'; -import 'package:stackwallet/pages/receive_view/receive_view.dart'; -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'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; 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/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/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/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -import 'package:stackwallet/services/exchange/exchange_data_loading_service.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'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; -import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; -import 'package:stackwallet/utilities/eth_commons.dart'; -import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; -import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing class TokenView extends ConsumerStatefulWidget { @@ -87,8 +64,6 @@ class _TokenViewState extends ConsumerState { late StreamSubscription _syncStatusSubscription; late StreamSubscription _nodeStatusSubscription; - final _cnLoadingService = ExchangeDataLoadingService(); - @override void initState() { walletId = widget.walletId; @@ -211,64 +186,6 @@ class _TokenViewState extends ConsumerState { } } - void _onExchangePressed(BuildContext context) async { - unawaited(_cnLoadingService.loadAll(ref)); - - final coin = ref.read(managerProvider).coin; - - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - final walletId = ref.read(managerProvider).walletId; - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; - - ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); - ref.read(exchangeFormStateProvider).exchangeType = - ExchangeRateType.estimated; - - final currencies = ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .where((element) => - element.ticker.toLowerCase() == coin.ticker.toLowerCase()); - - if (currencies.isNotEmpty) { - ref.read(exchangeFormStateProvider).setCurrencies( - currencies.first, - ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .firstWhere( - (element) => - element.ticker.toLowerCase() != coin.ticker.toLowerCase(), - ), - ); - } - - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - WalletInitiatedExchangeView.routeName, - arguments: Tuple3( - walletId, - coin, - _loadCNData, - ), - ), - ); - } - } - - void _loadCNData() { - // unawaited future - if (ref.read(prefsChangeNotifierProvider).externalCalls) { - _cnLoadingService.loadAll(ref, coin: ref.read(managerProvider).coin); - } else { - Logging.instance.log("User does not want to use external calls", - level: LogLevel.Info); - } - } - @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -357,7 +274,7 @@ class _TokenViewState extends ConsumerState { .textDark3, ), ), - BlueTextButton( + CustomTextButton( text: "See all", onTap: () { Navigator.of(context).pushNamed( diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart index d699cc26a..18ed2c5cb 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -334,6 +334,45 @@ class _WalletNavigationBarState extends ConsumerState { ), ), ), + if (widget.coin == Coin.ethereum) + RawMaterialButton( + constraints: const BoxConstraints( + minWidth: 66, + ), + onPressed: widget.onTokensPressed, + splashColor: + Theme.of(context).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + widget.height / 2.0, + ), + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + SvgPicture.asset( + Assets.svg.tokens, + width: 24, + height: 24, + ), + const SizedBox( + height: 4, + ), + Text( + "Tokens", + style: STextStyles.buttonSmall(context), + ), + const Spacer(), + ], + ), + ), + ), + ), if (widget.enableExchange) RawMaterialButton( constraints: const BoxConstraints( @@ -476,89 +515,6 @@ class _WalletNavigationBarState extends ConsumerState { ), ), ], - const Spacer(), - ], - ), - ), - ), - ), - const SizedBox( - width: 12, - ), - // TODO: Do not delete this code. - // only temporarily disabled - // Spacer( - // flex: 2, - // ), - // GestureDetector( - // onTap: onBuyPressed, - // child: Container( - // color: Colors.transparent, - // child: Padding( - // padding: const EdgeInsets.symmetric(vertical: 2.0), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // Spacer(), - // SvgPicture.asset( - // Assets.svg.buy, - // width: 24, - // height: 24, - // ), - // SizedBox( - // height: 4, - // ), - // Text( - // "Buy", - // style: STextStyles.buttonSmall(context), - // ), - // Spacer(), - // ], - // ), - // ), - // ), - // ), - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: onTokensPressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - height / 2.0, - ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - SvgPicture.asset( - Assets.svg.tokens, - width: 24, - height: 24, - ), - const SizedBox( - height: 4, - ), - Text( - "Tokens", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], - ), - ), - ), - ), - ], - ), - ), ); } } diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 4ca1f9aa6..58e07bc5f 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -36,7 +36,6 @@ 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/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -262,34 +261,6 @@ class _WalletViewState extends ConsumerState { ), ); } else { - // ref.read(currentExchangeNameStateProvider.state).state = - // ChangeNowExchange.exchangeName; - // final walletId = ref.read(managerProvider).walletId; - // ref.read(prefsChangeNotifierProvider).exchangeRateType = - // ExchangeRateType.estimated; - // - // final currencies = ref - // .read(availableChangeNowCurrenciesProvider) - // .currencies - // .where((element) => - // element.ticker.toLowerCase() == coin.ticker.toLowerCase()); - // - // if (currencies.isNotEmpty) { - // ref - // .read(exchangeFormStateProvider(ExchangeRateType.estimated)) - // .setCurrencies( - // currencies.first, - // ref - // .read(availableChangeNowCurrenciesProvider) - // .currencies - // .firstWhere( - // (element) => - // element.ticker.toLowerCase() != - // coin.ticker.toLowerCase(), - // ), - // ); - // } - if (mounted) { unawaited( Navigator.of(context).pushNamed( @@ -817,11 +788,17 @@ class _WalletViewState extends ConsumerState { .read(managerProvider) .currentReceivingAddress); - await Navigator.of(context).pushNamed( - MyTokensView.routeName, - arguments: Tuple4(managerProvider, - walletId, walletAddress, tokens), - ); + if (mounted) { + await Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: Tuple4( + managerProvider, + walletId, + walletAddress, + tokens, + ), + ); + } }, ), ), diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 2ab18c4e1..82bfda2e1 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1597,8 +1597,9 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { case DerivePathType.bip49: key = "${walletId}_${chainId}DerivationsP2SH"; break; - case DerivePathType.bip84: - throw UnsupportedError("bip84 not supported by BCH"); + default: + throw UnsupportedError( + "${derivePathType.name} not supported by ${coin.prettyName}"); } return key; } @@ -2712,7 +2713,8 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { addressTxid[address] = []; } (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { + final deriveType = addressType(address: address); + switch (deriveType) { case DerivePathType.bip44: case DerivePathType.bch44: addressesP2PKH.add(address); @@ -2720,8 +2722,9 @@ class BitcoinCashWallet extends CoinServiceAPI with WalletCache, WalletDB { case DerivePathType.bip49: addressesP2SH.add(address); break; - case DerivePathType.bip84: - throw UnsupportedError("bip84 not supported by BCH"); + default: + throw UnsupportedError( + "${deriveType.name} not supported by ${coin.prettyName}"); } } } diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 9adf47246..e5cb6f5de 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1,116 +1,110 @@ import 'dart:async'; -import 'dart:math'; -import 'package:bip39/bip39.dart' as bip39; +import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; -import 'package:devicelocale/devicelocale.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:http/http.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/models/balance.dart'; +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/models/paymint/transactions_model.dart'; -import 'package:stackwallet/models/paymint/utxo_model.dart'; -import 'package:stackwallet/services/price.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/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/wallet_cache.dart'; +import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/assets.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'; import 'package:stackwallet/utilities/eth_commons.dart'; 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 Transaction; -import 'package:stackwallet/models/models.dart' as models; - -import 'package:http/http.dart'; - -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/logger.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/refresh_percent_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/utilities/assets.dart'; -import 'package:stackwallet/services/notifications_api.dart'; -import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; -import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; +import 'package:web3dart/web3dart.dart' as web3; const int MINIMUM_CONFIRMATIONS = 3; -class EthereumWallet extends CoinServiceAPI { - NodeModel? _ethNode; - final _gasLimit = 21000; - - @override - String get walletId => _walletId; - late String _walletId; - - late String _walletName; - late Coin _coin; - Timer? timer; - Timer? _networkAliveTimer; - - @override - set isFavorite(bool markFavorite) { - DB.instance.put( - boxName: walletId, key: "isFavorite", value: markFavorite); - } - - @override - bool get isFavorite { - try { - return DB.instance.get(boxName: walletId, key: "isFavorite") - as bool; - } catch (e, s) { - Logging.instance.log( - "isFavorite fetch failed (returning false by default): $e\n$s", - level: LogLevel.Error); - return false; - } - } - - @override - Coin get coin => _coin; - - late SecureStorageInterface _secureStore; - late final TransactionNotificationTracker txTracker; - late PriceAPI _priceAPI; - final _prefs = Prefs.instance; - bool longMutex = false; - - Future getCurrentNode() async { - return NodeService(secureStorageInterface: _secureStore) - .getPrimaryNodeFor(coin: coin) ?? - DefaultNodes.getNodeFor(coin); - } - - Future getEthClient() async { - final node = await getCurrentNode(); - return Web3Client(node.host, Client()); - } - - late EthPrivateKey _credentials; - +class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { EthereumWallet({ required String walletId, required String walletName, required Coin coin, - PriceAPI? priceAPI, required SecureStorageInterface secureStore, required TransactionNotificationTracker tracker, + MainDB? mockableOverride, }) { txTracker = tracker; _walletId = walletId; _walletName = walletName; _coin = coin; - _priceAPI = priceAPI ?? PriceAPI(Client()); _secureStore = secureStore; + initCache(walletId, coin); + initWalletDB(mockableOverride: mockableOverride); } + NodeModel? _ethNode; + + final _gasLimit = 21000; + + Timer? timer; + Timer? _networkAliveTimer; + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + @override + set walletName(String newName) => _walletName = newName; + + @override + set isFavorite(bool markFavorite) { + _isFavorite = markFavorite; + updateCachedIsFavorite(markFavorite); + } + + @override + bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); + bool? _isFavorite; + + @override + Coin get coin => _coin; + late Coin _coin; + + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + final _prefs = Prefs.instance; + bool longMutex = false; + + NodeModel getCurrentNode() { + return _ethNode ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + web3.Web3Client getEthClient() { + final node = getCurrentNode(); + return web3.Web3Client(node.host, Client()); + } + + late web3.EthPrivateKey _credentials; + bool _shouldAutoSync = false; @override @@ -132,64 +126,68 @@ class EthereumWallet extends CoinServiceAPI { } @override - String get walletName => _walletName; + Future> get utxos => db.getUTXOs(walletId).findAll(); @override - Future> get allOwnAddresses => - _allOwnAddresses ??= _fetchAllOwnAddresses(); - Future>? _allOwnAddresses; + Future> get transactions => + db.getTransactions(walletId).sortByTimestampDesc().findAll(); - Future> _fetchAllOwnAddresses() async { - List addresses = []; - final ownAddress = _credentials.address; - addresses.add(ownAddress.toString()); - return addresses; + @override + Future get currentReceivingAddress async { + final address = await _currentReceivingAddress; + return address?.value ?? + checksumEthereumAddress(_credentials.address.toString()); } - @override - Future get availableBalance async { - Web3Client client = await getEthClient(); - EtherAmount ethBalance = await client.getBalance(_credentials.address); - return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); - } + Future get _currentReceivingAddress => db + .getAddresses(walletId) + .filter() + .typeEqualTo(AddressType.p2wpkh) + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); @override - Future get balanceMinusMaxFee async => - (await availableBalance) - - (Decimal.fromInt((await maxFee)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(); + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + Future updateBalance() async { + web3.Web3Client client = getEthClient(); + web3.EtherAmount ethBalance = await client.getBalance(_credentials.address); + // TODO: check if toInt() is ok and if getBalance actually returns enough balance data + _balance = Balance( + coin: coin, + total: ethBalance.getInWei.toInt(), + spendable: ethBalance.getInWei.toInt(), + blockedTotal: 0, + pendingSpendable: 0, + ); + await updateCachedBalance(_balance!); + } @override Future confirmSend({required Map txData}) async { - Web3Client client = await getEthClient(); + web3.Web3Client client = getEthClient(); final int chainId = await client.getNetworkId(); final amount = txData['recipientAmt']; - final decimalAmount = - Format.satoshisToAmount(amount as int, coin: Coin.ethereum); - const decimal = 18; //Eth has up to 18 decimal places - final bigIntAmount = amountToBigInt(decimalAmount.toDouble(), decimal); + final decimalAmount = Format.satoshisToAmount(amount as int, coin: coin); + final bigIntAmount = amountToBigInt( + decimalAmount.toDouble(), + Constants.decimalPlacesForCoin(coin), + ); - final tx = Transaction.Transaction( - to: EthereumAddress.fromHex(txData['address'] as String), - gasPrice: - EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']), + final tx = web3.Transaction( + to: web3.EthereumAddress.fromHex(txData['address'] as String), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, txData['feeInWei']), maxGas: _gasLimit, - value: EtherAmount.inWei(bigIntAmount)); + value: web3.EtherAmount.inWei(bigIntAmount)); final transaction = await client.sendTransaction(_credentials, tx, chainId: chainId); return transaction; } - @override - Future get currentReceivingAddress async { - final _currentReceivingAddress = _credentials.address; - final checkSumAddress = - checksumEthereumAddress(_currentReceivingAddress.toString()); - return checkSumAddress; - } - @override Future estimateFeeFor(int satoshiAmount, int feeRate) async { final fee = estimateFee(feeRate, _gasLimit, 18); @@ -233,61 +231,102 @@ class EthereumWallet extends CoinServiceAPI { @override Future initializeExisting() async { + Logging.instance.log( + "initializeExisting() ${coin.prettyName} wallet", + level: LogLevel.Info, + ); + //First get mnemonic so we can initialize credentials - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - String privateKey = getPrivateKey(mnemonicString!); - _credentials = EthPrivateKey.fromHex(privateKey); + String privateKey = + getPrivateKey((await mnemonicString)!, (await mnemonicPassphrase)!); + _credentials = web3.EthPrivateKey.fromHex(privateKey); - Logging.instance.log("Opening existing ${coin.prettyName} wallet.", - level: LogLevel.Info); - - if ((DB.instance.get(boxName: walletId, key: "id")) == null) { + if (getCachedId() == null) { throw Exception( "Attempted to initialize an existing wallet using an unknown wallet ID!"); } await _prefs.init(); - final data = - DB.instance.get(boxName: walletId, key: "latest_tx_model") - as TransactionData?; - if (data != null) { - _transactionData = Future(() => data); - } } @override Future initializeNew() async { + Logging.instance.log( + "Generating new ${coin.prettyName} wallet.", + level: LogLevel.Info, + ); + + if (getCachedId() != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + await _prefs.init(); + + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } + + Future _generateNewWallet() async { + // Logging.instance + // .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + // if (!integrationTestFlag) { + // try { + // final features = await electrumXClient.getServerFeatures(); + // Logging.instance.log("features: $features", level: LogLevel.Info); + // switch (coin) { + // case Coin.namecoin: + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // break; + // default: + // throw Exception( + // "Attempted to generate a EthereumWallet using a non eth coin type: ${coin.name}"); + // } + // } catch (e, s) { + // Logging.instance.log("$e/n$s", level: LogLevel.Info); + // } + // } + + // this should never fail - sanity check + if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + final String mnemonic = bip39.generateMnemonic(strength: 256); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); - String privateKey = getPrivateKey(mnemonic); - _credentials = EthPrivateKey.fromHex(privateKey); - //Store credentials in secure store await _secureStore.write( - key: '${_walletId}_credentials', value: _credentials.toString()); + key: '${_walletId}_mnemonicPassphrase', + value: "", + ); - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance.put( - boxName: walletId, - key: 'receivingAddresses', - value: [_credentials.address.toString()]); - await DB.instance - .put(boxName: walletId, key: "receivingIndex", value: 0); - await DB.instance - .put(boxName: walletId, key: "changeIndex", value: 0); - await DB.instance.put( - boxName: walletId, - key: 'blocked_tx_hashes', - value: ["0xdefault"], - ); // A list of transaction hashes to represent frozen utxos in wallet - // initialize address book entries - await DB.instance.put( - boxName: walletId, - key: 'addressBookEntries', - value: {}); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); + String privateKey = getPrivateKey(mnemonic, ""); + _credentials = web3.EthPrivateKey.fromHex(privateKey); + + final address = Address( + walletId: walletId, value: _credentials.address.toString(), + publicKey: [], // maybe store address bytes here? seems a waste of space though + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); } bool _isConnected = false; @@ -310,43 +349,47 @@ class EthereumWallet extends CoinServiceAPI { @override Future> get mnemonic => _getMnemonicList(); - Future get chainHeight async { - Web3Client client = await getEthClient(); - try { - final result = await client.getBlockNumber(); + @override + Future get mnemonicString => + _secureStore.read(key: '${_walletId}_mnemonic'); - return result; + @override + Future get mnemonicPassphrase => _secureStore.read( + key: '${_walletId}_mnemonicPassphrase', + ); + + Future get chainHeight async { + web3.Web3Client client = getEthClient(); + try { + final height = await client.getBlockNumber(); + await updateCachedChainHeight(height); + if (height > storedChainHeight) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "Updated current chain height in $walletId $walletName!", + walletId, + ), + ); + } + return height; } catch (e, s) { Logging.instance.log("Exception caught in chainHeight: $e\n$s", level: LogLevel.Error); - return -1; + return storedChainHeight; } } - int get storedChainHeight { - final storedHeight = DB.instance - .get(boxName: walletId, key: "storedChainHeight") as int?; - return storedHeight ?? 0; - } - - Future updateStoredChainHeight({required int newHeight}) async { - await DB.instance.put( - boxName: walletId, key: "storedChainHeight", value: newHeight); - } - - Future> _getMnemonicList() async { - final mnemonicString = - await _secureStore.read(key: '${_walletId}_mnemonic'); - if (mnemonicString == null) { - return []; - } - final List data = mnemonicString.split(' '); - return data; - } - @override - // TODO: implement pendingBalance - Not needed since we don't use UTXOs to get a balance - Future get pendingBalance => throw UnimplementedError(); + int get storedChainHeight => getCachedChainHeight(); + + Future> _getMnemonicList() async { + final _mnemonicString = await mnemonicString; + if (_mnemonicString == null) { + return []; + } + final List data = _mnemonicString.split(' '); + return data; + } @override Future> prepareSend( @@ -371,9 +414,8 @@ class EthereumWallet extends CoinServiceAPI { final feeEstimate = await estimateFeeFor(satoshiAmount, fee); bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(await availableBalance, coin); - if (satoshiAmount == balance) { + final availableBalance = balance.spendable; + if (satoshiAmount == availableBalance) { isSendAll = true; } @@ -393,30 +435,51 @@ class EthereumWallet extends CoinServiceAPI { } @override - Future recoverFromMnemonic( - {required String mnemonic, - required int maxUnusedAddressGap, - required int maxNumberOfIndexesToCheck, - required int height}) async { + Future recoverFromMnemonic({ + required String mnemonic, + String? mnemonicPassphrase, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { longMutex = true; final start = DateTime.now(); try { - if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) { + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await mnemonicString) != null || + (await this.mnemonicPassphrase) != null) { longMutex = false; throw Exception("Attempted to overwrite mnemonic on restore!"); } await _secureStore.write( key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _secureStore.write( + key: '${_walletId}_mnemonicPassphrase', + value: mnemonicPassphrase ?? "", + ); - String privateKey = getPrivateKey(mnemonic); - _credentials = EthPrivateKey.fromHex(privateKey); + String privateKey = + getPrivateKey(mnemonic.trim(), mnemonicPassphrase ?? ""); + _credentials = web3.EthPrivateKey.fromHex(privateKey); - await DB.instance - .put(boxName: walletId, key: "id", value: _walletId); - await DB.instance - .put(boxName: walletId, key: "isFavorite", value: false); + final address = Address( + walletId: walletId, value: _credentials.address.toString(), + publicKey: [], // maybe store address bytes here? seems a waste of space though + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); } catch (e, s) { Logging.instance.log( "Exception rethrown from recoverFromMnemonic(): $e\n$s", @@ -432,8 +495,20 @@ class EthereumWallet extends CoinServiceAPI { level: LogLevel.Info); } + Future> _fetchAllOwnAddresses() => db + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group((q) => q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.change)) + .findAll(); + Future refreshIfThereIsNewData() async { - Web3Client client = await getEthClient(); + web3.Web3Client client = getEthClient(); if (longMutex) return false; if (_hasCalledExit) return false; final currentChainHeight = await chainHeight; @@ -462,18 +537,24 @@ class EthereumWallet extends CoinServiceAPI { if (!needsRefresh) { var allOwnAddresses = await _fetchAllOwnAddresses(); AddressTransaction addressTransactions = await fetchAddressTransactions( - allOwnAddresses.elementAt(0), "txlist"); - final txData = await transactionData; + allOwnAddresses.elementAt(0).value, "txlist"); if (addressTransactions.message == "OK") { final allTxs = addressTransactions.result; - allTxs.forEach((element) { - if (txData.findTransaction(element["hash"] as String) == null) { + for (final element in allTxs) { + final txid = element["hash"] as String; + if ((await db + .getTransactions(walletId) + .filter() + .txidMatches(txid) + .findFirst()) == + null) { Logging.instance.log( - " txid not found in address history already ${element["hash"]}", + " txid not found in address history already ${element['hash']}", level: LogLevel.Info); needsRefresh = true; + break; } - }); + } } } return needsRefresh; @@ -485,16 +566,25 @@ class EthereumWallet extends CoinServiceAPI { } } - Future getAllTxsToWatch( - TransactionData txData, - ) async { + Future getAllTxsToWatch() async { if (_hasCalledExit) return; - List unconfirmedTxnsToNotifyPending = []; - List unconfirmedTxnsToNotifyConfirmed = []; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; - for (final chunk in txData.txChunks) { - for (final tx in chunk.transactions) { - if (tx.confirmedStatus) { + final currentChainHeight = await chainHeight; + + final txCount = await db.getTransactions(walletId).count(); + + const paginateLimit = 50; + + for (int i = 0; i < txCount; i += paginateLimit) { + final transactions = await db + .getTransactions(walletId) + .offset(i) + .limit(paginateLimit) + .findAll(); + for (final tx in transactions) { + if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { // get all transactions that were notified as pending but not as confirmed if (txTracker.wasNotifiedPending(tx.txid) && !txTracker.wasNotifiedConfirmed(tx.txid)) { @@ -511,31 +601,33 @@ class EthereumWallet extends CoinServiceAPI { // notify on unconfirmed transactions for (final tx in unconfirmedTxnsToNotifyPending) { - if (tx.txType == "Received") { + final confirmations = tx.getConfirmations(currentChainHeight); + + if (tx.type == TransactionType.incoming) { unawaited(NotificationApi.showNotification( title: "Incoming transaction", body: walletName, walletId: walletId, iconAssetName: Assets.svg.iconFor(coin: coin), date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, coinName: coin.name, txid: tx.txid, - confirmations: tx.confirmations, + confirmations: confirmations, requiredConfirmations: MINIMUM_CONFIRMATIONS, )); await txTracker.addNotifiedPending(tx.txid); - } else if (tx.txType == "Sent") { + } else if (tx.type == TransactionType.outgoing) { unawaited(NotificationApi.showNotification( title: "Sending transaction", body: walletName, walletId: walletId, iconAssetName: Assets.svg.iconFor(coin: coin), date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), - shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS, + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, coinName: coin.name, txid: tx.txid, - confirmations: tx.confirmations, + confirmations: confirmations, requiredConfirmations: MINIMUM_CONFIRMATIONS, )); await txTracker.addNotifiedPending(tx.txid); @@ -544,7 +636,7 @@ class EthereumWallet extends CoinServiceAPI { // notify on confirmed for (final tx in unconfirmedTxnsToNotifyConfirmed) { - if (tx.txType == "Received") { + if (tx.type == TransactionType.incoming) { unawaited(NotificationApi.showNotification( title: "Incoming transaction confirmed", body: walletName, @@ -555,7 +647,7 @@ class EthereumWallet extends CoinServiceAPI { coinName: coin.name, )); await txTracker.addNotifiedConfirmed(tx.txid); - } else if (tx.txType == "Sent") { + } else if (tx.type == TransactionType.outgoing) { unawaited(NotificationApi.showNotification( title: "Outgoing transaction confirmed", body: walletName, @@ -601,14 +693,9 @@ class EthereumWallet extends CoinServiceAPI { .log("cached height: $storedHeight", level: LogLevel.Info); if (currentHeight != storedHeight) { - if (currentHeight != -1) { - // -1 failed to fetch current height - unawaited(updateStoredChainHeight(newHeight: currentHeight)); - } - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); - final newTxData = _fetchTransactionData(); + final newTxDataFuture = _refreshTransactions(); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.50, walletId)); @@ -616,17 +703,16 @@ class EthereumWallet extends CoinServiceAPI { GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.60, walletId)); - _transactionData = Future(() => newTxData); - GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.70, walletId)); _feeObject = Future(() => feeObj); GlobalEventBus.instance .fire(RefreshPercentChangedEvent(0.80, walletId)); - final allTxsToWatch = getAllTxsToWatch(await newTxData); + final allTxsToWatch = getAllTxsToWatch(); await Future.wait([ - newTxData, + updateBalance(), + newTxDataFuture, feeObj, allTxsToWatch, ]); @@ -678,18 +764,9 @@ class EthereumWallet extends CoinServiceAPI { } } - @override - Future send( - {required String toAddress, - required int amount, - Map args = const {}}) { - // TODO: implement send - throw UnimplementedError(); - } - @override Future testNetworkConnection() async { - Web3Client client = await getEthClient(); + web3.Web3Client client = getEthClient(); try { final result = await client.isListeningForNetwork(); return result; @@ -710,24 +787,6 @@ class EthereumWallet extends CoinServiceAPI { } } - @override - Future get totalBalance async { - Web3Client client = await getEthClient(); - EtherAmount ethBalance = await client.getBalance(_credentials.address); - return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString()); - } - - @override - Future get transactionData => - _transactionData ??= _fetchTransactionData(); - Future? _transactionData; - - TransactionData? cachedTxData; - - @override - // TODO: implement unspentOutputs - NOT NEEDED, ETH DOES NOT USE UTXOs - Future> get unspentOutputs => throw UnimplementedError(); - @override Future updateNode(bool shouldRefresh) async { _ethNode = NodeService(secureStorageInterface: _secureStore) @@ -749,137 +808,114 @@ class EthereumWallet extends CoinServiceAPI { return isValidEthereumAddress(address); } - Future _fetchTransactionData() async { + Future _refreshTransactions() async { String thisAddress = await currentReceivingAddress; - final cachedTransactions = - DB.instance.get(boxName: walletId, key: 'latest_tx_model') - as TransactionData?; - int latestTxnBlockHeight = - DB.instance.get(boxName: walletId, key: "storedTxnDataHeight") - as int? ?? - 0; - - final priceData = - await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency); - Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; - final List> midSortedArray = []; AddressTransaction txs = await fetchAddressTransactions(thisAddress, "txlist"); if (txs.message == "OK") { final allTxs = txs.result; - allTxs.forEach((element) { - Map midSortedTx = {}; - // create final tx map - midSortedTx["txid"] = element["hash"]; - int confirmations = int.parse(element['confirmations'].toString()); - + final List> txnsData = []; + for (final element in allTxs) { int transactionAmount = int.parse(element['value'].toString()); - const decimal = 18; //Eth has up to 18 decimal places - final transactionAmountInDecimal = - transactionAmount / (pow(10, decimal)); - - //Convert to satoshi, default display for other coins - final satAmount = Format.decimalAmountToSatoshis( - Decimal.parse(transactionAmountInDecimal.toString()), coin); - - midSortedTx["confirmed_status"] = - (confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS); - midSortedTx["confirmations"] = confirmations; - midSortedTx["timestamp"] = element["timeStamp"]; + bool isIncoming; + bool txFailed = false; if (checksumEthereumAddress(element["from"].toString()) == thisAddress) { - midSortedTx["txType"] = (int.parse(element["isError"] as String) == 0) - ? "Sent" - : "Send Failed"; + if (!(int.parse(element["isError"] as String) == 0)) { + txFailed = true; + } + isIncoming = false; } else { - midSortedTx["txType"] = "Received"; + isIncoming = true; } - midSortedTx["amount"] = satAmount; - final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal(scaleOnInfinitePrecision: 2) - .toStringAsFixed(2); - //Calculate fees (GasLimit * gasPrice) int txFee = int.parse(element['gasPrice'].toString()) * int.parse(element['gasUsed'].toString()); - final txFeeDecimal = txFee / (pow(10, decimal)); - midSortedTx["worthNow"] = worthNow; - midSortedTx["worthAtBlockTimestamp"] = worthNow; - midSortedTx["aliens"] = []; - midSortedTx["fees"] = Format.decimalAmountToSatoshis( - Decimal.parse(txFeeDecimal.toString()), coin); - midSortedTx["address"] = element["to"]; - midSortedTx["inputSize"] = 1; - midSortedTx["outputSize"] = 1; - midSortedTx["inputs"] = []; - midSortedTx["outputs"] = []; - midSortedTx["height"] = int.parse(element['blockNumber'].toString()); + final String addressString = element["to"] as String; + final int height = int.parse(element['blockNumber'].toString()); - midSortedArray.add(midSortedTx); - }); - } + final txn = Transaction( + walletId: walletId, + txid: element["hash"] as String, + timestamp: element["timeStamp"] as int, + type: + isIncoming ? TransactionType.incoming : TransactionType.outgoing, + subType: TransactionSubType.none, + amount: transactionAmount, + fee: txFee, + height: height, + isCancelled: txFailed, + isLelantus: false, + slateId: null, + otherData: null, + inputs: [], + outputs: [], + ); - midSortedArray.sort((a, b) => - (int.parse(b['timestamp'].toString())) - - (int.parse(a['timestamp'].toString()))); + Address? transactionAddress = await db + .getAddresses(walletId) + .filter() + .valueEqualTo(addressString) + .findFirst(); - // buildDateTimeChunks - final Map result = {"dateTimeChunks": []}; - final dateArray = []; + if (transactionAddress == null) { + if (isIncoming) { + transactionAddress = Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + } else { + final myRcvAddr = await currentReceivingAddress; + final isSentToSelf = myRcvAddr == addressString; - for (int i = 0; i < midSortedArray.length; i++) { - final txObject = midSortedArray[i]; - final date = - extractDateFromTimestamp(int.parse(txObject['timestamp'].toString())); - final txTimeArray = [txObject["timestamp"], date]; - - if (dateArray.contains(txTimeArray[1])) { - result["dateTimeChunks"].forEach((dynamic chunk) { - if (extractDateFromTimestamp( - int.parse(chunk['timestamp'].toString())) == - txTimeArray[1]) { - if (chunk["transactions"] == null) { - chunk["transactions"] = >[]; - } - chunk["transactions"].add(txObject); + transactionAddress = Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: isSentToSelf + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); } - }); - } else { - dateArray.add(txTimeArray[1]); - final chunk = { - "timestamp": txTimeArray[0], - "transactions": [txObject], - }; - result["dateTimeChunks"].add(chunk); + } + + txnsData.add(Tuple2(txn, transactionAddress)); } + await db.addNewTransactionData(txnsData, walletId); + + // quick hack to notify manager to call notifyListeners if + // transactions changed + if (txnsData.isNotEmpty) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "Transactions updated/added for: $walletId $walletName ", + walletId, + ), + ); + } + } else { + Logging.instance.log( + "Failed to refresh transactions for ${coin.prettyName} $walletName $walletId: $txs", + level: LogLevel.Warning, + ); } - - final transactionsMap = cachedTransactions?.getAllTransactions() ?? {}; - transactionsMap - .addAll(TransactionData.fromJson(result).getAllTransactions()); - - final txModel = TransactionData.fromMap(transactionsMap); - - await DB.instance.put( - boxName: walletId, - key: 'storedTxnDataHeight', - value: latestTxnBlockHeight); - await DB.instance.put( - boxName: walletId, key: 'latest_tx_model', value: txModel); - - cachedTxData = txModel; - return txModel; } - @override - set walletName(String newName) => _walletName = newName; - void stopNetworkAlivePinging() { _networkAliveTimer?.cancel(); _networkAliveTimer = null; diff --git a/lib/services/tokens/ethereum/ethereum_token.dart b/lib/services/tokens/ethereum/ethereum_token.dart index c19ac7bbb..5bcb2870e 100644 --- a/lib/services/tokens/ethereum/ethereum_token.dart +++ b/lib/services/tokens/ethereum/ethereum_token.dart @@ -1,26 +1,25 @@ import 'dart:convert'; 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:http/http.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/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/tokens/token_service.dart'; -import 'package:stackwallet/utilities/format.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'; -import 'package:stackwallet/models/paymint/transactions_model.dart' as models; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/format.dart'; import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart' as transaction; -import 'package:stackwallet/models/node_model.dart'; -import 'package:stackwallet/utilities/default_nodes.dart'; -import 'package:stackwallet/services/node_service.dart'; - class AbiRequestResponse { final String message; final String result; @@ -195,7 +194,8 @@ class EthereumToken extends TokenServiceAPI { String mnemonicString = mnemonic.join(' '); //Get private key for given mnemonic - String privateKey = getPrivateKey(mnemonicString); + // TODO: replace empty string with actual passphrase + String privateKey = getPrivateKey(mnemonicString, ""); _credentials = EthPrivateKey.fromHex(privateKey); _contract = DeployedContract( @@ -225,7 +225,8 @@ class EthereumToken extends TokenServiceAPI { String mnemonicString = mnemonic.join(' '); //Get private key for given mnemonic - String privateKey = getPrivateKey(mnemonicString); + // TODO: replace empty string with actual passphrase + String privateKey = getPrivateKey(mnemonicString, ""); _credentials = EthPrivateKey.fromHex(privateKey); _contract = DeployedContract( diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 9ab429dea..194ea2753 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -296,7 +296,7 @@ class _PNG { "assets/images/${Theme.of(context).extension()!.themeType.name}/litecoin.png"; String epicCash(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/epic-cash.png"; - String epicCash(BuildContext context) => + String ethereum(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/ethereum.png"; String bitcoincash(BuildContext context) => "assets/images/${Theme.of(context).extension()!.themeType.name}/bitcoincash.png"; @@ -325,7 +325,7 @@ class _PNG { case Coin.epicCash: return epicCash(context); case Coin.ethereum: - return ethereum; + return ethereum(context); case Coin.firo: return firo(context); case Coin.firoTestNet: diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 40440b735..dbb50a554 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -18,19 +18,21 @@ abstract class Constants { static void exchangeForExperiencedUsers(int count) { enableExchange = Util.isDesktop || Platform.isAndroid || count > 5 || !Platform.isIOS; + enableBuy = + Util.isDesktop || Platform.isAndroid || count > 5 || !Platform.isIOS; } static bool enableExchange = Util.isDesktop || !Platform.isIOS; - static bool enableBuy = - true; // true for development, TODO change to "Util.isDesktop || !Platform.isIOS;" as above or even just = enableExchange + static bool enableBuy = Util.isDesktop || !Platform.isIOS; - //TODO: correct for monero? + static const int _satsPerCoinEthereum = 1000000000000000000; static const int _satsPerCoinMonero = 1000000000000; static const int _satsPerCoinWownero = 100000000000; static const int _satsPerCoin = 100000000; static const int _decimalPlaces = 8; static const int _decimalPlacesWownero = 11; static const int _decimalPlacesMonero = 12; + static const int _decimalPlacesEthereum = 18; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -57,7 +59,6 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: - case Coin.ethereum: case Coin.namecoin: case Coin.particl: return _satsPerCoin; @@ -67,6 +68,9 @@ abstract class Constants { case Coin.monero: return _satsPerCoinMonero; + + case Coin.ethereum: + return _satsPerCoinEthereum; } } @@ -83,7 +87,6 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: - case Coin.ethereum: case Coin.namecoin: case Coin.particl: return _decimalPlaces; @@ -93,6 +96,9 @@ abstract class Constants { case Coin.monero: return _decimalPlacesMonero; + + case Coin.ethereum: + return _decimalPlacesEthereum; } } diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 93edcc3e7..d65ccb976 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -187,6 +187,7 @@ extension CoinExt on Coin { case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: + case Coin.ethereum: return true; case Coin.firo: diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 53db94e96..8aa519727 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -5,6 +5,7 @@ enum DerivePathType { bch44, bip49, bip84, + eth, } extension DerivePathTypeExt on DerivePathType { @@ -26,6 +27,9 @@ extension DerivePathTypeExt on DerivePathType { case Coin.particl: return DerivePathType.bip84; + case Coin.ethereum: // TODO: do we need something here? + return DerivePathType.eth; + case Coin.epicCash: case Coin.monero: case Coin.wownero: diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index d31fbbccb..78dff426b 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -1,12 +1,12 @@ 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 '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; @@ -26,6 +26,15 @@ class AddressTransaction { status: json['status'] as String, ); } + + @override + String toString() { + return "AddressTransaction: {" + "\n\t message: $message," + "\n\t status: $status," + "\n\t result: $result," + "\n}"; + } } class GasTracker { @@ -52,7 +61,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 _hdPath = "m/44'/60'/0'/0"; +const hdPathEthereum = "m/44'/60'/0'/0"; const _gasTrackerUrl = "https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle"; @@ -106,16 +115,16 @@ Future> getWalletTokens(String address) async { return []; } -String getPrivateKey(String mnemonic) { +String getPrivateKey(String mnemonic, String mnemonicPassphrase) { final isValidMnemonic = bip39.validateMnemonic(mnemonic); if (!isValidMnemonic) { throw 'Invalid mnemonic'; } - final seed = bip39.mnemonicToSeed(mnemonic); + final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase); final root = bip32.BIP32.fromSeed(seed); const index = 0; - final addressAtIndex = root.derivePath("$_hdPath/$index"); + final addressAtIndex = root.derivePath("$hdPathEthereum/$index"); return HEX.encode(addressAtIndex.privateKey as List); } diff --git a/pubspec.lock b/pubspec.lock index 81c81bb2c..1fe0fa2b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1434,6 +1434,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + string_to_hex: + dependency: "direct main" + description: + name: string_to_hex + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2" string_validator: dependency: "direct main" description: